diff --git a/include/ant.h b/include/ant.h index 96b8b8d..629a483 100644 --- a/include/ant.h +++ b/include/ant.h @@ -75,6 +75,7 @@ void js_setthis(ant_t *, jsval_t); jsval_t js_getcurrentfunc(ant_t *); jsval_t js_get(ant_t *, jsval_t, const char *); jsval_t js_getprop_proto(ant_t *, jsval_t, const char *); +jsval_t js_getprop_fallback(ant_t *js, jsval_t obj, const char *name); jsoff_t js_arr_len(struct js *js, jsval_t arr); jsval_t js_arr_get(struct js *js, jsval_t arr, jsoff_t idx); @@ -94,6 +95,7 @@ uint64_t js_sym_id(jsval_t sym); const char *js_sym_desc(ant_t *js, jsval_t sym); jsval_t js_mksym_for(ant_t *, const char *key); +jsval_t js_symbol_to_string(struct js *js, jsval_t sym); const char *js_sym_key(jsval_t sym); jsval_t js_mkobj(ant_t *); diff --git a/include/internal.h b/include/internal.h index adaa949..83bd2a5 100644 --- a/include/internal.h +++ b/include/internal.h @@ -41,6 +41,7 @@ struct js { jsval_t tval; // holds last parsed numeric or string literal value jsval_t scope; // current scope jsval_t global; // global root object + jsval_t object; // global object prototype jsval_t this_val; // 'this' value for currently executing function jsval_t super_val; // 'super' value for class methods jsval_t new_target; // constructor called with 'new', undefined otherwise diff --git a/include/modules/symbol.h b/include/modules/symbol.h index ef2e037..cc4644a 100644 --- a/include/modules/symbol.h +++ b/include/modules/symbol.h @@ -14,6 +14,12 @@ const char *get_iterator_sym_key(void); const char *get_asyncIterator_sym_key(void); const char *get_toStringTag_sym_key(void); const char *get_observable_sym_key(void); -const char *get_symbol_description_from_key(const char *sym_key, size_t key_len); +const char *get_toPrimitive_sym_key(void); +const char *get_hasInstance_sym_key(void); + +const char *get_symbol_description_from_key( + const char *sym_key, + size_t key_len +); #endif diff --git a/src/ant.c b/src/ant.c index ff30c90..7a01e5c 100644 --- a/src/ant.c +++ b/src/ant.c @@ -4551,8 +4551,7 @@ static jsoff_t lkp_with_getter(struct js *js, jsval_t obj, const char *buf, size *has_getter_out = false; *getter_out = js_mkundef(); - jsval_t current = obj; - while (vtype(current) == T_OBJ || vtype(current) == T_FUNC) { + for (jsval_t current = obj; is_object_type(current); ) { jsoff_t current_off = (jsoff_t)vdata(current); descriptor_entry_t *desc = lookup_descriptor(current_off, buf, len); @@ -4566,7 +4565,7 @@ static jsoff_t lkp_with_getter(struct js *js, jsval_t obj, const char *buf, size if (prop_off != 0) return prop_off; jsval_t proto = get_proto(js, current); - if (vtype(proto) != T_OBJ && vtype(proto) != T_FUNC) break; + if (!is_object_type(proto)) break; current = proto; } @@ -4627,14 +4626,13 @@ static jsval_t call_proto_accessor(struct js *js, jsval_t prim, jsval_t accessor jsval_t js_get_proto(struct js *js, jsval_t obj) { uint8_t t = vtype(obj); - if (t != T_OBJ && t != T_ARR && t != T_FUNC && t != T_PROMISE) return js_mknull(); + if (!is_object_type(obj)) return js_mknull(); jsval_t as_obj = (t == T_OBJ) ? obj : mkval(T_OBJ, vdata(obj)); jsval_t proto = get_slot(js, as_obj, SLOT_PROTO); - uint8_t pt = vtype(proto); - if (pt == T_OBJ || pt == T_ARR || pt == T_FUNC) return proto; + if (is_object_type(proto)) return proto; - if (t == T_FUNC || t == T_ARR || t == T_PROMISE) return get_prototype_for_type(js, t); + if (t != T_OBJ) return get_prototype_for_type(js, t); return js_mknull(); } @@ -4644,7 +4642,7 @@ static jsval_t get_proto(struct js *js, jsval_t obj) { void js_set_proto(struct js *js, jsval_t obj, jsval_t proto) { uint8_t t = vtype(obj); - if (t != T_OBJ && t != T_ARR && t != T_FUNC) return; + if (!is_object_type(obj)) return; jsval_t as_obj = (t == T_OBJ) ? obj : mkval(T_OBJ, vdata(obj)); set_slot(js, as_obj, SLOT_PROTO, proto); @@ -4751,6 +4749,32 @@ jsoff_t lkp_proto(struct js *js, jsval_t obj, const char *key, size_t len) { return 0; } +static jsval_t getprop_any(struct js *js, jsval_t obj, const char *key, size_t key_len) { + uint8_t t = vtype(obj); + + if (t == T_STR && key_len == 6 && memcmp(key, "length", 6) == 0) { + jsoff_t byte_len; + jsoff_t str_off = vstr(js, obj, &byte_len); + return tov(D(utf16_strlen((const char *)&js->mem[str_off], byte_len))); + } + + if (t == T_STR || t == T_NUM || t == T_BOOL || t == T_BIGINT) { + jsoff_t off = lkp_proto(js, obj, key, key_len); + if (off != 0) return resolveprop(js, mkval(T_PROP, off)); + return js_mkundef(); + } + + if (t == T_OBJ || t == T_ARR || t == T_FUNC) { + jsval_t as_obj = (t == T_OBJ) ? obj : mkval(T_OBJ, vdata(obj)); + jsoff_t off = lkp(js, as_obj, key, key_len); + if (off != 0) return resolveprop(js, mkval(T_PROP, off)); + off = lkp_proto(js, obj, key, key_len); + if (off != 0) return resolveprop(js, mkval(T_PROP, off)); + } + + return js_mkundef(); +} + static jsval_t try_dynamic_getter(struct js *js, jsval_t obj, const char *key, size_t key_len) { jsoff_t obj_off = (jsoff_t)vdata(obj); dynamic_accessors_t *entry = NULL; @@ -5355,6 +5379,13 @@ static jsval_t do_dot_op(struct js *js, jsval_t l, jsval_t r) { if (desc) return js_mkstr(js, desc, strlen(desc)); return js_mkundef(); } + + jsval_t sym_proto = get_ctor_proto(js, "Symbol", 6); + if (vtype(sym_proto) == T_OBJ) { + jsoff_t off = lkp(js, sym_proto, ptr, plen); + if (off != 0) return resolveprop(js, mkval(T_PROP, off)); + } + return js_mkundef(); } @@ -5563,16 +5594,36 @@ static jsval_t bind_destruct_pattern(struct js *js, const char *p, jsoff_t len, jsoff_t var_pos = pos, var_len = name_len; jsoff_t src_pos = pos, src_len = name_len; + pos += name_len; pos = skiptonext(p, len, pos, NULL); + bool is_nested = false; + jsoff_t nested_start = 0, nested_len = 0; + if (!is_arr && !is_rest && pos < len && p[pos] == ':') { pos = skiptonext(p, len, pos + 1, NULL); - jsoff_t rlen = 0; - if (parseident(&p[pos], len - pos, &rlen) == TOK_IDENTIFIER) { - var_pos = pos; var_len = rlen; - pos += rlen; + + if (pos < len && (p[pos] == '{' || p[pos] == '[')) { + is_nested = true; + nested_start = pos; + char open = p[pos], close = (open == '{') ? '}' : ']'; + int depth = 1; + pos++; + while (pos < len && depth > 0) { + if (p[pos] == open) depth++; + else if (p[pos] == close) depth--; + pos++; + } + nested_len = pos - nested_start; pos = skiptonext(p, len, pos, NULL); + } else { + jsoff_t rlen = 0; + if (parseident(&p[pos], len - pos, &rlen) == TOK_IDENTIFIER) { + var_pos = pos; var_len = rlen; + pos += rlen; + pos = skiptonext(p, len, pos, NULL); + } } } @@ -5583,13 +5634,16 @@ static jsval_t bind_destruct_pattern(struct js *js, const char *p, jsoff_t len, jsoff_t alen = js_arr_len(js, val); for (jsoff_t i = idx; i < alen; i++) js_arr_push(js, rest, js_arr_get(js, val, i)); prop_val = rest; - } else if (is_rest) { - prop_val = mkobj(js, 0); - } else if (is_arr) { - prop_val = js_arr_get(js, val, idx); - } else { - jsoff_t off = lkp(js, val, &p[src_pos], src_len); - prop_val = off > 0 ? resolveprop(js, mkval(T_PROP, off)) : js_mkundef(); + } else if (is_rest) prop_val = mkobj(js, 0); + else if (is_arr) prop_val = js_arr_get(js, val, idx); + else prop_val = getprop_any(js, val, &p[src_pos], src_len); + + if (is_nested) { + jsval_t r = bind_destruct_pattern(js, &p[nested_start], nested_len, prop_val, scope); + if (is_err(r)) return r; + idx++; pos = skiptonext(p, len, pos, NULL); + if (pos < len && p[pos] == ',') pos++; + continue; } if (is_rest) goto bind; @@ -6494,45 +6548,74 @@ restore_state: return res; } -static jsval_t js_call_toString(struct js *js, jsval_t value) { - jsoff_t ts_off = lkp(js, value, "toString", 8); - if (ts_off == 0) ts_off = lkp_proto(js, value, "toString", 8); - if (ts_off == 0) goto fallback; +static bool js_try_call_method(struct js *js, jsval_t obj, const char *method, size_t method_len, jsval_t *args, int nargs, jsval_t *out_result) { + jsval_t getter = js_mkundef(); bool has_getter = false; + jsoff_t off = lkp_with_getter(js, obj, method, method_len, &getter, &has_getter); - jsval_t ts_func = resolveprop(js, mkval(T_PROP, ts_off)); - uint8_t ft = vtype(ts_func); - if (ft != T_FUNC && ft != T_CFUNC) goto fallback; + jsval_t fn; + if (has_getter) { + fn = call_proto_accessor(js, obj, getter, true, NULL, 0, false); + if (is_err(fn)) { *out_result = fn; return true; } + } else if (off != 0) { + fn = resolveprop(js, mkval(T_PROP, off)); + } else return false; + + uint8_t ft = vtype(fn); + if (ft != T_FUNC && ft != T_CFUNC) return false; + + js_parse_state_t saved_state; + JS_SAVE_STATE(js, saved_state); + uint8_t saved_flags = js->flags; jsval_t saved_this = js->this_val; - js->this_val = value; + js->this_val = obj; + push_this(obj); + jsval_t result; + if (ft == T_CFUNC) result = ((jsval_t (*)(struct js *, jsval_t *, int))vdata(fn))(js, args, nargs); + else result = call_js_with_args(js, fn, args, nargs); - if (ft == T_CFUNC) { - result = ((jsval_t (*)(struct js *, jsval_t *, int))vdata(ts_func))(js, NULL, 0); - } else { - jsval_t func_obj = mkval(T_OBJ, vdata(ts_func)); - jsoff_t fnlen; - const char *code_str = get_func_code(js, func_obj, &fnlen); - if (!code_str) goto restore_fallback; - - jsval_t closure_scope = get_slot(js, func_obj, SLOT_SCOPE); - if (vtype(closure_scope) == T_UNDEF) closure_scope = js->scope; - - result = call_js(js, code_str, fnlen, closure_scope); - } + bool had_throw = (js->flags & F_THROW) != 0; + jsval_t thrown = js->thrown_value; + pop_this(); js->this_val = saved_this; + + JS_RESTORE_STATE(js, saved_state); + js->flags = saved_flags; + + if (had_throw) { + js->flags |= F_THROW; + js->thrown_value = thrown; + } + + *out_result = result; + return true; +} + +static jsval_t js_call_method(struct js *js, jsval_t obj, const char *method, size_t method_len, jsval_t *args, int nargs) { + jsval_t result; + if (!js_try_call_method(js, obj, method, method_len, args, nargs, &result)) return js_mkundef(); + return result; +} + +static jsval_t js_call_toString(struct js *js, jsval_t value) { + jsval_t result = js_call_method(js, value, "toString", 8, NULL, 0); + + if (is_err(result)) return result; if (vtype(result) == T_STR) return result; uint8_t rtype = vtype(result); + if (rtype == T_UNDEF) { + goto fallback; + } + if (rtype != T_OBJ && rtype != T_ARR && rtype != T_FUNC) { char buf[256]; size_t len = tostr(js, result, buf, sizeof(buf)); return js_mkstr(js, buf, len); } -restore_fallback: - js->this_val = saved_this; fallback:; char buf[4096]; size_t len = tostr(js, value, buf, sizeof(buf)); @@ -6540,34 +6623,64 @@ fallback:; } static jsval_t js_call_valueOf(struct js *js, jsval_t value) { - jsoff_t off = lkp(js, value, "valueOf", 7); - if (off == 0) off = lkp_proto(js, value, "valueOf", 7); - if (off == 0) return value; + jsval_t result = js_call_method(js, value, "valueOf", 7, NULL, 0); + if (vtype(result) == T_UNDEF) return value; + return result; +} + +static inline bool is_primitive(jsval_t v) { + uint8_t t = vtype(v); + return t == T_STR || t == T_NUM || t == T_BOOL || t == T_NULL || t == T_UNDEF || t == T_SYMBOL || t == T_BIGINT; +} + +static jsval_t try_exotic_to_primitive(struct js *js, jsval_t value, int hint) { + const char *tp_key = get_toPrimitive_sym_key(); + if (!tp_key || !tp_key[0]) return mkval(T_UNDEF, 0); - jsval_t fn = resolveprop(js, mkval(T_PROP, off)); - uint8_t ft = vtype(fn); - if (ft != T_FUNC && ft != T_CFUNC) return value; + jsoff_t tp_off = lkp(js, value, tp_key, strlen(tp_key)); + if (tp_off == 0) tp_off = lkp_proto(js, value, tp_key, strlen(tp_key)); + if (tp_off == 0) return mkval(T_UNDEF, 0); + + jsval_t tp_fn = resolveprop(js, mkval(T_PROP, tp_off)); + uint8_t ft = vtype(tp_fn); + + if (ft == T_UNDEF) return mkval(T_UNDEF, 0); + if (ft != T_FUNC && ft != T_CFUNC) { + return js_mkerr_typed(js, JS_ERR_TYPE, "Symbol.toPrimitive is not a function"); + } + + const char *hint_str = hint == 1 ? "string" : (hint == 2 ? "number" : "default"); + jsval_t hint_arg = js_mkstr(js, hint_str, strlen(hint_str)); + jsval_t result = js_call_method(js, value, tp_key, strlen(tp_key), &hint_arg, 1); - jsval_t saved = js->this_val; - js->this_val = value; + if (is_err(result) || is_primitive(result)) return result; + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot convert object to primitive value"); +} + +static jsval_t try_ordinary_to_primitive(struct js *js, jsval_t value, int hint) { + static const char *names[] = {"valueOf", "toString"}; + static const size_t lens[] = {7, 8}; + + int first = (hint == 1); jsval_t result; - if (ft == T_CFUNC) { - result = ((jsval_t (*)(struct js *, jsval_t *, int))vdata(fn))(js, NULL, 0); - } else { - jsval_t func_obj = mkval(T_OBJ, vdata(fn)); - jsoff_t fnlen; - const char *code_str = get_func_code(js, func_obj, &fnlen); - if (!code_str) { js->this_val = saved; return value; } - - jsval_t closure_scope = get_slot(js, func_obj, SLOT_SCOPE); - if (vtype(closure_scope) == T_UNDEF) closure_scope = js->scope; - - result = call_js(js, code_str, fnlen, closure_scope); + for (int i = 0; i < 2; i++) { + int idx = first ^ i; + if (js_try_call_method(js, value, names[idx], lens[idx], NULL, 0, &result)) + if (is_err(result) || is_primitive(result)) return result; } - js->this_val = saved; - return result; + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot convert object to primitive value"); +} + +static jsval_t js_to_primitive(struct js *js, jsval_t value, int hint) { + if (is_primitive(value)) return value; + if (!is_object_type(value)) return value; + + jsval_t result = try_exotic_to_primitive(js, value, hint); + if (vtype(result) != T_UNDEF) return result; + + return try_ordinary_to_primitive(js, value, hint); } static inline bool strict_eq_values(struct js *js, jsval_t l, jsval_t r) { @@ -6584,11 +6697,27 @@ static inline bool strict_eq_values(struct js *js, jsval_t l, jsval_t r) { static inline jsval_t coerce_to_str(struct js *js, jsval_t v) { if (vtype(v) == T_STR) return v; - if (vtype(v) == T_ARR) { - char buf[1024]; - size_t len = array_to_string(js, v, buf, sizeof(buf)); - return js_mkstr(js, buf, len); + + if (is_object_type(v)) { + jsval_t prim = js_to_primitive(js, v, 1); + if (is_err(prim)) return prim; + if (vtype(prim) == T_STR) return prim; + return js_tostring_val(js, prim); } + + return js_tostring_val(js, v); +} + +static inline jsval_t coerce_to_str_concat(struct js *js, jsval_t v) { + if (vtype(v) == T_STR) return v; + + if (is_object_type(v)) { + jsval_t prim = js_to_primitive(js, v, 0); + if (is_err(prim)) return prim; + if (vtype(prim) == T_STR) return prim; + return js_tostring_val(js, prim); + } + return js_tostring_val(js, v); } @@ -6660,10 +6789,20 @@ static jsval_t do_op(struct js *js, uint8_t op, jsval_t lhs, jsval_t rhs) { L_TOK_UMINUS: if (vtype(r) == T_BIGINT) return bigint_neg(js, r); + if (is_object_type(r)) { + jsval_t prim = js_to_primitive(js, r, 2); + if (is_err(prim)) return prim; + return tov(-js_to_number(js, prim)); + } return tov(-js_to_number(js, r)); L_TOK_UPLUS: if (vtype(r) == T_BIGINT) return js_mkerr(js, "Cannot convert a BigInt value to a number"); + if (is_object_type(r)) { + jsval_t prim = js_to_primitive(js, r, 2); + if (is_err(prim)) return prim; + return tov(js_to_number(js, prim)); + } return tov(js_to_number(js, r)); L_TOK_TILDA: return tov((double)(~js_to_int32(js_to_number(js, r)))); @@ -6719,9 +6858,9 @@ static jsval_t do_op(struct js *js, uint8_t op, jsval_t lhs, jsval_t rhs) { if (vtype(lu) == T_BIGINT && vtype(ru) == T_BIGINT) return bigint_add(js, lu, ru); if (vtype(lu) == T_BIGINT || vtype(ru) == T_BIGINT) return js_mkerr(js, "Cannot mix BigInt value and other types"); if (is_non_numeric(lu) || is_non_numeric(ru) || (vtype(lu) == T_STR && vtype(ru) == T_STR)) { - jsval_t l_str = coerce_to_str(js, l); + jsval_t l_str = coerce_to_str_concat(js, l); if (is_err(l_str)) return l_str; - jsval_t r_str = coerce_to_str(js, r); + jsval_t r_str = coerce_to_str_concat(js, r); if (is_err(r_str)) return r_str; return do_string_op(js, op, l_str, r_str); } @@ -6890,9 +7029,13 @@ static jsval_t js_template_literal(struct js *js) { if (is_err(expr_result)) return expr_result; expr_result = resolveprop(js, expr_result); + if (vtype(expr_result) == T_SYMBOL) { + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot convert a Symbol value to a string"); + } + if (vtype(expr_result) != T_STR) { - const char *str = js_str(js, expr_result); - expr_result = js_mkstr(js, str, strlen(str)); + expr_result = coerce_to_str(js, expr_result); + if (is_err(expr_result)) return expr_result; } parts[part_count++] = expr_result; @@ -9093,9 +9236,8 @@ static jsval_t js_decl(struct js *js, bool is_const) { jsval_t obj = js_mkundef(); if (exe) { obj = resolveprop(js, v); - if (vtype(obj) != T_OBJ && vtype(obj) != T_ARR) { - return js_mkerr(js, "cannot destructure non-object"); - } + uint8_t ot = vtype(obj); + if (ot == T_NULL || ot == T_UNDEF) return js_mkerr(js, "cannot destructure null or undefined"); } js_parse_state_t end_state; @@ -9176,29 +9318,32 @@ static jsval_t js_decl(struct js *js, bool is_const) { obj_destruct_rest:; jsval_t rest_obj = mkobj(js, 0); if (is_err(rest_obj)) return rest_obj; - jsoff_t scan = loadoff(js, (jsoff_t) vdata(obj)) & ~(3U | FLAGMASK); - while (scan < js->brk && scan != 0) { - jsoff_t header = loadoff(js, scan); - if (is_slot_prop(header)) { scan = next_prop(header); continue; } - - const char *key; jsoff_t klen; - get_prop_key(js, scan, &key, &klen); - bool is_picked = false; - jsoff_t picked_len = js_arr_len(js, picked_keys); - for (jsoff_t i = 0; i < picked_len; i++) { - jsval_t pk = js_arr_get(js, picked_keys, i); - if (vtype(pk) != T_STR) continue; - jsoff_t pklen, pkoff = vstr(js, pk, &pklen); - if (klen == pklen && memcmp(key, &js->mem[pkoff], klen) == 0) { is_picked = true; break; } - } - if (!is_picked && !(klen == STR_PROTO_LEN && memcmp(key, STR_PROTO, STR_PROTO_LEN) == 0)) { - jsval_t val = get_prop_val(js, scan); - jsval_t key_str = js_mkstr(js, key, klen); - if (is_err(key_str)) return key_str; - jsval_t res = setprop(js, rest_obj, key_str, val); - if (is_err(res)) return res; + uint8_t obj_type = vtype(obj); + if (obj_type == T_OBJ || obj_type == T_ARR) { + jsoff_t scan = loadoff(js, (jsoff_t) vdata(obj)) & ~(3U | FLAGMASK); + while (scan < js->brk && scan != 0) { + jsoff_t header = loadoff(js, scan); + if (is_slot_prop(header)) { scan = next_prop(header); continue; } + + const char *key; jsoff_t klen; + get_prop_key(js, scan, &key, &klen); + bool is_picked = false; + jsoff_t picked_len = js_arr_len(js, picked_keys); + for (jsoff_t i = 0; i < picked_len; i++) { + jsval_t pk = js_arr_get(js, picked_keys, i); + if (vtype(pk) != T_STR) continue; + jsoff_t pklen, pkoff = vstr(js, pk, &pklen); + if (klen == pklen && memcmp(key, &js->mem[pkoff], klen) == 0) { is_picked = true; break; } + } + if (!is_picked && !(klen == STR_PROTO_LEN && memcmp(key, STR_PROTO, STR_PROTO_LEN) == 0)) { + jsval_t val = get_prop_val(js, scan); + jsval_t key_str = js_mkstr(js, key, klen); + if (is_err(key_str)) return key_str; + jsval_t res = setprop(js, rest_obj, key_str, val); + if (is_err(res)) return res; + } + scan = next_prop(header); } - scan = next_prop(header); } { const char *vn = &js->code[var_off]; @@ -9214,10 +9359,9 @@ obj_destruct_nested:; if (is_err(sk)) return sk; js_arr_push(js, picked_keys, sk); - jsoff_t poff = lkp(js, obj, &js->code[src_off], src_len); - jsval_t nobj = poff > 0 ? resolveprop(js, mkval(T_PROP, poff)) : js_mkundef(); - + jsval_t nobj = getprop_any(js, obj, &js->code[src_off], src_len); jsoff_t pattern_end = js->pos; + js->pos = nested_pattern_start; js->consumed = 1; @@ -9244,8 +9388,7 @@ obj_destruct_nested:; const char *ivn = &js->code[ivoff]; if (lkp_scope(js, js->scope, ivn, ivlen) > 0) return js_mkerr(js, "'%.*s' already declared", (int)ivlen, ivn); - jsoff_t ipoff = lkp(js, nobj, &js->code[isoff], islen); - jsval_t ival = ipoff > 0 ? resolveprop(js, mkval(T_PROP, ipoff)) : js_mkundef(); + jsval_t ival = getprop_any(js, nobj, &js->code[isoff], islen); jsval_t ix = mkprop(js, js->scope, js_mkstr(js, ivn, ivlen), ival, is_const ? CONSTMASK : 0); if (is_err(ix)) return ix; @@ -9269,9 +9412,7 @@ obj_destruct_simple:; jsval_t sk = js_mkstr(js, &js->code[src_off], src_len); if (is_err(sk)) return sk; js_arr_push(js, picked_keys, sk); - - jsoff_t poff = lkp(js, obj, &js->code[src_off], src_len); - jsval_t pval = poff > 0 ? resolveprop(js, mkval(T_PROP, poff)) : js_mkundef(); + jsval_t pval = getprop_any(js, obj, &js->code[src_off], src_len); if (vtype(pval) == T_UNDEF && default_len > 0) { pval = js_eval_slice(js, default_off, default_len); @@ -11909,19 +12050,40 @@ static jsval_t js_stmt(struct js *js) { return js_stmt_impl(js); } +jsval_t js_symbol_to_string(struct js *js, jsval_t sym) { + const char *desc = js_sym_desc(js, sym); + if (!desc) return js_mkstr(js, "Symbol()", 8); + + size_t desc_len = strlen(desc); + size_t total = 7 + desc_len + 1; + char *buf = malloc(total + 1); + if (!buf) return js_mkerr(js, "out of memory"); + + memcpy(buf, "Symbol(", 7); + memcpy(buf + 7, desc, desc_len); + buf[7 + desc_len] = ')'; + buf[total] = '\0'; + + jsval_t result = js_mkstr(js, buf, total); + free(buf); + return result; +} + static jsval_t builtin_String(struct js *js, jsval_t *args, int nargs) { jsval_t sval; + if (nargs == 0) { sval = js_mkstr(js, "", 0); + } else if (vtype(args[0]) == T_STR) { + sval = args[0]; + } else if (vtype(args[0]) == T_SYMBOL) { + sval = js_symbol_to_string(js, args[0]); + if (is_err(sval)) return sval; } else { - jsval_t arg = args[0]; - if (vtype(arg) == T_STR) { - sval = arg; - } else { - const char *str = js_str(js, arg); - sval = js_mkstr(js, str, strlen(str)); - } + sval = coerce_to_str(js, args[0]); + if (is_err(sval)) return sval; } + jsval_t string_proto = js_get_ctor_proto(js, "String", 6); if (is_unboxed_obj(js, js->this_val, string_proto)) { set_slot(js, js->this_val, SLOT_PRIMITIVE, sval); @@ -12583,6 +12745,46 @@ static jsval_t builtin_Error(struct js *js, jsval_t *args, int nargs) { return this_val; } +static jsval_t builtin_Error_toString(struct js *js, jsval_t *args, int nargs) { + jsval_t this_val = js_getthis(js); + + jsval_t name = js_get(js, this_val, "name"); + if (vtype(name) == T_UNDEF) name = js_mkstr(js, "Error", 5); + else if (vtype(name) != T_STR) { + const char *s = js_str(js, name); + name = js_mkstr(js, s, strlen(s)); + } + + jsval_t msg = js_get(js, this_val, "message"); + if (vtype(msg) == T_UNDEF) msg = js_mkstr(js, "", 0); + else if (vtype(msg) != T_STR) { + const char *s = js_str(js, msg); + msg = js_mkstr(js, s, strlen(s)); + } + + jsoff_t name_len, msg_len; + jsoff_t name_off = vstr(js, name, &name_len); + jsoff_t msg_off = vstr(js, msg, &msg_len); + + const char *name_str = (const char *)&js->mem[name_off]; + const char *msg_str = (const char *)&js->mem[msg_off]; + + if (name_len == 0) return msg; + if (msg_len == 0) return name; + + size_t total = name_len + 2 + msg_len; + char *buf = malloc(total + 1); + if (!buf) return js_mkerr(js, "out of memory"); + + memcpy(buf, name_str, name_len); + buf[name_len] = ':'; buf[name_len + 1] = ' '; + memcpy(buf + name_len + 2, msg_str, msg_len); + buf[total] = '\0'; + + jsval_t result = js_mkstr(js, buf, total); + free(buf); return result; +} + static jsval_t builtin_AggregateError(struct js *js, jsval_t *args, int nargs) { bool is_new = (vtype(js->new_target) != T_UNDEF); jsval_t this_val = js->this_val; @@ -13146,15 +13348,25 @@ static jsval_t builtin_Date_toISOString(struct js *js, jsval_t *args, int nargs) } static jsval_t builtin_Date_toString(struct js *js, jsval_t *args, int nargs) { - (void) args; - (void) nargs; double ms = date_get_time(js, js->this_val); if (isnan(ms)) return js_mkstr(js, "Invalid Date", 12); + time_t t = (time_t)(ms / 1000.0); - char *s = ctime(&t); - size_t len = strlen(s); - if (len > 0 && s[len - 1] == '\n') len--; - return js_mkstr(js, s, len); + struct tm *tm_local = localtime(&t); + if (!tm_local) return js_mkstr(js, "Invalid Date", 12); + + struct tm local_copy = *tm_local; + struct tm *gm = gmtime(&t); + long offset_sec = (long)difftime(mktime(&local_copy), mktime(gm)); + int offset_hours = (int)(offset_sec / 3600); + int offset_mins = (int)(labs(offset_sec) % 3600) / 60; + + char tz[32], buf[80]; + strftime(tz, sizeof(tz), "%Z", tm_local); + strftime(buf, sizeof(buf), "%a %b %d %Y %H:%M:%S", tm_local); + size_t n = strlen(buf); + n += snprintf(buf + n, sizeof(buf) - n, " GMT%+03d%02d (%s)", offset_hours, offset_mins, tz); + return js_mkstr(js, buf, n); } static jsval_t builtin_Date_valueOf(struct js *js, jsval_t *args, int nargs) { @@ -13987,7 +14199,7 @@ static jsval_t builtin_object_getPrototypeOf(struct js *js, jsval_t *args, int n uint8_t t = vtype(obj); if (t == T_STR || t == T_NUM || t == T_BOOL || t == T_BIGINT) return get_prototype_for_type(js, t); - if (t == T_OBJ || t == T_ARR || t == T_FUNC) return get_proto(js, obj); + if (is_object_type(obj)) return get_proto(js, obj); return js_mknull(); } @@ -14649,6 +14861,39 @@ static jsval_t builtin_object_getOwnPropertyNames(struct js *js, jsval_t *args, return mkval(T_ARR, vdata(arr)); } +// refactor with desc when SLOT_SYMBOL +static jsval_t builtin_object_getOwnPropertySymbols(struct js *js, jsval_t *args, int nargs) { + if (nargs == 0) return mkarr(js); + jsval_t obj = args[0]; + + uint8_t t = vtype(obj); + if (t != T_OBJ && t != T_ARR && t != T_FUNC) return mkarr(js); + if (t == T_FUNC) obj = mkval(T_OBJ, vdata(obj)); + + jsval_t arr = mkarr(js); jsoff_t idx = 0; + jsoff_t next = loadoff(js, (jsoff_t)vdata(obj)) & ~(3U | FLAGMASK); + + while (next < js->brk && next != 0) { + jsoff_t header = loadoff(js, next); + if (!is_slot_prop(header)) { + jsoff_t koff = loadoff(js, next + (jsoff_t)sizeof(next)); + jsoff_t klen = offtolen(loadoff(js, koff)); + const char *key = (char *)&js->mem[koff + sizeof(koff)]; + + if (klen >= 9 && memcmp(key, "__sym_", 6) == 0 && memcmp(key + klen - 2, "__", 2) == 0) { + uint64_t sym_id = 0; + for (const char *p = key + 6; *p >= '0' && *p <= '9'; p++) sym_id = sym_id * 10 + (uint64_t)(*p - '0'); + char idxstr[16]; size_t idxlen = uint_to_str(idxstr, sizeof(idxstr), (unsigned)idx++); + js_mkprop_fast(js, arr, idxstr, idxlen, mkval(T_SYMBOL, (sym_id & PROPREF_PAYLOAD) << PROPREF_KEY_SHIFT)); + } + } + next = next_prop(header); + } + + js_mkprop_fast(js, arr, "length", 6, tov((double)idx)); + return mkval(T_ARR, vdata(arr)); +} + static jsval_t builtin_object_isExtensible(struct js *js, jsval_t *args, int nargs) { if (nargs == 0) return js_true; @@ -14771,8 +15016,8 @@ static jsval_t builtin_object_toString(struct js *js, jsval_t *args, int nargs) obj = resolveprop(js, obj); uint8_t t = vtype(obj); - if (t == T_OBJ || t == T_ARR || t == T_FUNC) { - jsval_t check_obj = (t == T_FUNC) ? mkval(T_OBJ, vdata(obj)) : obj; + if (is_object_type(obj)) { + jsval_t check_obj = (t != T_OBJ) ? mkval(T_OBJ, vdata(obj)) : obj; const char *tostr_tag_key = get_toStringTag_sym_key(); jsoff_t tag_off = lkp(js, check_obj, tostr_tag_key, strlen(tostr_tag_key)); if (tag_off == 0) tag_off = lkp_proto(js, check_obj, tostr_tag_key, strlen(tostr_tag_key)); @@ -14792,17 +15037,18 @@ static jsval_t builtin_object_toString(struct js *js, jsval_t *args, int nargs) const char *type_name = NULL; switch (t) { - case T_UNDEF: type_name = "Undefined"; break; - case T_NULL: type_name = "Null"; break; - case T_BOOL: type_name = "Boolean"; break; - case T_NUM: type_name = "Number"; break; - case T_STR: type_name = "String"; break; - case T_ARR: type_name = "Array"; break; - case T_FUNC: type_name = "Function"; break; - case T_ERR: type_name = "Error"; break; - case T_BIGINT: type_name = "BigInt"; break; - case T_OBJ: type_name = "Object"; break; - default: type_name = "Unknown"; break; + case T_UNDEF: type_name = "Undefined"; break; + case T_NULL: type_name = "Null"; break; + case T_BOOL: type_name = "Boolean"; break; + case T_NUM: type_name = "Number"; break; + case T_STR: type_name = "String"; break; + case T_ARR: type_name = "Array"; break; + case T_FUNC: type_name = "Function"; break; + case T_ERR: type_name = "Error"; break; + case T_BIGINT: type_name = "BigInt"; break; + case T_PROMISE: type_name = "Promise"; break; + case T_OBJ: type_name = "Object"; break; + default: type_name = "Unknown"; break; } char buf[256]; @@ -15057,45 +15303,58 @@ static jsval_t builtin_array_join(struct js *js, jsval_t *args, int nargs) { if (elem_off != 0) { jsval_t elem = resolveprop(js, mkval(T_PROP, elem_off)); - if (vtype(elem) == T_STR) { - jsoff_t elem_len, elem_off_str = vstr(js, elem, &elem_len); + uint8_t et = vtype(elem); + if (et == T_NULL || et == T_UNDEF) continue; + + const char *elem_str = NULL; + size_t elem_len = 0; + char numstr[64]; + jsval_t str_val = js_mkundef(); + + if (et == T_STR) { + jsoff_t soff, slen; + soff = vstr(js, elem, &slen); + elem_str = (const char *)&js->mem[soff]; + elem_len = slen; + } else if (et == T_NUM) { + snprintf(numstr, sizeof(numstr), "%g", tod(elem)); + elem_str = numstr; + elem_len = strlen(numstr); + } else if (et == T_BOOL) { + elem_str = vdata(elem) ? "true" : "false"; + elem_len = strlen(elem_str); + } else if (et == T_ARR || et == T_OBJ || et == T_FUNC || et == T_BIGINT) { + str_val = to_string_val(js, elem); + + if (is_err(str_val)) { + free(result); + return str_val; + } + + if (vtype(str_val) == T_STR) { + jsoff_t soff, slen; + soff = vstr(js, str_val, &slen); + elem_str = (const char *)&js->mem[soff]; + elem_len = slen; + } + } + + + if (elem_str && elem_len > 0) { if (result_len + elem_len >= capacity) { capacity = (result_len + elem_len + 1) * 2; char *new_result = (char *)ant_realloc(result, capacity); - if (!new_result) return js_mkerr(js, "oom"); + if (!new_result) { free(result); return js_mkerr(js, "oom"); } result = new_result; } - memcpy(result + result_len, &js->mem[elem_off_str], elem_len); + memcpy(result + result_len, elem_str, elem_len); result_len += elem_len; - } else if (vtype(elem) == T_NUM) { - char numstr[32]; - snprintf(numstr, sizeof(numstr), "%g", tod(elem)); - size_t num_len = strlen(numstr); - if (result_len + num_len >= capacity) { - capacity = (result_len + num_len + 1) * 2; - char *new_result = (char *)ant_realloc(result, capacity); - if (!new_result) return js_mkerr(js, "oom"); - result = new_result; - } - memcpy(result + result_len, numstr, num_len); - result_len += num_len; - } else if (vtype(elem) == T_BOOL) { - const char *boolstr = vdata(elem) ? "true" : "false"; - size_t bool_len = strlen(boolstr); - if (result_len + bool_len >= capacity) { - capacity = (result_len + bool_len + 1) * 2; - char *new_result = (char *)ant_realloc(result, capacity); - if (!new_result) return js_mkerr(js, "oom"); - result = new_result; - } - memcpy(result + result_len, boolstr, bool_len); - result_len += bool_len; } } } + jsval_t ret = js_mkstr(js, result, result_len); - free(result); - return ret; + free(result); return ret; } static jsval_t builtin_array_includes(struct js *js, jsval_t *args, int nargs) { @@ -16540,15 +16799,15 @@ static jsval_t builtin_array_entries(struct js *js, jsval_t *args, int nargs) { } static jsval_t builtin_array_toString(struct js *js, jsval_t *args, int nargs) { - (void) args; - (void) nargs; jsval_t arr = js->this_val; - jsval_t sep_args[1] = { js_mkstr(js, ",", 1) }; - jsval_t old_this = js->this_val; - js->this_val = arr; - jsval_t result = builtin_array_join(js, sep_args, 1); - js->this_val = old_this; - return result; + + jsval_t join_result; + if (js_try_call_method(js, arr, "join", 4, NULL, 0, &join_result)) { + if (is_err(join_result)) return join_result; + return join_result; + } + + return js_mkstr(js, "[object Object]", 15); } static jsval_t builtin_array_toLocaleString(struct js *js, jsval_t *args, int nargs) { @@ -20770,6 +21029,7 @@ ant_t *js_create(void *buf, size_t len) { set_proto(js, error_proto, object_proto); setprop(js, error_proto, ANT_STRING("name"), ANT_STRING("Error")); setprop(js, error_proto, ANT_STRING("message"), js_mkstr(js, "", 0)); + setprop(js, error_proto, js_mkstr(js, "toString", 8), js_mkfun(builtin_Error_toString)); jsval_t err_ctor_obj = mkobj(js, 0); set_proto(js, err_ctor_obj, function_proto); @@ -20875,6 +21135,7 @@ ant_t *js_create(void *buf, size_t len) { setprop(js, promise_proto, js_mkstr(js, "then", 4), js_mkfun(builtin_promise_then)); setprop(js, promise_proto, js_mkstr(js, "catch", 5), js_mkfun(builtin_promise_catch)); setprop(js, promise_proto, js_mkstr(js, "finally", 7), js_mkfun(builtin_promise_finally)); + // Symbol.toStringTag is set in init_symbol_module after symbols are initialized jsval_t obj_func_obj = mkobj(js, 0); set_proto(js, obj_func_obj, function_proto); @@ -20896,6 +21157,7 @@ ant_t *js_create(void *buf, size_t len) { setprop(js, obj_func_obj, js_mkstr(js, "fromEntries", 11), js_mkfun(builtin_object_fromEntries)); setprop(js, obj_func_obj, js_mkstr(js, "getOwnPropertyDescriptor", 24), js_mkfun(builtin_object_getOwnPropertyDescriptor)); setprop(js, obj_func_obj, js_mkstr(js, "getOwnPropertyNames", 19), js_mkfun(builtin_object_getOwnPropertyNames)); + setprop(js, obj_func_obj, js_mkstr(js, "getOwnPropertySymbols", 21), js_mkfun(builtin_object_getOwnPropertySymbols)); setprop(js, obj_func_obj, js_mkstr(js, "isExtensible", 12), js_mkfun(builtin_object_isExtensible)); setprop(js, obj_func_obj, js_mkstr(js, "preventExtensions", 17), js_mkfun(builtin_object_preventExtensions)); setprop(js, obj_func_obj, ANT_STRING("name"), ANT_STRING("Object")); @@ -21115,6 +21377,7 @@ ant_t *js_create(void *buf, size_t len) { set_proto(js, glob, object_proto); + js->object = object_proto; js->owns_mem = false; js->max_size = 0; @@ -21340,6 +21603,13 @@ jsval_t js_getprop_proto(struct js *js, jsval_t obj, const char *key) { return off == 0 ? js_mkundef() : resolveprop(js, mkval(T_PROP, off)); } +jsval_t js_getprop_fallback(struct js *js, jsval_t obj, const char *name) { + jsval_t val = js_get(js, obj, name); + if (vtype(val) == T_UNDEF) { + val = js_getprop_proto(js, obj, name); + } return val; +} + typedef struct { bool (*callback)(struct js *js, jsval_t value, void *udata); void *udata; diff --git a/src/modules/buffer.c b/src/modules/buffer.c index e41979a..8153ce2 100644 --- a/src/modules/buffer.c +++ b/src/modules/buffer.c @@ -1659,11 +1659,14 @@ static jsval_t js_sharedarraybuffer_constructor(struct js *js, jsval_t *args, in } void init_buffer_module() { - struct js *js = rt->js; - jsval_t glob = js_glob(js); + ant_t *js = rt->js; + jsval_t glob = js->global; + jsval_t object_proto = js->object; + jsval_t arraybuffer_ctor_obj = js_mkobj(js); jsval_t arraybuffer_proto = js_mkobj(js); + js_set_proto(js, arraybuffer_proto, object_proto); js_set(js, arraybuffer_proto, "slice", js_mkfun(js_arraybuffer_slice)); js_set(js, arraybuffer_proto, "transfer", js_mkfun(js_arraybuffer_transfer)); @@ -1678,6 +1681,8 @@ void init_buffer_module() { js_set(js, glob, "ArrayBuffer", js_obj_to_func(arraybuffer_ctor_obj)); jsval_t typedarray_proto = js_mkobj(js); + js_set_proto(js, typedarray_proto, object_proto); + js_set(js, typedarray_proto, "slice", js_mkfun(js_typedarray_slice)); js_set(js, typedarray_proto, "subarray", js_mkfun(js_typedarray_subarray)); js_set(js, typedarray_proto, "fill", js_mkfun(js_typedarray_fill)); @@ -1710,6 +1715,7 @@ void init_buffer_module() { jsval_t dataview_ctor_obj = js_mkobj(js); jsval_t dataview_proto = js_mkobj(js); + js_set_proto(js, dataview_proto, object_proto); js_set(js, dataview_proto, "getUint8", js_mkfun(js_dataview_getUint8)); js_set(js, dataview_proto, "setUint8", js_mkfun(js_dataview_setUint8)); @@ -1731,6 +1737,7 @@ void init_buffer_module() { jsval_t sharedarraybuffer_ctor_obj = js_mkobj(js); jsval_t sharedarraybuffer_proto = js_mkobj(js); + js_set_proto(js, sharedarraybuffer_proto, object_proto); js_set(js, sharedarraybuffer_proto, "slice", js_mkfun(js_arraybuffer_slice)); js_set(js, sharedarraybuffer_proto, get_toStringTag_sym_key(), js_mkstr(js, "SharedArrayBuffer", 17)); @@ -1743,6 +1750,7 @@ void init_buffer_module() { jsval_t buffer_ctor_obj = js_mkobj(js); jsval_t buffer_proto = js_mkobj(js); + js_set_proto(js, buffer_proto, object_proto); js_set(js, buffer_proto, "toString", js_mkfun(js_buffer_toString)); js_set(js, buffer_proto, "toBase64", js_mkfun(js_buffer_toBase64)); diff --git a/src/modules/collections.c b/src/modules/collections.c index 34526e8..ca770e3 100644 --- a/src/modules/collections.c +++ b/src/modules/collections.c @@ -886,12 +886,15 @@ static jsval_t builtin_WeakSet(ant_t *js, jsval_t *args, int nargs) { void init_collections_module(void) { ant_t *js = rt->js; - jsval_t glob = js_glob(js); + + jsval_t glob = js->global; + jsval_t object_proto = js->object; const char *iter_key = get_iterator_sym_key(); const char *toStringTag_key = get_toStringTag_sym_key(); jsval_t map_proto = js_mkobj(js); + js_set_proto(js, map_proto, object_proto); js_set(js, map_proto, "set", js_mkfun(map_set)); js_set(js, map_proto, "get", js_mkfun(map_get)); js_set(js, map_proto, "has", js_mkfun(map_has)); @@ -913,6 +916,7 @@ void init_collections_module(void) { js_set(js, glob, "Map", js_obj_to_func(map_ctor)); jsval_t set_proto = js_mkobj(js); + js_set_proto(js, set_proto, object_proto); js_set(js, set_proto, "add", js_mkfun(set_add)); js_set(js, set_proto, "has", js_mkfun(set_has)); js_set(js, set_proto, "delete", js_mkfun(set_delete)); @@ -933,6 +937,7 @@ void init_collections_module(void) { js_set(js, glob, "Set", js_obj_to_func(set_ctor)); jsval_t weakmap_proto = js_mkobj(js); + js_set_proto(js, weakmap_proto, object_proto); js_set(js, weakmap_proto, "set", js_mkfun(weakmap_set)); js_set(js, weakmap_proto, "get", js_mkfun(weakmap_get)); js_set(js, weakmap_proto, "has", js_mkfun(weakmap_has)); @@ -947,6 +952,7 @@ void init_collections_module(void) { js_set(js, glob, "WeakMap", js_obj_to_func(weakmap_ctor)); jsval_t weakset_proto = js_mkobj(js); + js_set_proto(js, weakset_proto, object_proto); js_set(js, weakset_proto, "add", js_mkfun(weakset_add)); js_set(js, weakset_proto, "has", js_mkfun(weakset_has)); js_set(js, weakset_proto, "delete", js_mkfun(weakset_delete)); @@ -960,6 +966,7 @@ void init_collections_module(void) { js_set(js, glob, "WeakSet", js_obj_to_func(weakset_ctor)); jsval_t weakref_proto = js_mkobj(js); + js_set_proto(js, weakref_proto, object_proto); js_set(js, weakref_proto, "deref", js_mkfun(weakref_deref)); js_set(js, weakref_proto, toStringTag_key, js_mkstr(js, "WeakRef", 7)); @@ -971,6 +978,7 @@ void init_collections_module(void) { js_set(js, glob, "WeakRef", js_obj_to_func(weakref_ctor)); jsval_t finreg_proto = js_mkobj(js); + js_set_proto(js, finreg_proto, object_proto); js_set(js, finreg_proto, "register", js_mkfun(finreg_register)); js_set(js, finreg_proto, "unregister", js_mkfun(finreg_unregister)); js_set(js, finreg_proto, toStringTag_key, js_mkstr(js, "FinalizationRegistry", 20)); diff --git a/src/modules/symbol.c b/src/modules/symbol.c index f13ecdd..45335f5 100644 --- a/src/modules/symbol.c +++ b/src/modules/symbol.c @@ -8,25 +8,55 @@ #include "internal.h" #include "modules/symbol.h" -static jsval_t g_iterator_sym = 0; -static jsval_t g_asyncIterator_sym = 0; -static jsval_t g_toStringTag_sym = 0; -static jsval_t g_hasInstance_sym = 0; -static jsval_t g_observable_sym = 0; - -static char g_iter_sym_key[32] = {0}; -static char g_asyncIter_sym_key[32] = {0}; -static char g_toStringTag_sym_key[32] = {0}; -static char g_observable_sym_key[32] = {0}; - -jsval_t get_iterator_symbol(void) { return g_iterator_sym; } -jsval_t get_asyncIterator_symbol(void) { return g_asyncIterator_sym; } -jsval_t get_observable_symbol(void) { return g_observable_sym; } - -const char *get_iterator_sym_key(void) { return g_iter_sym_key; } -const char *get_asyncIterator_sym_key(void) { return g_asyncIter_sym_key; } -const char *get_toStringTag_sym_key(void) { return g_toStringTag_sym_key; } -const char *get_observable_sym_key(void) { return g_observable_sym_key; } +typedef struct { + jsval_t sym; + char key[32]; +} wellknown_sym_t; + +static wellknown_sym_t g_iterator = {0}; +static wellknown_sym_t g_asyncIterator = {0}; +static wellknown_sym_t g_toStringTag = {0}; +static wellknown_sym_t g_hasInstance = {0}; +static wellknown_sym_t g_observable = {0}; +static wellknown_sym_t g_toPrimitive = {0}; + +jsval_t get_iterator_symbol(void) { return g_iterator.sym; } +jsval_t get_asyncIterator_symbol(void) { return g_asyncIterator.sym; } +jsval_t get_observable_symbol(void) { return g_observable.sym; } + +const char *get_iterator_sym_key(void) { return g_iterator.key; } +const char *get_asyncIterator_sym_key(void) { return g_asyncIterator.key; } +const char *get_toStringTag_sym_key(void) { return g_toStringTag.key; } +const char *get_observable_sym_key(void) { return g_observable.key; } +const char *get_toPrimitive_sym_key(void) { return g_toPrimitive.key; } +const char *get_hasInstance_sym_key(void) { return g_hasInstance.key; } + +static const struct { jsval_t *sym; const char *name; } sym_table[] = { + { &g_iterator.sym, "Symbol.iterator" }, + { &g_asyncIterator.sym, "Symbol.asyncIterator" }, + { &g_toStringTag.sym, "Symbol.toStringTag" }, + { &g_hasInstance.sym, "Symbol.hasInstance" }, + { &g_observable.sym, "Symbol.observable" }, + { &g_toPrimitive.sym, "Symbol.toPrimitive" }, +}; + +const char *get_symbol_description_from_key(const char *sym_key, size_t key_len) { + if (key_len < 9 || memcmp(sym_key, "__sym_", 6) != 0) return NULL; + + uint64_t id = 0; + for (const char *p = sym_key + 6; *p >= '0' && *p <= '9'; p++) id = id * 10 + (*p - '0'); + + for (size_t i = 0; i < sizeof(sym_table) / sizeof(sym_table[0]); i++) { + if (js_sym_id(*sym_table[i].sym) == id) return sym_table[i].name; + } + + return "Symbol()"; +} + +static inline void init_symbol(struct js *js, wellknown_sym_t *sym_var, const char *name) { + sym_var->sym = js_mksym(js, name); + snprintf(sym_var->key, sizeof(sym_var->key), "__sym_%llu__", (unsigned long long)js_sym_id(sym_var->sym)); +} static jsval_t builtin_Symbol(struct js *js, jsval_t *args, int nargs) { const char *desc = NULL; @@ -58,8 +88,17 @@ static jsval_t builtin_Symbol_keyFor(struct js *js, jsval_t *args, int nargs) { return js_mkstr(js, key, strlen(key)); } +static jsval_t builtin_Symbol_toString(struct js *js, jsval_t *args, int nargs) { + jsval_t this_val = js_getthis(js); + + if (vtype(this_val) != T_SYMBOL) { + return js_mkerr(js, "Symbol.prototype.toString requires a symbol"); + } + + return js_symbol_to_string(js, this_val); +} + static jsval_t iterator_next(struct js *js, jsval_t *args, int nargs) { - (void)args; (void)nargs; jsval_t this_val = js_getthis(js); jsval_t arr = js_get(js, this_val, "__arr"); @@ -87,7 +126,6 @@ static jsval_t iterator_next(struct js *js, jsval_t *args, int nargs) { } static jsval_t array_iterator(struct js *js, jsval_t *args, int nargs) { - (void)args; (void)nargs; jsval_t arr = js_getthis(js); jsval_t len_val = js_get(js, arr, "length"); @@ -103,7 +141,6 @@ static jsval_t array_iterator(struct js *js, jsval_t *args, int nargs) { } static jsval_t string_iterator_next(struct js *js, jsval_t *args, int nargs) { - (void)args; (void)nargs; jsval_t this_val = js_getthis(js); jsval_t str = js_get(js, this_val, "__str"); @@ -145,54 +182,76 @@ static jsval_t string_iterator(struct js *js, jsval_t *args, int nargs) { return iter; } -const char *get_symbol_description_from_key(const char *sym_key, size_t key_len) { - if ( - key_len < 9 || sym_key[0] != '_' || sym_key[1] != '_' || - sym_key[2] != 's' || sym_key[3] != 'y' || sym_key[4] != 'm' || sym_key[5] != '_' - ) return NULL; +static jsval_t date_toPrimitive(struct js *js, jsval_t *args, int nargs) { + jsval_t this_val = js_getthis(js); - if (g_iter_sym_key[0] && strncmp(sym_key, g_iter_sym_key, key_len) == 0 && g_iter_sym_key[key_len] == '\0') return "Symbol.iterator"; - if (g_toStringTag_sym_key[0] && strncmp(sym_key, g_toStringTag_sym_key, key_len) == 0 && g_toStringTag_sym_key[key_len] == '\0') return "Symbol.toStringTag"; + const char *hint = "default"; + if (nargs > 0 && vtype(args[0]) == T_STR) { + hint = js_getstr(js, args[0], NULL); + } bool prefer_string = (hint == NULL || strcmp(hint, "number") != 0); - return "Symbol()"; + const char *methods[2] = { + prefer_string ? "toString" : "valueOf", + prefer_string ? "valueOf" : "toString" + }; + + for (int i = 0; i < 2; i++) { + jsval_t method = js_getprop_fallback(js, this_val, methods[i]); + if (vtype(method) == T_FUNC || vtype(method) == T_CFUNC) { + jsval_t result = js_call_with_this(js, method, this_val, NULL, 0); + if (is_err(result) || !is_object_type(result)) return result; + } + } + + return js_mkerr(js, "Cannot convert object to primitive value"); } + void init_symbol_module(void) { struct js *js = rt->js; - g_iterator_sym = js_mksym(js, "Symbol.iterator"); - g_asyncIterator_sym = js_mksym(js, "Symbol.asyncIterator"); - g_toStringTag_sym = js_mksym(js, "Symbol.toStringTag"); - g_hasInstance_sym = js_mksym(js, "Symbol.hasInstance"); - g_observable_sym = js_mksym(js, "Symbol.observable"); - - snprintf(g_iter_sym_key, sizeof(g_iter_sym_key), "__sym_%llu__", (unsigned long long)js_sym_id(g_iterator_sym)); - snprintf(g_asyncIter_sym_key, sizeof(g_asyncIter_sym_key), "__sym_%llu__", (unsigned long long)js_sym_id(g_asyncIterator_sym)); - snprintf(g_toStringTag_sym_key, sizeof(g_toStringTag_sym_key), "__sym_%llu__", (unsigned long long)js_sym_id(g_toStringTag_sym)); - snprintf(g_observable_sym_key, sizeof(g_observable_sym_key), "__sym_%llu__", (unsigned long long)js_sym_id(g_observable_sym)); + init_symbol(js, &g_iterator, "Symbol.iterator"); + init_symbol(js, &g_asyncIterator, "Symbol.asyncIterator"); + init_symbol(js, &g_toStringTag, "Symbol.toStringTag"); + init_symbol(js, &g_observable, "Symbol.observable"); + init_symbol(js, &g_toPrimitive, "Symbol.toPrimitive"); + init_symbol(js, &g_hasInstance, "Symbol.hasInstance"); + + jsval_t symbol_proto = js_mkobj(js); + js_set(js, symbol_proto, "toString", js_mkfun(builtin_Symbol_toString)); jsval_t symbol_ctor = js_mkobj(js); js_set_slot(js, symbol_ctor, SLOT_CFUNC, js_mkfun(builtin_Symbol)); js_setprop(js, symbol_ctor, js_mkstr(js, "for", 3), js_mkfun(builtin_Symbol_for)); js_set(js, symbol_ctor, "keyFor", js_mkfun(builtin_Symbol_keyFor)); + js_set(js, symbol_ctor, "prototype", symbol_proto); - js_set(js, symbol_ctor, "iterator", g_iterator_sym); - js_set(js, symbol_ctor, "asyncIterator", g_asyncIterator_sym); - js_set(js, symbol_ctor, "toStringTag", g_toStringTag_sym); - js_set(js, symbol_ctor, "hasInstance", g_hasInstance_sym); - js_set(js, symbol_ctor, "observable", g_observable_sym); + js_set(js, symbol_ctor, "iterator", g_iterator.sym); + js_set(js, symbol_ctor, "asyncIterator", g_asyncIterator.sym); + js_set(js, symbol_ctor, "toStringTag", g_toStringTag.sym); + js_set(js, symbol_ctor, "hasInstance", g_hasInstance.sym); + js_set(js, symbol_ctor, "observable", g_observable.sym); + js_set(js, symbol_ctor, "toPrimitive", g_toPrimitive.sym); jsval_t func_symbol = js_obj_to_func(symbol_ctor); js_set(js, js_glob(js), "Symbol", func_symbol); - // set internal types before module ready + // set internal types before ant module snapshot js_set(js, rt->ant_obj, get_toStringTag_sym_key(), js_mkstr(js, "Ant", 3)); jsval_t array_ctor = js_get(js, js_glob(js), "Array"); jsval_t array_proto = js_get(js, array_ctor, "prototype"); - js_set(js, array_proto, g_iter_sym_key, js_mkfun(array_iterator)); + js_set(js, array_proto, g_iterator.key, js_mkfun(array_iterator)); jsval_t string_ctor = js_get(js, js_glob(js), "String"); jsval_t string_proto = js_get(js, string_ctor, "prototype"); - js_set(js, string_proto, g_iter_sym_key, js_mkfun(string_iterator)); + js_set(js, string_proto, g_iterator.key, js_mkfun(string_iterator)); + + jsval_t date_ctor = js_get(js, js_glob(js), "Date"); + jsval_t date_proto = js_get(js, date_ctor, "prototype"); + js_set(js, date_proto, g_toPrimitive.key, js_mkfun(date_toPrimitive)); + + jsval_t promise_ctor = js_get(js, js_glob(js), "Promise"); + jsval_t promise_proto = js_get(js, promise_ctor, "prototype"); + js_set(js, promise_proto, g_toStringTag.key, js_mkstr(js, "Promise", 7)); } diff --git a/src/runtime.c b/src/runtime.c index 3eb9f09..82075fe 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -92,13 +92,11 @@ void code_arena_reset(void) { code_arena_current = NULL; } -struct ant_runtime *ant_runtime_init(struct js *js, int argc, char **argv, struct arg_file *ls_p) { +struct ant_runtime *ant_runtime_init(ant_t *js, int argc, char **argv, struct arg_file *ls_p) { runtime = (struct ant_runtime){ .js = js, .ant_obj = js_newobj(js), - .flags = 0, - .argc = argc, - .argv = argv, + .flags = 0, .argc = argc, .argv = argv, .ls_fp = (ls_p && ls_p->count > 0) ? ls_p->filename[0] : NULL, }; diff --git a/tests/sym_date.js b/tests/sym_date.js new file mode 100644 index 0000000..0c2e0bb --- /dev/null +++ b/tests/sym_date.js @@ -0,0 +1,42 @@ +let d = new Date(); + +// Test 1: Default behavior +console.log(d[Symbol.toPrimitive]('default')); // Should call toString + +// Test 2: String hint +console.log(d[Symbol.toPrimitive]('string')); // Should call toString + +// Test 3: Number hint +console.log(d[Symbol.toPrimitive]('number')); // Should call valueOf (timestamp) + +// Test 4: Coercion tests +console.log(d + ''); // Uses default hint -> string +console.log(+d); // Uses number hint -> number +console.log(d - 0); // Uses number hint -> number + +// Test 5: Verify order - SAVE ORIGINAL METHODS FIRST +const originalValueOf = Date.prototype.valueOf; +const originalToString = Date.prototype.toString; + +Date.prototype.valueOf = function () { + console.log('valueOf called'); + return originalValueOf.call(this); // Call the SAVED original +}; +Date.prototype.toString = function () { + console.log('toString called'); + return originalToString.call(this); // Call the SAVED original +}; + +let d2 = new Date(); +console.log('\nTesting string hint:'); +d2[Symbol.toPrimitive]('string'); // Should log: toString only (returns primitive) + +console.log('\nTesting number hint:'); +d2[Symbol.toPrimitive]('number'); // Should log: valueOf only (returns primitive) + +console.log('\nTesting default hint:'); +d2[Symbol.toPrimitive]('default'); // Should log: toString only (returns primitive) + +// Restore original methods +Date.prototype.valueOf = originalValueOf; +Date.prototype.toString = originalToString; diff --git a/tests/sym_id.js b/tests/sym_id.js new file mode 100644 index 0000000..7080628 --- /dev/null +++ b/tests/sym_id.js @@ -0,0 +1,35 @@ +console.log('=== SYMBOL KEY STRINGIFICATION TEST ===\n'); + +const obj = {}; + +obj[Symbol.iterator] = 'iter-value'; +obj[Symbol.asyncIterator] = 'async-iter-value'; +obj[Symbol.toStringTag] = 'tag-value'; +obj[Symbol.hasInstance] = 'has-instance-value'; +obj[Symbol.observable] = 'observable-value'; +obj[Symbol.toPrimitive] = 'to-primitive-value'; +obj[Symbol('custom')] = 'custom-value'; +obj[Symbol()] = 'anonymous-value'; +obj.normalKey = 'normal-value'; + +console.log('Object with symbol keys:'); +console.log(obj); + +console.log('\nIndividual access:'); +console.log(' [Symbol.iterator]: ' + obj[Symbol.iterator]); +console.log(' [Symbol.toStringTag]: ' + obj[Symbol.toStringTag]); + +console.log('\nObject.keys (should NOT include symbols):'); +console.log(Object.keys(obj)); + +console.log('\nObject.getOwnPropertySymbols (if supported):'); +try { + console.log(Object.getOwnPropertySymbols(obj)); +} catch (e) { + console.log(' Not supported: ' + e.message); +} + +console.log('\nJSON.stringify (symbols should be skipped):'); +console.log(JSON.stringify(obj)); + +console.log('\n=== DONE ==='); diff --git a/tests/test-coercion-matrix.js b/tests/test-coercion-matrix.js new file mode 100644 index 0000000..c6f6f0f --- /dev/null +++ b/tests/test-coercion-matrix.js @@ -0,0 +1,139 @@ +// ============================================ +// TEST: String coercion comparison matrix +// ============================================ +// Compares: concat, template, String(), toString() +// ============================================ + +console.log("=== STRING COERCION MATRIX ===\n"); + +function safe(fn) { + try { + const result = fn(); + if (result === undefined) return "undefined"; + if (result === null) return "null"; + return result; + } catch (e) { + return "ERROR:" + e.name; + } +} + +function test(name, value) { + console.log(name + ":"); + console.log(" concat: " + safe(function() { return "" + value; })); + console.log(" template: " + safe(function() { return `${value}`; })); + console.log(" String(): " + safe(function() { return String(value); })); + if (value !== null && value !== undefined) { + console.log(" toString: " + safe(function() { return value.toString(); })); + } + console.log(""); +} + +// Primitives +console.log("--- PRIMITIVES ---\n"); +test("number 42", 42); +test("number 0", 0); +test("number -1", -1); +test("number NaN", NaN); +test("number Infinity", Infinity); +test("string 'hello'", "hello"); +test("string ''", ""); +test("boolean true", true); +test("boolean false", false); +test("null", null); +test("undefined", undefined); +test("BigInt 123n", 123n); + +// Symbol +console.log("--- SYMBOL ---\n"); +test("Symbol('test')", Symbol("test")); + +// Arrays +console.log("--- ARRAYS ---\n"); +test("[]", []); +test("[1, 2, 3]", [1, 2, 3]); +test("[[1,2], [3,4]]", [[1, 2], [3, 4]]); +test("[null, undefined]", [null, undefined]); +test("['a', 'b']", ["a", "b"]); + +// Functions +console.log("--- FUNCTIONS ---\n"); +test("() => 1", () => 1); +test("function() {}", function() {}); +function namedFn() { return 1; } +test("named function", namedFn); +test("async () => 1", async () => 1); + +// Objects +console.log("--- OBJECTS ---\n"); +test("{}", {}); +test("{ a: 1 }", { a: 1 }); +test("{ a: 1, b: 2 }", { a: 1, b: 2 }); + +// Built-in objects +console.log("--- BUILT-IN OBJECTS ---\n"); +test("new Date(0)", new Date(0)); +test("new Date()", new Date()); +test("/test/gi", /test/gi); +test("new Error('oops')", new Error("oops")); +test("new TypeError('bad')", new TypeError("bad")); +test("new Map([['a',1]])", new Map([["a", 1]])); +test("new Set([1,2,3])", new Set([1, 2, 3])); +test("new WeakMap()", new WeakMap()); +test("new WeakSet()", new WeakSet()); +test("new ArrayBuffer(8)", new ArrayBuffer(8)); +test("new Uint8Array([1,2])", new Uint8Array([1, 2])); +test("Promise.resolve(1)", Promise.resolve(1)); + +// Custom toString +console.log("--- CUSTOM toString (CRASH TESTS) ---\n"); + +console.log("native toString (should work):"); +test("{ toString: Object.prototype.toString }", { toString: Object.prototype.toString }); + +console.log("user toString (may crash):"); +const userToString = { toString: function() { return "custom"; } }; +console.log(" direct call works: " + userToString.toString()); +test("{ toString: fn => 'custom' }", userToString); + +console.log("arrow toString (may crash):"); +const arrowToString = { toString: () => "arrow" }; +console.log(" direct call works: " + arrowToString.toString()); +test("{ toString: () => 'arrow' }", arrowToString); + +// Custom valueOf +console.log("--- CUSTOM valueOf ---\n"); +test("{ valueOf: fn => 42 }", { valueOf: function() { return 42; } }); +test("{ valueOf: fn => 'val' }", { valueOf: function() { return "val"; } }); + +// Symbol.toPrimitive +console.log("--- Symbol.toPrimitive ---\n"); +const withPrimitive = {}; +withPrimitive[Symbol.toPrimitive] = function(hint) { return "hint:" + hint; }; +test("{ [Symbol.toPrimitive]: fn }", withPrimitive); + +// Class instances +console.log("--- CLASS INSTANCES ---\n"); + +class WithToString { + toString() { return "class-toString"; } +} +console.log("class with toString method:"); +const inst1 = new WithToString(); +console.log(" direct call works: " + inst1.toString()); +test("new WithToString()", inst1); + +class WithValueOf { + valueOf() { return "class-valueOf"; } +} +test("new WithValueOf()", new WithValueOf()); + +class PlainClass {} +test("new PlainClass()", new PlainClass()); + +console.log("=== SUMMARY ===\n"); +console.log("Check for:"); +console.log("1. Missing output after test name = SILENT CRASH"); +console.log("2. '[Function: ...]' instead of function source = INSPECT BUG"); +console.log("3. '{ ... }' or '[ ... ]' with spaces = INSPECT BUG"); +console.log("4. Different results between concat and template = BUG"); +console.log(""); \ No newline at end of file diff --git a/tests/test-concat-crash.js b/tests/test-concat-crash.js new file mode 100644 index 0000000..bcb9845 --- /dev/null +++ b/tests/test-concat-crash.js @@ -0,0 +1,80 @@ +// ============================================ +// TEST: Concatenation crashes with user-defined toString +// ============================================ +// Bug: "" + obj crashes silently when obj has user-defined toString +// ============================================ + +console.log("=== CONCAT CRASH TESTS ===\n"); + +// --- BASELINE (should work) --- + +console.log("1. No custom toString:"); +const obj1 = {}; +console.log(" result: " + ("" + obj1)); + +console.log("\n2. toString = native function:"); +const obj2 = { toString: Object.prototype.toString }; +console.log(" result: " + ("" + obj2)); + +console.log("\n3. toString = Array.prototype.toString:"); +const obj3 = { toString: Array.prototype.toString }; +console.log(" result: " + ("" + obj3)); + +console.log("\n4. toString = console.log (native):"); +const obj4 = { toString: console.log }; +console.log(" result: " + ("" + obj4)); + +// --- CRASH TESTS (these crash silently) --- + +console.log("\n5. toString = user function:"); +const obj5 = { toString: function() { return "custom"; } }; +console.log(" expected: custom"); +console.log(" direct call: " + obj5.toString()); +const result5 = "" + obj5; +console.log(" concat: " + result5); +console.log(" (if missing, CRASH)"); + +console.log("\n6. toString = arrow function:"); +const obj6 = { toString: () => "arrow" }; +console.log(" expected: arrow"); +console.log(" direct call: " + obj6.toString()); +const result6 = "" + obj6; +console.log(" concat: " + result6); +console.log(" (if missing, CRASH)"); + +console.log("\n7. toString = user function returning number:"); +const obj7 = { toString: function() { return 42; } }; +console.log(" expected: 42"); +console.log(" direct call: " + obj7.toString()); +const result7 = "" + obj7; +console.log(" concat: " + result7); +console.log(" (if missing, CRASH)"); + +console.log("\n8. Inherited toString from prototype:"); +function MyClass() {} +MyClass.prototype.toString = function() { return "inherited"; }; +const obj8 = new MyClass(); +console.log(" expected: inherited"); +console.log(" direct call: " + obj8.toString()); +const result8 = "" + obj8; +console.log(" concat: " + result8); +console.log(" (if missing, CRASH)"); + +console.log("\n9. toString as getter returning function:"); +const obj9 = { get toString() { return function() { return "getter"; }; } }; +console.log(" expected: getter"); +const result9 = "" + obj9; +console.log(" concat: " + result9); + +console.log("\n10. toString = non-function value:"); +const obj10 = { toString: "not a function" }; +console.log(" expected: TypeError"); +try { + const result10 = "" + obj10; + console.log(" concat: " + result10); + console.log(" ERROR: should have thrown TypeError!"); +} catch (e) { + console.log(" threw: " + e.name + " (correct)"); +} + +console.log("\n=== DONE ==="); diff --git a/tests/test-string-constructor.js b/tests/test-string-constructor.js new file mode 100644 index 0000000..fbd3d56 --- /dev/null +++ b/tests/test-string-constructor.js @@ -0,0 +1,158 @@ +// ============================================ +// TEST: String() uses inspect instead of toString +// ============================================ +// Bug: String(obj) uses inspect-style output instead of calling toString() +// ============================================ + +console.log("=== String() CONSTRUCTOR TESTS ===\n"); + +// --- PRIMITIVES --- + +console.log("1. Primitives:"); +console.log(" String(42): expected '42', got '" + String(42) + "'"); +console.log(" String('hi'): expected 'hi', got '" + String("hi") + "'"); +console.log(" String(true): expected 'true', got '" + String(true) + "'"); +console.log(" String(false): expected 'false', got '" + String(false) + "'"); +console.log(" String(null): expected 'null', got '" + String(null) + "'"); +console.log(" String(undefined): expected 'undefined', got '" + String(undefined) + "'"); +console.log(" String(123n): expected '123', got '" + String(123n) + "'"); + +// --- ARRAYS --- + +console.log("\n2. Array [1, 2, 3]:"); +const arr1 = [1, 2, 3]; +console.log(" expected: '1,2,3'"); +console.log(" String(): '" + String(arr1) + "'"); +console.log(" toString: '" + arr1.toString() + "'"); + +console.log("\n3. Nested array [[1,2], [3,4]]:"); +const arr2 = [[1, 2], [3, 4]]; +console.log(" expected: '1,2,3,4'"); +console.log(" String(): '" + String(arr2) + "'"); +console.log(" toString: '" + arr2.toString() + "'"); + +console.log("\n4. Empty array []:"); +const arr3 = []; +console.log(" expected: ''"); +console.log(" String(): '" + String(arr3) + "'"); + +console.log("\n5. Array with objects:"); +const arr4 = [{ a: 1 }, { b: 2 }]; +console.log(" expected: '[object Object],[object Object]'"); +console.log(" String(): '" + String(arr4) + "'"); + +// --- FUNCTIONS --- + +console.log("\n6. Arrow function:"); +const fn1 = () => 1; +console.log(" expected: '() => 1'"); +console.log(" String(): '" + String(fn1) + "'"); +console.log(" toString: '" + fn1.toString() + "'"); + +console.log("\n7. Named function:"); +function namedFn() { return 1; } +console.log(" expected: 'function namedFn() { return 1; }'"); +console.log(" String(): '" + String(namedFn) + "'"); +console.log(" toString: '" + namedFn.toString() + "'"); + +console.log("\n8. Async function:"); +async function asyncFn() { return 1; } +console.log(" expected: 'async function asyncFn() { return 1; }'"); +console.log(" String(): '" + String(asyncFn) + "'"); +console.log(" toString: '" + asyncFn.toString() + "'"); + +// --- OBJECTS --- + +console.log("\n9. Plain object {}:"); +const obj1 = {}; +console.log(" expected: '[object Object]'"); +console.log(" String(): '" + String(obj1) + "'"); + +console.log("\n10. Object with properties:"); +const obj2 = { a: 1, b: 2 }; +console.log(" expected: '[object Object]'"); +console.log(" String(): '" + String(obj2) + "'"); + +console.log("\n11. Object with custom toString:"); +const obj3 = { toString: function() { return "custom-toString"; } }; +console.log(" expected: 'custom-toString'"); +console.log(" toString: '" + obj3.toString() + "'"); +console.log(" String(): '" + String(obj3) + "'"); + +console.log("\n12. Object with valueOf only:"); +const obj4 = { valueOf: function() { return "custom-valueOf"; } }; +console.log(" expected: 'custom-valueOf' or '[object Object]'"); +console.log(" String(): '" + String(obj4) + "'"); + +console.log("\n13. Object with both toString and valueOf:"); +const obj5 = { + toString: function() { return "from-toString"; }, + valueOf: function() { return "from-valueOf"; } +}; +console.log(" expected: 'from-toString' (toString has priority)"); +console.log(" String(): '" + String(obj5) + "'"); + +// --- BUILT-IN OBJECTS --- + +console.log("\n14. Date:"); +const date = new Date(0); +console.log(" expected: date string like 'Thu Jan 01 1970...'"); +console.log(" String(): '" + String(date) + "'"); +console.log(" toString: '" + date.toString() + "'"); + +console.log("\n15. RegExp:"); +const re = /test/gi; +console.log(" expected: '/test/gi'"); +console.log(" String(): '" + String(re) + "'"); +console.log(" toString: '" + re.toString() + "'"); + +console.log("\n16. Error:"); +const err = new Error("oops"); +console.log(" expected: 'Error: oops'"); +console.log(" String(): '" + String(err) + "'"); +console.log(" toString: '" + err.toString() + "'"); + +console.log("\n17. Map:"); +const map = new Map([["a", 1]]); +console.log(" expected: '[object Map]'"); +console.log(" String(): '" + String(map) + "'"); + +console.log("\n18. Set:"); +const set = new Set([1, 2, 3]); +console.log(" expected: '[object Set]'"); +console.log(" String(): '" + String(set) + "'"); + +// --- SYMBOL --- + +console.log("\n19. Symbol:"); +const sym = Symbol("test"); +console.log(" expected: 'Symbol(test)'"); +console.log(" String(): '" + String(sym) + "'"); +console.log(" toString: '" + sym.toString() + "'"); + +// --- Symbol.toPrimitive --- + +console.log("\n20. Object with Symbol.toPrimitive:"); +const obj6 = {}; +obj6[Symbol.toPrimitive] = function(hint) { return "toPrimitive-" + hint; }; +console.log(" expected: 'toPrimitive-string'"); +console.log(" String(): '" + String(obj6) + "'"); + +// --- CLASS INSTANCES --- + +console.log("\n21. Class instance with toString:"); +class MyClass { + toString() { return "MyClass instance"; } +} +const inst = new MyClass(); +console.log(" expected: 'MyClass instance'"); +console.log(" toString: '" + inst.toString() + "'"); +console.log(" String(): '" + String(inst) + "'"); + +console.log("\n22. Class instance without toString:"); +class PlainClass {} +const inst2 = new PlainClass(); +console.log(" expected: '[object Object]'"); +console.log(" String(): '" + String(inst2) + "'"); + +console.log("\n=== DONE ==="); diff --git a/tests/test-template-literal.js b/tests/test-template-literal.js new file mode 100644 index 0000000..1274502 --- /dev/null +++ b/tests/test-template-literal.js @@ -0,0 +1,135 @@ +// ============================================ +// TEST: Template literals use inspect instead of toString +// ============================================ +// Bug: `${obj}` uses inspect-style output instead of calling toString() +// ============================================ + +console.log("=== TEMPLATE LITERAL TESTS ===\n"); + +// --- PRIMITIVES --- + +console.log("1. Primitives:"); +console.log(" number: expected '42', got '" + `${42}` + "'"); +console.log(" string: expected 'hi', got '" + `${"hi"}` + "'"); +console.log(" boolean: expected 'true', got '" + `${true}` + "'"); +console.log(" null: expected 'null', got '" + `${null}` + "'"); +console.log(" undefined: expected 'undefined', got '" + `${undefined}` + "'"); +console.log(" BigInt: expected '123', got '" + `${123n}` + "'"); + +// --- ARRAYS --- + +console.log("\n2. Array [1, 2, 3]:"); +const arr1 = [1, 2, 3]; +console.log(" expected: '1,2,3'"); +console.log(" concat: '" + ("" + arr1) + "'"); +console.log(" template: '" + `${arr1}` + "'"); +console.log(" toString: '" + arr1.toString() + "'"); + +console.log("\n3. Nested array [[1,2], [3,4]]:"); +const arr2 = [[1, 2], [3, 4]]; +console.log(" expected: '1,2,3,4'"); +console.log(" concat: '" + ("" + arr2) + "'"); +console.log(" template: '" + `${arr2}` + "'"); +console.log(" toString: '" + arr2.toString() + "'"); + +console.log("\n4. Empty array []:"); +const arr3 = []; +console.log(" expected: ''"); +console.log(" concat: '" + ("" + arr3) + "'"); +console.log(" template: '" + `${arr3}` + "'"); + +// --- FUNCTIONS --- + +console.log("\n5. Arrow function:"); +const fn1 = () => 1; +console.log(" expected: '() => 1'"); +console.log(" concat: '" + ("" + fn1) + "'"); +console.log(" template: '" + `${fn1}` + "'"); +console.log(" toString: '" + fn1.toString() + "'"); + +console.log("\n6. Named function:"); +function namedFn() { return 1; } +console.log(" expected: 'function namedFn() { return 1; }'"); +console.log(" concat: '" + ("" + namedFn) + "'"); +console.log(" template: '" + `${namedFn}` + "'"); + +console.log("\n7. Function expression:"); +const fn2 = function() { return 1; }; +console.log(" expected: 'function() { return 1; }'"); +console.log(" concat: '" + ("" + fn2) + "'"); +console.log(" template: '" + `${fn2}` + "'"); + +// --- OBJECTS --- + +console.log("\n8. Plain object {}:"); +const obj1 = {}; +console.log(" expected: '[object Object]'"); +console.log(" concat: '" + ("" + obj1) + "'"); +console.log(" template: '" + `${obj1}` + "'"); + +console.log("\n9. Object with properties {a:1, b:2}:"); +const obj2 = { a: 1, b: 2 }; +console.log(" expected: '[object Object]'"); +console.log(" concat: '" + ("" + obj2) + "'"); +console.log(" template: '" + `${obj2}` + "'"); + +console.log("\n10. Object with custom toString:"); +const obj3 = { toString: function() { return "custom"; } }; +console.log(" expected: 'custom'"); +console.log(" toString: '" + obj3.toString() + "'"); +console.log(" template: '" + `${obj3}` + "'"); + +// --- BUILT-IN OBJECTS --- + +console.log("\n11. Date:"); +const date = new Date(0); +console.log(" expected: 'Thu Jan 01 1970...' (locale date string)"); +console.log(" concat: '" + ("" + date) + "'"); +console.log(" template: '" + `${date}` + "'"); + +console.log("\n12. RegExp /test/gi:"); +const re = /test/gi; +console.log(" expected: '/test/gi'"); +console.log(" concat: '" + ("" + re) + "'"); +console.log(" template: '" + `${re}` + "'"); + +console.log("\n13. Error:"); +const err = new Error("oops"); +console.log(" expected: 'Error: oops'"); +console.log(" concat: '" + ("" + err) + "'"); +console.log(" template: '" + `${err}` + "'"); + +console.log("\n14. Map:"); +const map = new Map([["a", 1]]); +console.log(" expected: '[object Map]'"); +console.log(" concat: '" + ("" + map) + "'"); +console.log(" template: '" + `${map}` + "'"); + +console.log("\n15. Set:"); +const set = new Set([1, 2, 3]); +console.log(" expected: '[object Set]'"); +console.log(" concat: '" + ("" + set) + "'"); +console.log(" template: '" + `${set}` + "'"); + +// --- SYMBOL (should throw) --- + +console.log("\n16. Symbol (should throw TypeError):"); +const sym = Symbol("test"); +console.log(" toString: '" + sym.toString() + "'"); +try { + console.log(" template: '" + `${sym}` + "'"); + console.log(" ERROR: should have thrown!"); +} catch (e) { + console.log(" template: threw " + e.name + " (correct)"); +} + +// --- QUINE TEST --- + +console.log("\n17. Quine test (self-referencing function):"); +const $ = function(_) { return "$=" + $ + ";$()"; }; +console.log(" expected: '$=function(_) { return \"$=\" + $ + \";$()\"; };$()'"); +console.log(" concat: '" + $() + "'"); +const $2 = function(_) { return `$=${$2};$()`; }; +console.log(" template: '" + $2() + "'"); + +console.log("\n=== DONE ==="); diff --git a/tests/test-valueof-toprimitive.js b/tests/test-valueof-toprimitive.js new file mode 100644 index 0000000..9a773f7 --- /dev/null +++ b/tests/test-valueof-toprimitive.js @@ -0,0 +1,243 @@ +// ============================================ +// TEST: valueOf and Symbol.toPrimitive behavior +// ============================================ +// Bug: valueOf is never called, toPrimitive may not work +// ============================================ + +console.log('=== valueOf AND Symbol.toPrimitive TESTS ===\n'); + +// --- valueOf TESTS --- + +console.log('SECTION 1: valueOf\n'); + +console.log('1. Only valueOf, returns string:'); +const obj1 = { + valueOf: function () { + return 'from-valueOf'; + } +}; +console.log(" expected concat: 'from-valueOf'"); +console.log(" concat: '" + ('' + obj1) + "'"); +console.log(" String(): '" + String(obj1) + "'"); + +console.log('\n2. Only valueOf, returns number:'); +const obj2 = { + valueOf: function () { + return 42; + } +}; +console.log(" expected concat: '42'"); +console.log(" concat: '" + ('' + obj2) + "'"); +console.log(" String(): '" + String(obj2) + "'"); + +console.log('\n3. Only valueOf, returns boolean:'); +const obj3 = { + valueOf: function () { + return true; + } +}; +console.log(" expected concat: 'true'"); +console.log(" concat: '" + ('' + obj3) + "'"); + +console.log('\n4. valueOf returns object (should fallback to toString):'); +const obj4 = { + valueOf: function () { + return {}; + } +}; +console.log(" expected: '[object Object]' (fallback)"); +console.log(" concat: '" + ('' + obj4) + "'"); + +console.log('\n5. valueOf returns null:'); +const obj5 = { + valueOf: function () { + return null; + } +}; +console.log(" expected: 'null'"); +console.log(" concat: '" + ('' + obj5) + "'"); + +console.log('\n6. valueOf returns undefined:'); +const obj6 = { + valueOf: function () { + return undefined; + } +}; +console.log(" expected: 'undefined'"); +console.log(" concat: '" + ('' + obj6) + "'"); + +// --- toString + valueOf priority --- + +console.log('\n\nSECTION 2: toString vs valueOf priority\n'); + +console.log('7. Both toString and valueOf (user functions):'); +const obj7 = { + toString: function () { + return 'from-toString'; + }, + valueOf: function () { + return 'from-valueOf'; + } +}; +console.log(" expected: 'from-toString' (toString has priority for string hint)"); +console.log(" direct toString: '" + obj7.toString() + "'"); +console.log(" direct valueOf: '" + obj7.valueOf() + "'"); +const result7 = '' + obj7; +console.log(" concat: '" + result7 + "'"); +console.log(' (if missing, CRASH)'); + +console.log('\n8. toString throws, valueOf works:'); +const obj8 = { + toString: function () { + throw new Error('toString error'); + }, + valueOf: function () { + return 'from-valueOf'; + } +}; +console.log(" expected: 'from-valueOf' (fallback) or Error"); +try { + console.log(" concat: '" + ('' + obj8) + "'"); +} catch (e) { + console.log(' error: ' + e.message); +} + +console.log('\n9. toString returns object, valueOf returns string:'); +const obj9 = { + toString: function () { + return {}; + }, + valueOf: function () { + return 'from-valueOf'; + } +}; +console.log(" expected: 'from-valueOf' (fallback)"); +try { + console.log(" concat: '" + ('' + obj9) + "'"); +} catch (e) { + console.log(' error: ' + e.message); +} + +console.log('\n10. Both return objects (should throw TypeError):'); +const obj10 = { + toString: function () { + return {}; + }, + valueOf: function () { + return {}; + } +}; +console.log(' expected: TypeError'); +try { + console.log(" concat: '" + ('' + obj10) + "'"); +} catch (e) { + console.log(' error: ' + e.name + ': ' + e.message); +} + +// --- Symbol.toPrimitive --- + +console.log('\n\nSECTION 3: Symbol.toPrimitive\n'); + +console.log('11. toPrimitive returns string:'); +const obj11 = {}; +obj11[Symbol.toPrimitive] = function (hint) { + return 'primitive-' + hint; +}; +console.log(" expected concat: 'primitive-default' or 'primitive-string'"); +console.log(" concat: '" + ('' + obj11) + "'"); +console.log(" String(): '" + String(obj11) + "'"); +console.log(" template: '" + `${obj11}` + "'"); + +console.log('\n12. toPrimitive returns number:'); +const obj12 = {}; +obj12[Symbol.toPrimitive] = function (hint) { + return 99; +}; +console.log(" expected: '99'"); +console.log(" concat: '" + ('' + obj12) + "'"); + +console.log('\n13. toPrimitive overrides toString and valueOf:'); +const obj13 = { + toString: function () { + return 'from-toString'; + }, + valueOf: function () { + return 'from-valueOf'; + } +}; +obj13[Symbol.toPrimitive] = function (hint) { + return 'from-toPrimitive'; +}; +console.log(" expected: 'from-toPrimitive' (toPrimitive has highest priority)"); +console.log(" concat: '" + ('' + obj13) + "'"); + +console.log('\n14. toPrimitive returns object (should throw):'); +const obj14 = {}; +obj14[Symbol.toPrimitive] = function (hint) { + return {}; +}; +console.log(' expected: TypeError'); +try { + console.log(" concat: '" + ('' + obj14) + "'"); +} catch (e) { + console.log(' error: ' + e.name + ': ' + e.message); +} + +console.log('\n15. toPrimitive throws:'); +const obj15 = {}; +obj15[Symbol.toPrimitive] = function (hint) { + throw new Error('toPrimitive error'); +}; +console.log(' expected: Error: toPrimitive error'); +try { + console.log(" concat: '" + ('' + obj15) + "'"); +} catch (e) { + if (e && e.message) { + console.log(' error: ' + e.message); + } else { + console.log(' error: ' + String(e)); + } +} + +console.log('\n16. toPrimitive is not a function:'); +const obj16 = {}; +obj16[Symbol.toPrimitive] = 'not a function'; +console.log(' expected: TypeError or fallback to toString'); +try { + console.log(" concat: '" + ('' + obj16) + "'"); +} catch (e) { + console.log(' error: ' + e.name + ': ' + e.message); +} + +// --- Hint values --- + +console.log('\n\nSECTION 4: Hint values\n'); + +console.log('17. Check hint values:'); +const hints = []; +const obj17 = {}; +obj17[Symbol.toPrimitive] = function (hint) { + hints.push(hint); + return 'ok'; +}; + +console.log(" concat '' + obj:"); +let r1 = '' + obj17; +console.log(" hint was: '" + hints[hints.length - 1] + "'"); + +console.log(' String(obj):'); +let r2 = String(obj17); +console.log(" hint was: '" + hints[hints.length - 1] + "'"); + +console.log(' template `${obj}`:'); +let r3 = `${obj17}`; +console.log(" hint was: '" + hints[hints.length - 1] + "'"); + +console.log(' +obj (unary plus, number hint):'); +let r4 = +obj17; +console.log(" hint was: '" + hints[hints.length - 1] + "'"); + +console.log('\n All hints collected: [' + hints.join(', ') + ']'); +console.log(" Expected: ['default', 'string', 'string', 'number']"); + +console.log('\n=== DONE ===');