diff --git a/examples/demo/kat.js b/examples/demo/kat.js index cb03897..6e4651d 100755 --- a/examples/demo/kat.js +++ b/examples/demo/kat.js @@ -3,6 +3,11 @@ import fs from 'node:fs'; import path from 'node:path'; +function exit(message) { + console.log(message); + process.exit(1); +} + const file = process.argv[2]; if (!file) exit('usage: kat '); @@ -12,6 +17,5 @@ try { console.log(Ant.highlight(content)); } else console.log(content); } catch (err) { - console.log(`file '${err.path}' not found`); - process.exit(1); + exit(`file '${err.path}' not found`); } diff --git a/include/common.h b/include/common.h index f805ef1..fcb0cff 100644 --- a/include/common.h +++ b/include/common.h @@ -79,6 +79,11 @@ X(SLOT_RESPONSE_HEADERS) \ X(SLOT_RESPONSE_BODY_STREAM) \ X(SLOT_PIPE_ABORT_LISTENER) \ + X(SLOT_REGEXP_FLAGS_MASK) \ + X(SLOT_REGEXP_FLAGS_STRING) \ + X(SLOT_REGEXP_NAMED_GROUPS) \ + X(SLOT_REGEXP_RESULT_GROUPS) \ + X(SLOT_REGEXP_GROUPS_CACHE) \ X(SLOT_MATCHALL_RX) \ X(SLOT_MATCHALL_STR) \ X(SLOT_MATCHALL_DONE) diff --git a/include/modules/regex.h b/include/modules/regex.h index 122a2d5..70964f1 100644 --- a/include/modules/regex.h +++ b/include/modules/regex.h @@ -28,4 +28,12 @@ ant_value_t is_regexp_like(ant_t *js, ant_value_t value); ant_value_t do_regex_match_pcre2(ant_t *js, regex_match_args_t args); ant_value_t reject_regexp_arg(ant_t *js, ant_value_t value, const char *method_name); +bool regexp_exec_truthy_try_fast( + ant_t *js, + ant_value_t call_func, + ant_value_t regexp, + ant_value_t arg, + ant_value_t *out_result +); + #endif diff --git a/include/silver/opcode.h b/include/silver/opcode.h index 21ec9f2..6983764 100644 --- a/include/silver/opcode.h +++ b/include/silver/opcode.h @@ -162,6 +162,7 @@ OP_DEF( JMP_TRUE8, 2, 1, 0, label8) /* short conditional */ OP_DEF( CALL, 3, 1, 1, npop) /* func args... -> result */ OP_DEF( CALL_METHOD, 3, 2, 1, npop) /* this func args... -> result */ OP_DEF( CALL_IS_PROTO, 3, 3, 1, u16) /* this func arg -> bool (ic_idx:u16) */ +OP_DEF( RE_EXEC_TRUTHY, 1, 3, 1, none) /* this func arg -> bool */ OP_DEF( TAIL_CALL, 3, 1, 0, npop) /* tail-position call */ OP_DEF( TAIL_CALL_METHOD, 3, 2, 0, npop) OP_DEF( NEW, 3, 2, 1, npop) /* func new.target args -> obj */ diff --git a/src/modules/regex.c b/src/modules/regex.c index 1223398..640fc1f 100644 --- a/src/modules/regex.c +++ b/src/modules/regex.c @@ -27,12 +27,86 @@ typedef struct { bool jit_ready; } regex_cache_entry_t; +enum { + REGEXP_FLAG_HAS_INDICES = 1 << 0, + REGEXP_FLAG_GLOBAL = 1 << 1, + REGEXP_FLAG_IGNORE_CASE = 1 << 2, + REGEXP_FLAG_MULTILINE = 1 << 3, + REGEXP_FLAG_DOTALL = 1 << 4, + REGEXP_FLAG_UNICODE = 1 << 5, + REGEXP_FLAG_UNICODE_SET = 1 << 6, + REGEXP_FLAG_STICKY = 1 << 7, +}; + static regex_cache_entry_t *regex_cache = NULL; static ant_value_t regexp_matchall_iter_proto_val = 0; static size_t regex_cache_count = 0; static size_t regex_cache_cap = 0; +static inline uint8_t regexp_parse_flags_mask(const char *fstr, ant_offset_t flen) { + uint8_t mask = 0; + for (ant_offset_t k = 0; k < flen; k++) { + switch (fstr[k]) { + case 'd': mask |= REGEXP_FLAG_HAS_INDICES; break; + case 'g': mask |= REGEXP_FLAG_GLOBAL; break; + case 'i': mask |= REGEXP_FLAG_IGNORE_CASE; break; + case 'm': mask |= REGEXP_FLAG_MULTILINE; break; + case 's': mask |= REGEXP_FLAG_DOTALL; break; + case 'u': mask |= REGEXP_FLAG_UNICODE; break; + case 'v': mask |= REGEXP_FLAG_UNICODE_SET; break; + case 'y': mask |= REGEXP_FLAG_STICKY; break; + default: break; + }} + return mask; +} + +static inline uint8_t regexp_flags_mask(ant_t *js, ant_value_t regexp) { + ant_offset_t flags_off = lkp(js, regexp, "flags", 5); + if (flags_off == 0) return 0; + + ant_value_t flags_val = js_propref_load(js, flags_off); + if (vtype(flags_val) != T_STR) return 0; + + ant_value_t cached_flags = js_get_slot(regexp, SLOT_REGEXP_FLAGS_STRING); + ant_value_t cached = js_get_slot(regexp, SLOT_REGEXP_FLAGS_MASK); + if (flags_val == cached_flags && vtype(cached) == T_NUM) return (uint8_t)tod(cached); + + ant_offset_t flen, foff = vstr(js, flags_val, &flen); + uint8_t mask = regexp_parse_flags_mask((const char *)(uintptr_t)foff, flen); + js_set_slot(regexp, SLOT_REGEXP_FLAGS_MASK, tov((double)mask)); + js_set_slot(regexp, SLOT_REGEXP_FLAGS_STRING, flags_val); + + return mask; +} + +static ant_value_t regexp_build_named_groups_meta(ant_t *js, pcre2_code *code) { + uint32_t namecount = 0; + pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &namecount); + if (namecount == 0) return js_mkundef(); + + uint32_t nameentrysize = 0; + PCRE2_SPTR nametable = NULL; + pcre2_pattern_info(code, PCRE2_INFO_NAMEENTRYSIZE, &nameentrysize); + pcre2_pattern_info(code, PCRE2_INFO_NAMETABLE, (void *)&nametable); + + ant_value_t meta = js_mkarr(js); + if (is_err(meta)) return meta; + + PCRE2_SPTR tabptr = nametable; + for (uint32_t i = 0; i < namecount; i++) { + int n = (tabptr[0] << 8) | tabptr[1]; + const char *name = (const char *)(tabptr + 2); + ant_value_t name_val = js_mkstr(js, name, strlen(name)); + if (is_err(name_val)) return name_val; + js_arr_push(js, meta, name_val); + js_arr_push(js, meta, tov((double)n)); + tabptr += nameentrysize; + } + + return meta; +} + static void update_regexp_statics(ant_t *js, const char *str_ptr, PCRE2_SIZE *ovector, uint32_t ovcount) { ant_value_t regexp_ctor = js_get(js, js_glob(js), "RegExp"); if (is_err(regexp_ctor) || vtype(regexp_ctor) == T_UNDEF) return; @@ -43,14 +117,14 @@ static void update_regexp_statics(ant_t *js, const char *str_ptr, PCRE2_SIZE *ov ant_value_t val = empty; if ((uint32_t)i < ovcount && ovector[2*i] != PCRE2_UNSET) val = js_mkstr(js, str_ptr + ovector[2*i], ovector[2*i+1] - ovector[2*i]); - js_set(js, regexp_ctor, key, val); + if (is_err(setprop_cstr(js, regexp_ctor, key, 2, val))) return; } ant_value_t match0 = empty; if (ovcount > 0 && ovector[0] != PCRE2_UNSET) match0 = js_mkstr(js, str_ptr + ovector[0], ovector[1] - ovector[0]); - js_set(js, regexp_ctor, "lastMatch", match0); - js_set(js, regexp_ctor, "$&", match0); + if (is_err(setprop_cstr(js, regexp_ctor, "lastMatch", 9, match0))) return; + (void)setprop_cstr(js, regexp_ctor, "$&", 2, match0); } static inline bool is_pcre2_passthrough_escape(char c) { @@ -415,19 +489,15 @@ size_t js_to_pcre2_pattern(const char *src, size_t src_len, char *dst, size_t ds : js_setprop(js, obj, js_mkstr(js, key, klen), val)) static void regexp_init_flags(ant_t *js, ant_value_t obj, const char *fstr, ant_offset_t flen, bool is_new) { - bool d = false, g = false, i = false, m = false; - bool s = false, u = false, v = false, y = false; - - for (ant_offset_t k = 0; k < flen; k++) { - if (fstr[k] == 'd') d = true; - if (fstr[k] == 'g') g = true; - if (fstr[k] == 'i') i = true; - if (fstr[k] == 'm') m = true; - if (fstr[k] == 's') s = true; - if (fstr[k] == 'u') u = true; - if (fstr[k] == 'v') v = true; - if (fstr[k] == 'y') y = true; - } + uint8_t mask = regexp_parse_flags_mask(fstr, flen); + bool d = (mask & REGEXP_FLAG_HAS_INDICES) != 0; + bool g = (mask & REGEXP_FLAG_GLOBAL) != 0; + bool i = (mask & REGEXP_FLAG_IGNORE_CASE) != 0; + bool m = (mask & REGEXP_FLAG_MULTILINE) != 0; + bool s = (mask & REGEXP_FLAG_DOTALL) != 0; + bool u = (mask & REGEXP_FLAG_UNICODE) != 0; + bool v = (mask & REGEXP_FLAG_UNICODE_SET) != 0; + bool y = (mask & REGEXP_FLAG_STICKY) != 0; char sorted[10]; int si = 0; if (d) sorted[si++] = 'd'; @@ -439,7 +509,8 @@ static void regexp_init_flags(ant_t *js, ant_value_t obj, const char *fstr, ant_ if (v) sorted[si++] = 'v'; if (y) sorted[si++] = 'y'; - REGEXP_SET_PROP(js, obj, "flags", 5, js_mkstr(js, sorted, si), is_new); + ant_value_t flags_value = js_mkstr(js, sorted, si); + REGEXP_SET_PROP(js, obj, "flags", 5, flags_value, is_new); REGEXP_SET_PROP(js, obj, "hasIndices", 10, mkval(T_BOOL, d ? 1 : 0), is_new); REGEXP_SET_PROP(js, obj, "global", 6, mkval(T_BOOL, g ? 1 : 0), is_new); REGEXP_SET_PROP(js, obj, "ignoreCase", 10, mkval(T_BOOL, i ? 1 : 0), is_new); @@ -449,6 +520,9 @@ static void regexp_init_flags(ant_t *js, ant_value_t obj, const char *fstr, ant_ REGEXP_SET_PROP(js, obj, "unicodeSets", 11, mkval(T_BOOL, v ? 1 : 0), is_new); REGEXP_SET_PROP(js, obj, "sticky", 6, mkval(T_BOOL, y ? 1 : 0), is_new); REGEXP_SET_PROP(js, obj, "lastIndex", 9, tov(0), is_new); + js_set_slot(obj, SLOT_REGEXP_FLAGS_MASK, tov((double)mask)); + js_set_slot(obj, SLOT_REGEXP_FLAGS_STRING, flags_value); + js_set_slot(obj, SLOT_REGEXP_NAMED_GROUPS, js_mkundef()); } ant_value_t is_regexp_like(ant_t *js, ant_value_t value) { @@ -554,6 +628,7 @@ typedef struct { static bool regex_get_or_compile(ant_t *js, ant_value_t regexp_obj, compiled_regex_t *out) { ant_object_t *obj_ptr = js_obj_ptr(regexp_obj); + uint8_t flags_mask = regexp_flags_mask(js, regexp_obj); regex_cache_entry_t *cached = regex_cache_lookup(obj_ptr); if (cached) { @@ -571,28 +646,16 @@ static bool regex_get_or_compile(ant_t *js, ant_value_t regexp_obj, compiled_reg ant_offset_t plen, poff = vstr(js, source_val, &plen); const char *pattern_ptr = (char *)(uintptr_t)(poff); - bool ignore_case = false, multiline = false, dotall = false, v_flag = false; - ant_offset_t flags_off = lkp(js, regexp_obj, "flags", 5); - if (flags_off != 0) { - ant_value_t flags_val = js_propref_load(js, flags_off); - if (vtype(flags_val) == T_STR) { - ant_offset_t flen, foff = vstr(js, flags_val, &flen); - const char *flags_str = (char *)(uintptr_t)(foff); - for (ant_offset_t i = 0; i < flen; i++) { - if (flags_str[i] == 'i') ignore_case = true; - if (flags_str[i] == 'm') multiline = true; - if (flags_str[i] == 's') dotall = true; - if (flags_str[i] == 'v') v_flag = true; - }} - } - char pcre2_pattern[4096]; - size_t pcre2_len = js_to_pcre2_pattern(pattern_ptr, plen, pcre2_pattern, sizeof(pcre2_pattern), v_flag); + size_t pcre2_len = js_to_pcre2_pattern( + pattern_ptr, plen, pcre2_pattern, sizeof(pcre2_pattern), + (flags_mask & REGEXP_FLAG_UNICODE_SET) != 0 + ); uint32_t options = PCRE2_UTF | PCRE2_UCP | PCRE2_MATCH_UNSET_BACKREF | PCRE2_DUPNAMES; - if (ignore_case) options |= PCRE2_CASELESS; - if (multiline) options |= PCRE2_MULTILINE; - if (dotall) options |= PCRE2_DOTALL; + if (flags_mask & REGEXP_FLAG_IGNORE_CASE) options |= PCRE2_CASELESS; + if (flags_mask & REGEXP_FLAG_MULTILINE) options |= PCRE2_MULTILINE; + if (flags_mask & REGEXP_FLAG_DOTALL) options |= PCRE2_DOTALL; int errcode; PCRE2_SIZE erroffset; @@ -602,10 +665,20 @@ static bool regex_get_or_compile(ant_t *js, ant_value_t regexp_obj, compiled_reg pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(re, NULL); bool jit_ready = pcre2_jit_compile(re, PCRE2_JIT_COMPLETE) == 0; regex_cache_insert(obj_ptr, re, match_data, jit_ready); - + ant_value_t groups_meta = regexp_build_named_groups_meta(js, re); + + if (is_err(groups_meta)) { + pcre2_match_data_free(match_data); + pcre2_code_free(re); + regex_cache_count--; + return false; + } + + js_set_slot(regexp_obj, SLOT_REGEXP_NAMED_GROUPS, groups_meta); out->code = re; out->match_data = match_data; out->jit_ready = jit_ready; + return true; } @@ -677,31 +750,45 @@ static ant_value_t builtin_RegExp(ant_t *js, ant_value_t *args, int nargs) { return regexp_obj; } -static ant_value_t builtin_regexp_exec(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t regexp = js->this_val; - if (vtype(regexp) != T_OBJ) return js_mkerr(js, "exec called on non-regexp"); - if (nargs < 1) return js_mknull(); +static ant_value_t builtin_regexp_groups_getter(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t result_arr = js->this_val; + if (!is_object_type(result_arr)) return js_mkundef(); + + ant_value_t cached = js_get_slot(result_arr, SLOT_REGEXP_GROUPS_CACHE); + if (is_object_type(cached)) return cached; + + ant_value_t meta = js_get_slot(result_arr, SLOT_REGEXP_RESULT_GROUPS); + if (!is_object_type(meta)) return js_mkundef(); + + ant_value_t groups = js_mkobj(js); + if (is_err(groups)) return groups; + js_set_proto_init(groups, js_mknull()); + + for (ant_offset_t i = 0; ; i += 2) { + ant_value_t name = js_arr_get(js, meta, i); + if (vtype(name) == T_UNDEF) break; + ant_value_t index_val = js_arr_get(js, meta, i + 1); + ant_offset_t index = (vtype(index_val) == T_NUM) ? (ant_offset_t)tod(index_val) : 0; + char idxstr[16]; + (void)uint_to_str(idxstr, sizeof(idxstr), (uint64_t)index); + ant_value_t value = js_getprop_fallback(js, result_arr, idxstr); + ant_offset_t name_len, name_off = vstr(js, name, &name_len); + ant_value_t status = setprop_cstr(js, groups, (const char *)(uintptr_t)name_off, (size_t)name_len, value); + if (is_err(status)) return status; + } - ant_value_t str_arg = args[0]; - if (vtype(str_arg) != T_STR) return js_mknull(); + js_set_slot(result_arr, SLOT_REGEXP_GROUPS_CACHE, groups); + return groups; +} +static ant_value_t regexp_exec_internal(ant_t *js, ant_value_t regexp, ant_value_t str_arg, bool truthy_only) { ant_offset_t str_len, str_off = vstr(js, str_arg, &str_len); const char *str_ptr = (char *)(uintptr_t)(str_off); + uint8_t flags_mask = regexp_flags_mask(js, regexp); + bool global_flag = (flags_mask & REGEXP_FLAG_GLOBAL) != 0; + bool sticky_flag = (flags_mask & REGEXP_FLAG_STICKY) != 0; - bool global_flag = false, sticky_flag = false; - ant_offset_t flags_off = lkp(js, regexp, "flags", 5); - if (flags_off != 0) { - ant_value_t flags_val = js_propref_load(js, flags_off); - if (vtype(flags_val) == T_STR) { - ant_offset_t flen, foff = vstr(js, flags_val, &flen); - const char *flags_str = (char *)(uintptr_t)(foff); - for (ant_offset_t i = 0; i < flen; i++) { - if (flags_str[i] == 'g') global_flag = true; - if (flags_str[i] == 'y') sticky_flag = true; - } - } - } - + // TODO: reduce nesting PCRE2_SIZE start_offset = 0; if (global_flag || sticky_flag) { ant_offset_t lastindex_off = lkp(js, regexp, "lastIndex", 9); @@ -711,7 +798,7 @@ static ant_value_t builtin_regexp_exec(ant_t *js, ant_value_t *args, int nargs) double li = tod(li_val); if (li >= 0 && li <= (double)str_len) start_offset = (PCRE2_SIZE)li; else { - js_setprop(js, regexp, js_mkstr(js, "lastIndex", 9), tov(0)); + if (is_err(setprop_cstr(js, regexp, "lastIndex", 9, tov(0)))) return js_mkerr(js, "oom"); return js_mknull(); } } @@ -730,14 +817,26 @@ static ant_value_t builtin_regexp_exec(ant_t *js, ant_value_t *args, int nargs) } else rc = pcre2_match(compiled.code, (PCRE2_SPTR)str_ptr, str_len, start_offset, match_options, compiled.match_data, NULL); if (rc < 0) { - if (global_flag || sticky_flag) js_setprop(js, regexp, js_mkstr(js, "lastIndex", 9), tov(0)); + if ((global_flag || sticky_flag) && is_err(setprop_cstr(js, regexp, "lastIndex", 9, tov(0)))) { + return js_mkerr(js, "oom"); + } return js_mknull(); } PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(compiled.match_data); uint32_t ovcount = pcre2_get_ovector_count(compiled.match_data); + update_regexp_statics(js, str_ptr, ovector, ovcount); + + if (global_flag || sticky_flag) { + ant_value_t next_idx = tov((double)ovector[1]); + if (is_err(setprop_cstr(js, regexp, "lastIndex", 9, next_idx))) return js_mkerr(js, "oom"); + } + + if (truthy_only) return js_true; + ant_value_t result_arr = js_mkarr(js); + if (is_err(result_arr)) return result_arr; for (uint32_t i = 0; i < ovcount && i < 32; i++) { PCRE2_SIZE start = ovector[2*i]; PCRE2_SIZE end = ovector[2*i+1]; @@ -749,42 +848,31 @@ static ant_value_t builtin_regexp_exec(ant_t *js, ant_value_t *args, int nargs) } } - js_setprop(js, result_arr, js_mkstr(js, "index", 5), tov((double)ovector[0])); - js_setprop(js, result_arr, js_mkstr(js, "input", 5), str_arg); + if (is_err(setprop_cstr(js, result_arr, "index", 5, tov((double)ovector[0])))) return js_mkerr(js, "oom"); + if (is_err(setprop_cstr(js, result_arr, "input", 5, str_arg))) return js_mkerr(js, "oom"); - uint32_t namecount = 0; - pcre2_pattern_info(compiled.code, PCRE2_INFO_NAMECOUNT, &namecount); - if (namecount > 0) { - uint32_t nameentrysize = 0; - PCRE2_SPTR nametable = NULL; - pcre2_pattern_info(compiled.code, PCRE2_INFO_NAMEENTRYSIZE, &nameentrysize); - pcre2_pattern_info(compiled.code, PCRE2_INFO_NAMETABLE, (void *)&nametable); - - ant_value_t groups = js_mkobj(js); - js_set_proto_init(groups, js_mknull()); - - PCRE2_SPTR tabptr = nametable; - for (uint32_t i = 0; i < namecount; i++) { - int n = (tabptr[0] << 8) | tabptr[1]; - const char *name = (const char *)(tabptr + 2); - ant_value_t val = ((uint32_t)n < ovcount) ? js_arr_get(js, result_arr, n) : js_mkundef(); - js_setprop(js, groups, js_mkstr(js, name, strlen(name)), val); - tabptr += nameentrysize; - } - js_setprop(js, result_arr, js_mkstr(js, "groups", 6), groups); - } else js_setprop(js, result_arr, js_mkstr(js, "groups", 6), js_mkundef()); + ant_value_t groups_meta = js_get_slot(regexp, SLOT_REGEXP_NAMED_GROUPS); + if (is_object_type(groups_meta)) { + js_set_slot(result_arr, SLOT_REGEXP_RESULT_GROUPS, groups_meta); + js_set_slot(result_arr, SLOT_REGEXP_GROUPS_CACHE, js_mkundef()); + js_set_getter_desc(js, js_as_obj(result_arr), "groups", 6, js_mkfun(builtin_regexp_groups_getter), JS_DESC_E | JS_DESC_C); + } else if (is_err(setprop_cstr(js, result_arr, "groups", 6, js_mkundef()))) return js_mkerr(js, "oom"); - update_regexp_statics(js, str_ptr, ovector, ovcount); + return result_arr; +} - if (global_flag || sticky_flag) { - js_setprop(js, regexp, js_mkstr(js, "lastIndex", 9), tov((double)ovector[1])); - } +static ant_value_t builtin_regexp_exec(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t regexp = js->this_val; + if (vtype(regexp) != T_OBJ) return js_mkerr(js, "exec called on non-regexp"); + if (nargs < 1) return js_mknull(); - return result_arr; + ant_value_t str_arg = args[0]; + if (vtype(str_arg) != T_STR) return js_mknull(); + + return regexp_exec_internal(js, regexp, str_arg, false); } static ant_value_t builtin_regexp_toString(ant_t *js, ant_value_t *args, int nargs) { - (void)args; (void)nargs; ant_value_t regexp = js->this_val; if (!is_object_type(regexp)) return js_mkerr_typed(js, JS_ERR_TYPE, "toString called on non-object"); @@ -955,13 +1043,42 @@ static ant_value_t regexp_exec_abstract(ant_t *js, ant_value_t rx, ant_value_t s return result; } +bool regexp_exec_truthy_try_fast( + ant_t *js, + ant_value_t call_func, + ant_value_t regexp, + ant_value_t arg, + ant_value_t *out_result +) { + if (!out_result || vtype(call_func) != T_CFUNC) return false; + if (js_as_cfunc(call_func) != builtin_regexp_exec) return false; + if (!is_object_type(regexp) || vtype(arg) != T_STR) return false; + + ant_value_t result = regexp_exec_internal(js, regexp, arg, true); + if (is_err(result)) { + *out_result = result; + return true; + } + + *out_result = mkval(T_BOOL, vtype(result) != T_NULL ? 1 : 0); + return true; +} + static ant_value_t builtin_regexp_test(ant_t *js, ant_value_t *args, int nargs) { ant_value_t regexp = js->this_val; if (!is_object_type(regexp)) return js_mkerr_typed(js, JS_ERR_TYPE, "test called on non-object"); ant_value_t str_arg = nargs > 0 ? js_tostring_val(js, args[0]) : js_mkstr(js, "undefined", 9); if (is_err(str_arg)) return str_arg; - ant_value_t result = regexp_exec_abstract(js, regexp, str_arg); + ant_value_t exec_fn = js_get(js, regexp, "exec"); + if (is_err(exec_fn)) return exec_fn; + + ant_value_t result; + if (vtype(exec_fn) == T_CFUNC && js_as_cfunc(exec_fn) == builtin_regexp_exec) { + result = regexp_exec_internal(js, regexp, str_arg, true); + } else { + result = regexp_exec_abstract(js, regexp, str_arg); + } if (is_err(result)) return result; return mkval(T_BOOL, vtype(result) != T_NULL ? 1 : 0); } @@ -974,18 +1091,16 @@ static ant_value_t builtin_regexp_flags_getter(ant_t *js, ant_value_t *args, int char buf[16]; int n = 0; - - static const struct { const char *name; size_t len; char flag; } flag_props[] = { - {"hasIndices", 10, 'd'}, {"global", 6, 'g'}, {"ignoreCase", 10, 'i'}, - {"multiline", 9, 'm'}, {"dotAll", 6, 's'}, {"unicode", 7, 'u'}, - {"unicodeSets", 11, 'v'}, {"sticky", 6, 'y'}, - }; - - for (int i = 0; i < 8; i++) { - ant_value_t v = js_getprop_fallback(js, rx, flag_props[i].name); - if (is_err(v)) return v; - if (js_truthy(js, v)) buf[n++] = flag_props[i].flag; - } + uint8_t mask = regexp_flags_mask(js, rx); + + if (mask & REGEXP_FLAG_HAS_INDICES) buf[n++] = 'd'; + if (mask & REGEXP_FLAG_GLOBAL) buf[n++] = 'g'; + if (mask & REGEXP_FLAG_IGNORE_CASE) buf[n++] = 'i'; + if (mask & REGEXP_FLAG_MULTILINE) buf[n++] = 'm'; + if (mask & REGEXP_FLAG_DOTALL) buf[n++] = 's'; + if (mask & REGEXP_FLAG_UNICODE) buf[n++] = 'u'; + if (mask & REGEXP_FLAG_UNICODE_SET) buf[n++] = 'v'; + if (mask & REGEXP_FLAG_STICKY) buf[n++] = 'y'; return js_mkstr(js, buf, n); } diff --git a/src/silver/compiler.c b/src/silver/compiler.c index 85c6504..72a7d05 100644 --- a/src/silver/compiler.c +++ b/src/silver/compiler.c @@ -56,6 +56,9 @@ static void emit_op(sv_compiler_t *c, sv_op_t op) { emit(c, (uint8_t)op); } +static void compile_receiver_property_get(sv_compiler_t *c, sv_ast_t *node); +static void compile_truthy_test_expr(sv_compiler_t *c, sv_ast_t *node); + static void emit_srcpos(sv_compiler_t *c, sv_ast_t *node) { if (!node) return; const char *code = c->source; @@ -1969,7 +1972,7 @@ void compile_lhs_set(sv_compiler_t *c, sv_ast_t *target, bool keep) { } void compile_ternary(sv_compiler_t *c, sv_ast_t *node) { - compile_expr(c, node->cond); + compile_truthy_test_expr(c, node->cond); int else_jump = emit_jump(c, OP_JMP_FALSE); compile_expr(c, node->left); int end_jump = emit_jump(c, OP_JMP); @@ -2212,6 +2215,32 @@ static bool compile_call_is_proto_intrinsic( return true; } +static bool compile_regexp_exec_truthy_intrinsic( + sv_compiler_t *c, sv_ast_t *node +) { + if (!node || node->type != N_CALL || call_has_spread_arg(node) || node->args.count != 1) + return false; + + sv_ast_t *callee = node->left; + if (!callee || callee->type != N_MEMBER) return false; + if ((callee->flags & 1) || !callee->right || !callee->right->str) return false; + if (is_ident_name(callee->left, "super")) return false; + if (!is_ident_str(callee->right->str, callee->right->len, "exec", 4)) + return false; + + compile_expr(c, callee->left); + compile_receiver_property_get(c, callee); + compile_expr(c, node->args.items[0]); + emit_op(c, OP_RE_EXEC_TRUTHY); + + return true; +} + +static void compile_truthy_test_expr(sv_compiler_t *c, sv_ast_t *node) { + if (compile_regexp_exec_truthy_intrinsic(c, node)) return; + compile_expr(c, node); +} + static void compile_optional_call_after_setup( sv_compiler_t *c, sv_ast_t *call_node, sv_call_kind_t kind, bool has_spread @@ -2763,7 +2792,7 @@ static void compile_tail_call(sv_compiler_t *c, sv_ast_t *node) { void compile_tail_return_expr(sv_compiler_t *c, sv_ast_t *expr) { if (expr->type == N_TERNARY) { - compile_expr(c, expr->cond); + compile_truthy_test_expr(c, expr->cond); int else_jump = emit_jump(c, OP_JMP_FALSE); compile_tail_return_expr(c, expr->left); patch_jump(c, else_jump); @@ -3190,8 +3219,7 @@ void compile_var_decl(sv_compiler_t *c, sv_ast_t *node) { } } -void compile_destructure_binding(sv_compiler_t *c, sv_ast_t *pat, - sv_var_kind_t kind) { +void compile_destructure_binding(sv_compiler_t *c, sv_ast_t *pat, sv_var_kind_t kind) { compile_destructure_pattern(c, pat, false, false, DESTRUCTURE_BIND, kind); } @@ -3229,7 +3257,6 @@ static bool fold_static_typeof_compare( return true; } - void compile_if(sv_compiler_t *c, sv_ast_t *node) { bool folded_truth = false; if (fold_static_typeof_compare(c, node->cond, &folded_truth)) { @@ -3238,7 +3265,7 @@ void compile_if(sv_compiler_t *c, sv_ast_t *node) { return; } - compile_expr(c, node->cond); + compile_truthy_test_expr(c, node->cond); int else_jump = emit_jump(c, OP_JMP_FALSE); compile_stmt(c, node->left); if (node->right) { @@ -3251,12 +3278,11 @@ void compile_if(sv_compiler_t *c, sv_ast_t *node) { } } - void compile_while(sv_compiler_t *c, sv_ast_t *node) { int loop_start = c->code_len; push_loop(c, loop_start, NULL, 0, false); - compile_expr(c, node->cond); + compile_truthy_test_expr(c, node->cond); int exit_jump = emit_jump(c, OP_JMP_FALSE); compile_stmt(c, node->body); @@ -3269,27 +3295,22 @@ void compile_while(sv_compiler_t *c, sv_ast_t *node) { pop_loop(c); } - void compile_do_while(sv_compiler_t *c, sv_ast_t *node) { int loop_start = c->code_len; push_loop(c, loop_start, NULL, 0, false); - compile_stmt(c, node->body); sv_loop_t *loop = &c->loops[c->loop_count - 1]; - int cond_start = c->code_len; for (int i = 0; i < loop->continues.count; i++) patch_jump(c, loop->continues.offsets[i]); - compile_expr(c, node->cond); + compile_truthy_test_expr(c, node->cond); int exit_jump = emit_jump(c, OP_JMP_FALSE); emit_loop(c, loop_start); patch_jump(c, exit_jump); pop_loop(c); - (void)cond_start; } - static void for_add_slot_unique(int **slots, int *count, int *cap, int slot) { if (slot < 0) return; for (int i = 0; i < *count; i++) { @@ -3305,8 +3326,7 @@ static void for_add_slot_unique(int **slots, int *count, int *cap, int slot) { (*slots)[(*count)++] = slot; } -static void for_collect_pattern_slots(sv_compiler_t *c, sv_ast_t *pat, - int **slots, int *count, int *cap) { +static void for_collect_pattern_slots(sv_compiler_t *c, sv_ast_t *pat, int **slots, int *count, int *cap) { if (!pat) return; switch (pat->type) { case N_IDENT: { @@ -3343,8 +3363,7 @@ static void for_collect_pattern_slots(sv_compiler_t *c, sv_ast_t *pat, } } -static void for_collect_var_decl_slots(sv_compiler_t *c, sv_ast_t *init_var, - int **slots, int *count, int *cap) { +static void for_collect_var_decl_slots(sv_compiler_t *c, sv_ast_t *init_var, int **slots, int *count, int *cap) { if (!init_var || init_var->type != N_VAR) return; for (int i = 0; i < init_var->args.count; i++) { sv_ast_t *decl = init_var->args.items[i]; @@ -3377,7 +3396,7 @@ void compile_for(sv_compiler_t *c, sv_ast_t *node) { int exit_jump = -1; if (node->cond) { - compile_expr(c, node->cond); + compile_truthy_test_expr(c, node->cond); exit_jump = emit_jump(c, OP_JMP_FALSE); } diff --git a/src/silver/engine.c b/src/silver/engine.c index b6e87a9..0f5faa9 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -4,6 +4,7 @@ #include "silver/engine.h" #include "silver/swarm.h" +#include "modules/regex.h" #include "ops/literals.h" #include "ops/stack.h" @@ -1419,6 +1420,26 @@ ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant NEXT(3); } + L_RE_EXEC_TRUTHY: { + ant_value_t call_arg = vm->stack[vm->sp - 1]; + ant_value_t call_func = vm->stack[vm->sp - 2]; + ant_value_t call_this = vm->stack[vm->sp - 3]; + ant_value_t call_result; + + if (!regexp_exec_truthy_try_fast(js, call_func, call_this, call_arg, &call_result)) { + ant_value_t call_args[1] = { call_arg }; frame->ip = ip; + ant_value_t raw_result = sv_vm_call(vm, js, call_func, call_this, call_args, 1, NULL, false); + sv_sync_frame_locals(vm, &frame, &func, &bp, &lp); + if (is_err(raw_result)) call_result = raw_result; + else call_result = mkval(T_BOOL, js_truthy(js, raw_result) ? 1 : 0); + } + + vm->sp -= 3; + if (is_err(call_result)) { sv_err = call_result; goto sv_throw; } + vm->stack[vm->sp++] = call_result; + NEXT(1); + } + L_TAIL_CALL: { uint16_t call_argc = sv_get_u16(ip + 1); ant_value_t call_func = vm->stack[vm->sp - call_argc - 1]; diff --git a/src/silver/swarm.c b/src/silver/swarm.c index f7464c9..7177147 100644 --- a/src/silver/swarm.c +++ b/src/silver/swarm.c @@ -2015,6 +2015,9 @@ static bool jit_is_eligible(sv_func_t *func) { if (vtype(cv) != T_CFUNC) return false; break; } + case OP_RE_EXEC_TRUTHY: + eligible = false; + break; default: if (sv_jit_warn_unlikely) fprintf(stderr, "jit: ineligible op %s in %s\n", diff --git a/src/utils.c b/src/utils.c index 8e173b1..c6ee8a2 100644 --- a/src/utils.c +++ b/src/utils.c @@ -191,7 +191,9 @@ static bool is_entrypoint_script_extension(const char *ext) { } static bool has_js_extension(const char *filename) { - const char *dot = strrchr(filename, '.'); + const char *slash = strrchr(filename, '/'); + const char *base = slash ? slash + 1 : filename; + const char *dot = strrchr(base, '.'); if (!dot) return false; for (const char *const *ext = module_resolve_extensions; *ext; ext++) { if (!is_entrypoint_script_extension(*ext)) continue; @@ -208,7 +210,9 @@ char *resolve_js_file(const char *filename) { struct stat st; if (stat(filename, &st) == 0) { if (S_ISREG(st.st_mode)) { - const char *dot = strrchr(filename, '.'); + const char *slash = strrchr(filename, '/'); + const char *base = slash ? slash + 1 : filename; + const char *dot = strrchr(base, '.'); if (dot && !has_js_extension(filename)) return NULL; return strdup(filename); } diff --git a/tests/test_extensionless_hidden_entrypoint.cjs b/tests/test_extensionless_hidden_entrypoint.cjs new file mode 100644 index 0000000..d77685a --- /dev/null +++ b/tests/test_extensionless_hidden_entrypoint.cjs @@ -0,0 +1,53 @@ +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ant-hidden-entry-')); +const hiddenDir = path.join(tmpRoot, '.bin'); +const scriptPath = path.join(hiddenDir, 'kat2'); + +fs.mkdirSync(hiddenDir, { recursive: true }); +fs.writeFileSync( + scriptPath, + [ + '#!/usr/bin/env ant', + '', + 'console.log("hidden entry ok");', + '', + ].join('\n') +); +fs.chmodSync(scriptPath, 0o755); + +const env = { ...process.env }; +env.PATH = `${path.dirname(process.execPath)}${path.delimiter}${env.PATH || ''}`; + +const direct = spawnSync(scriptPath, [], { env }); +if (direct.error) throw direct.error; + +assert( + direct.status === 0, + `extensionless shebang entrypoint in hidden dir should exit 0, got ${direct.status}\nstdout:\n${String(direct.stdout)}\nstderr:\n${String(direct.stderr)}` +); +assert( + String(direct.stdout) === 'hidden entry ok\n', + `expected shebang stdout to be hidden entry ok, got ${JSON.stringify(String(direct.stdout))}` +); + +const viaAnt = spawnSync(process.execPath, [scriptPath]); +if (viaAnt.error) throw viaAnt.error; + +assert( + viaAnt.status === 0, + `direct ant entrypoint in hidden dir should exit 0, got ${viaAnt.status}\nstdout:\n${String(viaAnt.stdout)}\nstderr:\n${String(viaAnt.stderr)}` +); +assert( + String(viaAnt.stdout) === 'hidden entry ok\n', + `expected ant stdout to be hidden entry ok, got ${JSON.stringify(String(viaAnt.stdout))}` +); + +console.log('extensionless hidden entrypoint test passed'); diff --git a/tests/test_regexp_exec_fast_paths.cjs b/tests/test_regexp_exec_fast_paths.cjs new file mode 100644 index 0000000..1c1e452 --- /dev/null +++ b/tests/test_regexp_exec_fast_paths.cjs @@ -0,0 +1,62 @@ +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +const routeRe = + /^\/api\/v(?[0-9]+)\/users\/(?[0-9]+)\/posts\/(?[0-9]+)(?:\?(?.*))?$/; +const routeMatch = routeRe.exec('/api/v3/users/42/posts/9?limit=10'); + +assert(routeMatch !== null, 'expected route regexp to match'); +assert(routeMatch[0] === '/api/v3/users/42/posts/9?limit=10', 'full match mismatch'); + +const groups1 = routeMatch.groups; +const groups2 = routeMatch.groups; +assert(groups1 === groups2, 'groups getter should cache the created object'); +assert(groups1.version === '3', 'named group version mismatch'); +assert(groups1.user === '42', 'named group user mismatch'); +assert(groups1.post === '9', 'named group post mismatch'); +assert(groups1.query === 'limit=10', 'named group query mismatch'); + +const wordRe = /\b[a-z]+\b/g; +const words = 'alpha beta gamma'; +let count = 0; +let lastMatch = ''; + +while (wordRe.exec(words)) { + count++; + lastMatch = RegExp.lastMatch; +} + +assert(count === 3, 'truthy exec loop should count all matches'); +assert(lastMatch === 'gamma', 'RegExp.lastMatch should track the final successful exec'); +assert(wordRe.lastIndex === 0, 'global exec loop should reset lastIndex after the final miss'); + +const order = []; +let customCalls = 0; +const customExec = { + get exec() { + order.push('get'); + return function (value) { + order.push('call:' + value); + return customCalls++ === 0 ? { ok: true } : null; + }; + } +}; + +function nextCustomArg() { + order.push('arg'); + return 'payload'; +} + +let customCount = 0; +while (customExec.exec(nextCustomArg())) { + customCount++; +} + +assert(customCount === 1, 'custom exec truthiness loop should still use fallback call semantics'); +assert( + order.join(',') === 'get,arg,call:payload,get,arg,call:payload', + 'custom exec truthiness lowering should preserve getter/arg/call order' +); + +console.log('regex exec fast path semantics ok');