From 899dad6b8a9ebdcc8a138cab03780be0a8df4639 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Fri, 1 May 2026 19:38:36 -0700 Subject: [PATCH] inherit type hints from typescript --- include/oxc.h | 12 ++ include/silver/engine.h | 38 ++++ include/utils.h | 3 + src/silver/compiler.c | 91 +++++++++ src/silver/engine.c | 4 + src/silver/swarm.c | 175 +++++++++++++++++- src/strip/Cargo.lock | 1 + src/strip/Cargo.toml | 1 + src/strip/src/ffi.rs | 100 +++++++++- src/strip/src/lib.rs | 2 +- src/strip/src/strip.rs | 117 +++++++++++- src/utils.c | 46 ++++- tests/bench_typescript_type_hints_compare.cjs | 44 +++++ tests/bench_typescript_type_hints_compare.js | 44 +++++ tests/bench_typescript_type_hints_compare.ts | 44 +++++ tests/fixtures/type_hints_compare.js | 27 +++ tests/fixtures/type_hints_compare.ts | 27 +++ tests/test_typescript_type_hint_compare.cjs | 40 ++++ tests/test_typescript_type_hints.cjs | 55 ++++++ 19 files changed, 858 insertions(+), 13 deletions(-) create mode 100644 tests/bench_typescript_type_hints_compare.cjs create mode 100644 tests/bench_typescript_type_hints_compare.js create mode 100644 tests/bench_typescript_type_hints_compare.ts create mode 100644 tests/fixtures/type_hints_compare.js create mode 100644 tests/fixtures/type_hints_compare.ts create mode 100644 tests/test_typescript_type_hint_compare.cjs create mode 100644 tests/test_typescript_type_hints.cjs diff --git a/include/oxc.h b/include/oxc.h index 48acd98..f010b99 100644 --- a/include/oxc.h +++ b/include/oxc.h @@ -19,4 +19,16 @@ char *OXC_strip_types_owned( size_t error_output_len ); +char *OXC_strip_types_with_hints_owned( + const char *input, + const char *filename, + int is_module, + size_t *out_len, + int *out_error, + char **out_hints, + size_t *out_hints_len, + char *error_output, + size_t error_output_len +); + #endif diff --git a/include/silver/engine.h b/include/silver/engine.h index 208ab52..c0b6c5a 100644 --- a/include/silver/engine.h +++ b/include/silver/engine.h @@ -144,6 +144,9 @@ struct sv_func { sv_type_info_t *local_types; int local_type_count; + sv_type_info_t *param_hints; + int param_hint_count; + uint8_t return_hint; int param_count; int upvalue_count; @@ -182,6 +185,7 @@ struct sv_func { uint32_t jit_compiled_tfb_ver; uint8_t *type_feedback; uint8_t *local_type_feedback; + uint8_t *param_type_feedback; uint64_t ctor_prop_samples; uint64_t ctor_prop_hist[17]; uint8_t ctor_inobj_limit; @@ -918,11 +922,45 @@ if (func->type_feedback) { if (neu != old) { func->type_feedback[off] = neu; func->tfb_version++; } }} +static inline void sv_tfb_seed_param_hints(sv_func_t *fn) { + if (!fn->param_type_feedback || !fn->param_hints) return; + int n = fn->param_hint_count < fn->param_count + ? fn->param_hint_count : fn->param_count; + for (int i = 0; i < n; i++) { + if (fn->param_hints[i].type == SV_TI_NUM) + fn->param_type_feedback[i] |= SV_TFB_NUM; + } +} + static inline void sv_tfb_ensure(sv_func_t *fn) { if (!fn->type_feedback && fn->code_len > 0) fn->type_feedback = calloc((size_t)fn->code_len, 1); if (!fn->local_type_feedback && fn->max_locals > 0) fn->local_type_feedback = calloc((size_t)fn->max_locals, 1); + if (!fn->param_type_feedback && fn->param_count > 0) { + fn->param_type_feedback = calloc((size_t)fn->param_count, 1); + sv_tfb_seed_param_hints(fn); + } +} + +static inline void sv_tfb_record_param(sv_func_t *func, int idx, ant_value_t v) { + if (func->param_type_feedback && idx >= 0 && idx < func->param_count) { + uint8_t old = func->param_type_feedback[idx]; + uint8_t neu = old | sv_tfb_classify(v); + if (neu != old) { func->param_type_feedback[idx] = neu; func->tfb_version++; } + } +} + +static inline bool sv_tfb_param_numeric_hint(const sv_func_t *func, int idx) { + if (!func || idx < 0 || idx >= func->param_count) return false; + uint8_t fb = func->param_type_feedback ? func->param_type_feedback[idx] : 0; + if (fb && (fb & ~SV_TFB_NUM)) return false; + if (fb == SV_TFB_NUM) return true; + return ( + func->param_hints && + idx < func->param_hint_count && + func->param_hints[idx].type == SV_TI_NUM + ); } static inline void sv_tfb_record_call_target(sv_func_t *func, int bc_off, sv_func_t *callee) { diff --git a/include/utils.h b/include/utils.h index 17a6085..ba45806 100644 --- a/include/utils.h +++ b/include/utils.h @@ -41,6 +41,9 @@ int strip_typescript_inplace( const char **error_detail ); +void ant_ts_hints_store(const char *filename, const char *hints); +const char *ant_ts_hints_find(const char *filename); + void *try_oom(size_t size); void cstr_free(cstr_buf_t *buf); diff --git a/src/silver/compiler.c b/src/silver/compiler.c index 7afd9a7..f431115 100644 --- a/src/silver/compiler.c +++ b/src/silver/compiler.c @@ -9,6 +9,7 @@ #include "tokens.h" #include "runtime.h" #include "ops/coercion.h" +#include "utils.h" #include #include @@ -659,6 +660,80 @@ static uint8_t iter_hint_for_type(uint8_t type) { } } +static uint8_t ts_hint_char_to_type(char hint) { + switch (hint) { + case 'N': return SV_TI_NUM; + case 'S': return SV_TI_STR; + case 'B': return SV_TI_BOOL; + case 'A': return SV_TI_ARR; + case 'O': return SV_TI_OBJ; + case 'V': return SV_TI_UNDEF; + case '0': return SV_TI_NULL; + default: return SV_TI_UNKNOWN; + } +} + +static const char *ts_hint_find_field( + const char *line, const char *line_end, + const char *field, size_t field_len, + const char **out_end +) { + const char *p = line; + while (p < line_end) { + const char *next = memchr(p, '|', (size_t)(line_end - p)); + if (!next) next = line_end; + if ((size_t)(next - p) >= field_len && memcmp(p, field, field_len) == 0) { + const char *value = p + field_len; + if (out_end) *out_end = next; + return value; + } + p = next < line_end ? next + 1 : line_end; + } + return NULL; +} + +static bool lookup_ts_function_hints( + const char *filename, + const char *name, + uint32_t name_len, + int param_count, + sv_type_info_t *out_params, + uint8_t *out_return +) { + const char *hints = ant_ts_hints_find(filename); + if (!hints || !name || name_len == 0) return false; + + bool found = false; + for (const char *line = hints; *line;) { + const char *line_end = strchr(line, '\n'); + if (!line_end) line_end = line + strlen(line); + + const char *fn_end = NULL; + const char *fn = ts_hint_find_field(line, line_end, "fn:", 3, &fn_end); + if (fn && (uint32_t)(fn_end - fn) == name_len && memcmp(fn, name, name_len) == 0) { + const char *params_end = NULL; + const char *params = ts_hint_find_field(line, line_end, "p:", 2, ¶ms_end); + const char *ret_end = NULL; + const char *ret = ts_hint_find_field(line, line_end, "r:", 2, &ret_end); + + if (out_params && params) { + int n = (int)(params_end - params); + if (n > param_count) n = param_count; + for (int i = 0; i < n; i++) + out_params[i].type = ts_hint_char_to_type(params[i]); + } + if (out_return && ret && ret < ret_end) + *out_return = ts_hint_char_to_type(*ret); + found = true; + break; + } + + line = *line_end == '\n' ? line_end + 1 : line_end; + } + + return found; +} + static int ensure_local_at_depth( sv_compiler_t *c, const char *name, uint32_t len, bool is_const, int depth @@ -5275,6 +5350,22 @@ sv_func_t *compile_function_body( memcpy(func->local_types, comp.slot_types, (size_t)ncopy * sizeof(sv_type_info_t)); } } + if (comp.param_count > 0 && node->str && node->len > 0) { + sv_type_info_t *param_hints = code_arena_bump((size_t)comp.param_count * sizeof(sv_type_info_t)); + if (param_hints) { + memset(param_hints, 0, (size_t)comp.param_count * sizeof(sv_type_info_t)); + uint8_t return_hint = SV_TI_UNKNOWN; + if (lookup_ts_function_hints( + enclosing->filename ? enclosing->filename : enclosing->js->filename, + node->str, node->len, + comp.param_count, param_hints, &return_hint + )) { + func->param_hints = param_hints; + func->param_hint_count = comp.param_count; + func->return_hint = return_hint; + } + } + } func->param_count = comp.param_count; func->is_strict = comp.is_strict; func->is_arrow = comp.is_arrow; diff --git a/src/silver/engine.c b/src/silver/engine.c index 8a3f56d..520c5e3 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -510,6 +510,10 @@ static inline ant_value_t sv_stage_frame_args( memmove(base, args, (size_t)argc * sizeof(ant_value_t)); for (int i = argc; i < arg_slots; i++) base[i] = js_mkundef(); + #ifdef ANT_JIT + for (int i = 0; i < func->param_count; i++) + sv_tfb_record_param(func, i, base[i]); + #endif if (func->max_locals > 0) for (int i = 0; i < func->max_locals; i++) (*out_lp)[i] = js_mkundef(); diff --git a/src/silver/swarm.c b/src/silver/swarm.c index d5c05bb..6f177b7 100644 --- a/src/silver/swarm.c +++ b/src/silver/swarm.c @@ -318,6 +318,43 @@ static void mir_emit_bailout_check(MIR_context_t ctx, MIR_item_t fn, MIR_append_insn(ctx, fn, no_bail); } +static void mir_emit_bailout_jump(MIR_context_t ctx, MIR_item_t fn, + MIR_reg_t r_bailout_off, int bc_off, + MIR_reg_t r_bailout_sp, int pre_op_sp, + MIR_label_t bailout_tramp, + MIR_reg_t r_args_buf, + jit_vstack_t *vs, + MIR_reg_t *local_regs, int n_locals, + MIR_reg_t r_lbuf, + MIR_reg_t r_d_slot) { + for (int i = 0; i < pre_op_sp; i++) { + if (vs->slot_type && vs->slot_type[i] == SLOT_NUM) + mir_d_to_i64(ctx, fn, vs->regs[i], vs->d_regs[i], r_d_slot); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_mem_op(ctx, MIR_T_I64, + (MIR_disp_t)(i * (int)sizeof(ant_value_t)), r_args_buf, 0, 1), + MIR_new_reg_op(ctx, vs->regs[i]))); + } + for (int i = 0; i < n_locals; i++) + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_mem_op(ctx, MIR_T_I64, + (MIR_disp_t)(i * (int)sizeof(ant_value_t)), r_lbuf, 0, 1), + MIR_new_reg_op(ctx, local_regs[i]))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, r_bailout_off), + MIR_new_int_op(ctx, bc_off))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, r_bailout_sp), + MIR_new_int_op(ctx, pre_op_sp))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_JMP, + MIR_new_label_op(ctx, bailout_tramp))); +} + static void mir_load_imm(MIR_context_t ctx, MIR_item_t fn, MIR_reg_t dst, uint64_t imm) { @@ -1018,6 +1055,30 @@ static bool *scan_captured_params(sv_func_t *func) { return captured; } +static bool *scan_mutated_params(sv_func_t *func) { + int param_count = func ? func->param_count : 0; + if (param_count <= 0) return NULL; + bool *mutated = calloc((size_t)param_count, sizeof(bool)); + if (!mutated) return NULL; + + uint8_t *ip = func->code; + uint8_t *end = func->code + func->code_len; + while (ip < end) { + sv_op_t op = (sv_op_t)*ip; + int sz = sv_op_size[op]; + if (sz == 0) break; + + if (op == OP_PUT_ARG || op == OP_SET_ARG) { + uint16_t idx = sv_get_u16(ip + 1); + if (idx < (uint16_t)param_count) mutated[idx] = true; + } + + ip += sz; + } + + return mutated; +} + #define JIT_INLINE_MAX_BYTECODE 128 @@ -1896,6 +1957,14 @@ typedef struct { static jit_features_t jit_prescan_features(sv_func_t *func) { jit_features_t f = {0}; + if (func->param_count > 0) { + for (int i = 0; i < func->param_count; i++) { + if (sv_tfb_param_numeric_hint(func, i)) { + f.needs_bailout = true; + break; + } + } + } uint8_t *ip = func->code; uint8_t *end = func->code + func->code_len; while (ip < end) { @@ -2653,6 +2722,9 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos int param_count = func->param_count; bool *captured_params = scan_captured_params(func); bool *captured_locals = scan_captured_locals(func, n_locals); + bool *mutated_params = scan_mutated_params(func); + bool *entry_num_params = NULL; + MIR_reg_t *param_d_regs = NULL; bool has_captured_params = false; bool has_captures = false; if (captured_params) { @@ -2667,11 +2739,66 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos bool use_unified_slotbuf = has_captured_slots && has_captures; int slotbuf_count = use_unified_slotbuf ? (param_count + n_locals) : param_count; + if (param_count > 0) { + entry_num_params = calloc((size_t)param_count, sizeof(bool)); + param_d_regs = calloc((size_t)param_count, sizeof(MIR_reg_t)); + if (!entry_num_params || !param_d_regs) { + free(vs.regs); free(vs.known_func); free(vs.d_regs); free(vs.slot_type); + free(vs.known_const); free(vs.has_const); + free(local_regs); free(local_d_regs); free(known_func_locals); free(known_type_locals); + free(captured_params); free(captured_locals); free(mutated_params); + free(entry_num_params); free(param_d_regs); + MIR_finish_func(ctx); MIR_finish_module(ctx); func->jit_compiling = false; return NULL; + } + + for (int i = 0; i < param_count; i++) { + if (!func->param_hints || i >= func->param_hint_count || + func->param_hints[i].type != SV_TI_NUM) continue; + if (!sv_tfb_param_numeric_hint(func, i)) continue; + if (captured_params && captured_params[i]) continue; + if (mutated_params && mutated_params[i]) continue; + + char dname[32]; + snprintf(dname, sizeof(dname), "pd%d", i); + entry_num_params[i] = true; + param_d_regs[i] = MIR_new_func_reg(ctx, jit_func->u.func, MIR_T_D, dname); + + MIR_label_t in_range = MIR_new_label(ctx); + MIR_label_t is_num = MIR_new_label(ctx); + MIR_append_insn(ctx, jit_func, + MIR_new_insn(ctx, MIR_UBGT, + MIR_new_label_op(ctx, in_range), + MIR_new_reg_op(ctx, r_argc), + MIR_new_int_op(ctx, (int64_t)i))); + mir_load_imm(ctx, jit_func, r_bailout_val, (uint64_t)SV_JIT_BAILOUT); + MIR_append_insn(ctx, jit_func, + MIR_new_ret_insn(ctx, 1, MIR_new_reg_op(ctx, r_bailout_val))); + MIR_append_insn(ctx, jit_func, in_range); + MIR_append_insn(ctx, jit_func, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, r_tmp), + MIR_new_mem_op(ctx, MIR_JSVAL, + (MIR_disp_t)(i * (int)sizeof(ant_value_t)), + r_args, 0, 1))); + mir_emit_is_num_guard(ctx, jit_func, r_bool, r_tmp, is_num); + mir_i64_to_d(ctx, jit_func, param_d_regs[i], r_tmp, r_d_slot); + MIR_label_t guard_done = MIR_new_label(ctx); + MIR_append_insn(ctx, jit_func, + MIR_new_insn(ctx, MIR_JMP, MIR_new_label_op(ctx, guard_done))); + MIR_append_insn(ctx, jit_func, is_num); + mir_load_imm(ctx, jit_func, r_bailout_val, (uint64_t)SV_JIT_BAILOUT); + MIR_append_insn(ctx, jit_func, + MIR_new_ret_insn(ctx, 1, MIR_new_reg_op(ctx, r_bailout_val))); + MIR_append_insn(ctx, jit_func, guard_done); + } + } + if (has_captured_params && needs_bailout) { free(vs.regs); free(vs.known_func); free(vs.d_regs); free(vs.slot_type); free(vs.known_const); free(vs.has_const); free(local_regs); free(local_d_regs); free(known_func_locals); free(known_type_locals); - free(captured_params); free(captured_locals); + free(captured_params); free(captured_locals); free(mutated_params); + free(entry_num_params); free(param_d_regs); MIR_finish_func(ctx); MIR_finish_module(ctx); func->jit_compiling = false; return NULL; } @@ -2921,6 +3048,14 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos case OP_GET_ARG: { uint16_t idx = sv_get_u16(ip + 1); MIR_reg_t dst = vstack_push(&vs); + if (entry_num_params && idx < (uint16_t)param_count && entry_num_params[idx]) { + MIR_append_insn(ctx, jit_func, + MIR_new_insn(ctx, MIR_DMOV, + MIR_new_reg_op(ctx, vs.d_regs[vs.sp - 1]), + MIR_new_reg_op(ctx, param_d_regs[idx]))); + if (vs.slot_type) vs.slot_type[vs.sp - 1] = SLOT_NUM; + break; + } if (has_captured_params && captured_params && idx < (uint16_t)param_count && captured_params[idx]) { MIR_append_insn(ctx, jit_func, MIR_new_insn(ctx, MIR_MOV, @@ -2948,6 +3083,21 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos r_args, 0, 1))); MIR_append_insn(ctx, jit_func, arg_done); } + if (sv_tfb_param_numeric_hint(func, (int)idx)) { + MIR_label_t hint_bail = MIR_new_label(ctx); + MIR_label_t hint_done = MIR_new_label(ctx); + mir_emit_is_num_guard(ctx, jit_func, r_bool, dst, hint_bail); + mir_i64_to_d(ctx, jit_func, vs.d_regs[vs.sp - 1], dst, r_d_slot); + if (vs.slot_type) vs.slot_type[vs.sp - 1] = SLOT_NUM; + MIR_append_insn(ctx, jit_func, + MIR_new_insn(ctx, MIR_JMP, MIR_new_label_op(ctx, hint_done))); + MIR_append_insn(ctx, jit_func, hint_bail); + mir_emit_bailout_jump(ctx, jit_func, + r_bailout_off, bc_off, + r_bailout_sp, vs.sp - 1, bailout_tramp, + r_args_buf, &vs, local_regs, n_locals, r_lbuf, r_d_slot); + MIR_append_insn(ctx, jit_func, hint_done); + } break; } @@ -3356,11 +3506,12 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos case OP_ADD_NUM: { uint8_t fb = func->type_feedback ? func->type_feedback[bc_off] : 0; bool force_num_only = (op == OP_ADD_NUM); - bool fb_num_only = force_num_only || (fb && !(fb & ~SV_TFB_NUM)); - bool fb_never_num = !force_num_only && fb && !(fb & SV_TFB_NUM); bool l_is_num = vs.slot_type && vs.slot_type[vs.sp - 2] == SLOT_NUM; bool r_is_num = vs.slot_type && vs.slot_type[vs.sp - 1] == SLOT_NUM; + bool hinted_num_only = l_is_num && r_is_num; + bool fb_num_only = force_num_only || hinted_num_only || (fb && !(fb & ~SV_TFB_NUM)); + bool fb_never_num = !force_num_only && !hinted_num_only && fb && !(fb & SV_TFB_NUM); MIR_reg_t rr = vstack_pop(&vs); MIR_reg_t rl = vstack_pop(&vs); @@ -3546,11 +3697,12 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos case OP_SUB_NUM: { uint8_t fb = func->type_feedback ? func->type_feedback[bc_off] : 0; bool force_num_only = (op == OP_SUB_NUM); - bool fb_num_only = force_num_only || (fb && !(fb & ~SV_TFB_NUM)); - bool fb_never_num = !force_num_only && fb && !(fb & SV_TFB_NUM); bool l_is_num = vs.slot_type && vs.slot_type[vs.sp - 2] == SLOT_NUM; bool r_is_num = vs.slot_type && vs.slot_type[vs.sp - 1] == SLOT_NUM; + bool hinted_num_only = l_is_num && r_is_num; + bool fb_num_only = force_num_only || hinted_num_only || (fb && !(fb & ~SV_TFB_NUM)); + bool fb_never_num = !force_num_only && !hinted_num_only && fb && !(fb & SV_TFB_NUM); MIR_reg_t rr = vstack_pop(&vs); MIR_reg_t rl = vstack_pop(&vs); @@ -3719,11 +3871,12 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos case OP_MUL_NUM: { uint8_t fb = func->type_feedback ? func->type_feedback[bc_off] : 0; bool force_num_only = (op == OP_MUL_NUM); - bool fb_num_only = force_num_only || (fb && !(fb & ~SV_TFB_NUM)); - bool fb_never_num = !force_num_only && fb && !(fb & SV_TFB_NUM); bool l_is_num = vs.slot_type && vs.slot_type[vs.sp - 2] == SLOT_NUM; bool r_is_num = vs.slot_type && vs.slot_type[vs.sp - 1] == SLOT_NUM; + bool hinted_num_only = l_is_num && r_is_num; + bool fb_num_only = force_num_only || hinted_num_only || (fb && !(fb & ~SV_TFB_NUM)); + bool fb_never_num = !force_num_only && !hinted_num_only && fb && !(fb & SV_TFB_NUM); MIR_reg_t rr = vstack_pop(&vs); MIR_reg_t rl = vstack_pop(&vs); @@ -3892,11 +4045,12 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos case OP_DIV_NUM: { uint8_t fb = func->type_feedback ? func->type_feedback[bc_off] : 0; bool force_num_only = (op == OP_DIV_NUM); - bool fb_num_only = force_num_only || (fb && !(fb & ~SV_TFB_NUM)); - bool fb_never_num = !force_num_only && fb && !(fb & SV_TFB_NUM); bool l_is_num = vs.slot_type && vs.slot_type[vs.sp - 2] == SLOT_NUM; bool r_is_num = vs.slot_type && vs.slot_type[vs.sp - 1] == SLOT_NUM; + bool hinted_num_only = l_is_num && r_is_num; + bool fb_num_only = force_num_only || hinted_num_only || (fb && !(fb & ~SV_TFB_NUM)); + bool fb_never_num = !force_num_only && !hinted_num_only && fb && !(fb & SV_TFB_NUM); MIR_reg_t rr = vstack_pop(&vs); MIR_reg_t rl = vstack_pop(&vs); @@ -8213,6 +8367,9 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos free(known_type_locals); free(captured_params); free(captured_locals); + free(mutated_params); + free(entry_num_params); + free(param_d_regs); if (!ok) return NULL; diff --git a/src/strip/Cargo.lock b/src/strip/Cargo.lock index 3a8e2cb..5f42504 100644 --- a/src/strip/Cargo.lock +++ b/src/strip/Cargo.lock @@ -304,6 +304,7 @@ name = "oxc" version = "0.0.0" dependencies = [ "oxc_allocator", + "oxc_ast", "oxc_codegen", "oxc_parser", "oxc_semantic", diff --git a/src/strip/Cargo.toml b/src/strip/Cargo.toml index c74615e..02573cc 100644 --- a/src/strip/Cargo.toml +++ b/src/strip/Cargo.toml @@ -7,6 +7,7 @@ crate-type = ["staticlib"] [dependencies] oxc_allocator = "0.110.0" +oxc_ast = "0.110.0" oxc_span = "0.110.0" oxc_transformer = "0.110.0" oxc_semantic = "0.110.0" diff --git a/src/strip/src/ffi.rs b/src/strip/src/ffi.rs index 3a13def..03f6bf9 100644 --- a/src/strip/src/ffi.rs +++ b/src/strip/src/ffi.rs @@ -1,7 +1,7 @@ use std::ffi::{CStr, c_char, c_int}; use std::ptr; -use crate::strip::strip_types_internal; +use crate::strip::{strip_types_internal, strip_types_with_hints_internal}; pub const OXC_ERR_NULL_INPUT: c_int = -1; pub const OXC_ERR_INVALID_UTF8: c_int = -2; @@ -34,6 +34,20 @@ unsafe fn write_error(output: *mut c_char, output_len: usize, msg: &str) { } } +unsafe fn alloc_c_string(bytes: &[u8]) -> *mut c_char { + let alloc_len = bytes.len() + 1; + let out_ptr = unsafe { malloc(alloc_len) as *mut c_char }; + if out_ptr.is_null() { + return ptr::null_mut(); + } + + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr as *mut u8, bytes.len()); + *out_ptr.add(bytes.len()) = 0; + } + out_ptr +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn OXC_strip_types_owned( input: *const c_char, filename: *const c_char, is_module: c_int, out_len: *mut usize, out_error: *mut c_int, error_output: *mut c_char, error_output_len: usize, @@ -105,3 +119,87 @@ pub unsafe extern "C" fn OXC_strip_types_owned( } } } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn OXC_strip_types_with_hints_owned( + input: *const c_char, filename: *const c_char, is_module: c_int, out_len: *mut usize, out_error: *mut c_int, out_hints: *mut *mut c_char, out_hints_len: *mut usize, error_output: *mut c_char, error_output_len: usize, +) -> *mut c_char { + if !out_error.is_null() { + unsafe { *out_error = OXC_ERR_NULL_INPUT }; + } + + if !out_len.is_null() { + unsafe { *out_len = 0 }; + } + if !out_hints.is_null() { + unsafe { *out_hints = ptr::null_mut() }; + } + if !out_hints_len.is_null() { + unsafe { *out_hints_len = 0 }; + } + + if input.is_null() || filename.is_null() || out_len.is_null() { + unsafe { write_error(error_output, error_output_len, "null input/output passed") }; + return ptr::null_mut(); + } + + let filename_str = match unsafe { CStr::from_ptr(filename).to_str() } { + Ok(s) => s, + Err(_) => { + unsafe { write_error(error_output, error_output_len, "filename is not valid UTF-8") }; + if !out_error.is_null() { + unsafe { *out_error = OXC_ERR_INVALID_UTF8 }; + } + return ptr::null_mut(); + } + }; + + let input_str = match unsafe { CStr::from_ptr(input).to_str() } { + Ok(s) => s, + Err(_) => { + unsafe { write_error(error_output, error_output_len, "source input is not valid UTF-8") }; + if !out_error.is_null() { + unsafe { *out_error = OXC_ERR_INVALID_UTF8 }; + } + return ptr::null_mut(); + } + }; + + match strip_types_with_hints_internal(input_str, filename_str, is_module != 0) { + Ok(result) => { + let code_bytes = result.code.as_bytes(); + let out_ptr = unsafe { alloc_c_string(code_bytes) }; + if out_ptr.is_null() { + unsafe { write_error(error_output, error_output_len, "out of memory allocating strip output") }; + if !out_error.is_null() { + unsafe { *out_error = OXC_ERR_OUTPUT_TOO_LARGE }; + } + return ptr::null_mut(); + } + + unsafe { *out_len = code_bytes.len() }; + if !result.hints.is_empty() && !out_hints.is_null() && !out_hints_len.is_null() { + let hint_bytes = result.hints.as_bytes(); + let hints_ptr = unsafe { alloc_c_string(hint_bytes) }; + if !hints_ptr.is_null() { + unsafe { + *out_hints = hints_ptr; + *out_hints_len = hint_bytes.len(); + } + } + } + + if !out_error.is_null() { + unsafe { *out_error = 0 }; + } + out_ptr + } + Err(err) => { + unsafe { write_error(error_output, error_output_len, &err) }; + if !out_error.is_null() { + unsafe { *out_error = classify_strip_error(&err) }; + } + ptr::null_mut() + } + } +} diff --git a/src/strip/src/lib.rs b/src/strip/src/lib.rs index 8296843..127fbd1 100644 --- a/src/strip/src/lib.rs +++ b/src/strip/src/lib.rs @@ -2,4 +2,4 @@ mod ffi; mod strip; pub use ffi::*; -pub use strip::strip_types_internal; +pub use strip::{strip_types_internal, strip_types_with_hints_internal}; diff --git a/src/strip/src/strip.rs b/src/strip/src/strip.rs index 69beb92..42f141e 100644 --- a/src/strip/src/strip.rs +++ b/src/strip/src/strip.rs @@ -1,13 +1,127 @@ use std::path::Path; use oxc_allocator::Allocator; +use oxc_ast::ast::{ + BindingPattern, Declaration, ExportDefaultDeclarationKind, FormalParameter, Function, Program, + Statement, TSType, TSTypeAnnotation, +}; use oxc_codegen::Codegen; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; use oxc_transformer::{TransformOptions, Transformer, TypeScriptOptions}; +pub struct StripTypesResult { + pub code: String, + pub hints: String, +} + +fn type_hint_from_type(ts_type: &TSType<'_>) -> char { + match ts_type { + TSType::TSNumberKeyword(_) => 'N', + TSType::TSStringKeyword(_) => 'S', + TSType::TSBooleanKeyword(_) => 'B', + TSType::TSArrayType(_) | TSType::TSTupleType(_) => 'A', + TSType::TSObjectKeyword(_) | TSType::TSTypeLiteral(_) => 'O', + TSType::TSUndefinedKeyword(_) | TSType::TSVoidKeyword(_) => 'V', + TSType::TSNullKeyword(_) => '0', + TSType::TSParenthesizedType(parenthesized) => type_hint_from_type(&parenthesized.type_annotation), + _ => 'U', + } +} + +fn type_hint_from_annotation(annotation: Option<&TSTypeAnnotation<'_>>) -> char { + annotation.map_or('U', |annotation| type_hint_from_type(&annotation.type_annotation)) +} + +fn binding_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> { + match pattern { + BindingPattern::BindingIdentifier(ident) => Some(ident.name.as_str()), + BindingPattern::AssignmentPattern(assign) => binding_name(&assign.left), + _ => None, + } +} + +fn param_hint(param: &FormalParameter<'_>) -> char { + if binding_name(¶m.pattern).is_none() { + return 'U'; + } + type_hint_from_annotation(param.type_annotation.as_deref()) +} + +fn collect_function_hint(func: &Function<'_>, out: &mut String) { + let Some(id) = &func.id else { + return; + }; + if func.body.is_none() { + return; + } + + let mut params = String::new(); + let mut has_hint = false; + for param in &func.params.items { + let hint = param_hint(param); + if hint != 'U' { + has_hint = true; + } + params.push(hint); + } + + if let Some(rest) = &func.params.rest { + let hint = type_hint_from_annotation(rest.type_annotation.as_deref()); + if hint != 'U' { + has_hint = true; + } + params.push(hint); + } + + let ret = type_hint_from_annotation(func.return_type.as_deref()); + if ret != 'U' { + has_hint = true; + } + if !has_hint { + return; + } + + out.push_str("fn:"); + out.push_str(id.name.as_str()); + out.push_str("|p:"); + out.push_str(¶ms); + out.push_str("|r:"); + out.push(ret); + out.push('\n'); +} + +fn collect_statement_hints(stmt: &Statement<'_>, out: &mut String) { + match stmt { + Statement::FunctionDeclaration(func) => collect_function_hint(func, out), + Statement::ExportNamedDeclaration(export) => { + if let Some(Declaration::FunctionDeclaration(func)) = &export.declaration { + collect_function_hint(func, out); + } + } + Statement::ExportDefaultDeclaration(export) => { + if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = &export.declaration { + collect_function_hint(func, out); + } + } + _ => {} + } +} + +fn collect_type_hints(program: &Program<'_>) -> String { + let mut hints = String::new(); + for stmt in &program.body { + collect_statement_hints(stmt, &mut hints); + } + hints +} + pub fn strip_types_internal(source: &str, filename: &str, is_module: bool) -> Result { + strip_types_with_hints_internal(source, filename, is_module).map(|result| result.code) +} + +pub fn strip_types_with_hints_internal(source: &str, filename: &str, is_module: bool) -> Result { let allocator = Allocator::default(); let source_type = SourceType::from_path(filename).unwrap_or_else(|_| SourceType::ts()).with_module(is_module); let parser_ret = Parser::new(&allocator, source, source_type).parse(); @@ -18,6 +132,7 @@ pub fn strip_types_internal(source: &str, filename: &str, is_module: bool) -> Re } let mut program = parser_ret.program; + let hints = collect_type_hints(&program); let semantic_ret = SemanticBuilder::new().build(&program); if !semantic_ret.errors.is_empty() { @@ -44,5 +159,5 @@ pub fn strip_types_internal(source: &str, filename: &str, is_module: bool) -> Re } let output = Codegen::new().build(&program).code; - Ok(output) + Ok(StripTypesResult { code: output, hints }) } diff --git a/src/utils.c b/src/utils.c index 92287dd..a213a0b 100644 --- a/src/utils.c +++ b/src/utils.c @@ -21,6 +21,45 @@ const char *const module_resolve_extensions[] = { ".json", ".node", NULL }; +typedef struct ant_ts_hints_entry { + char *filename; + char *hints; + struct ant_ts_hints_entry *next; +} ant_ts_hints_entry_t; + +static ant_ts_hints_entry_t *ant_ts_hints_head = NULL; + +void ant_ts_hints_store(const char *filename, const char *hints) { + if (!filename || !filename[0]) return; + + for (ant_ts_hints_entry_t *entry = ant_ts_hints_head; entry; entry = entry->next) { + if (strcmp(entry->filename, filename) != 0) continue; + free(entry->hints); + entry->hints = (hints && hints[0]) ? strdup(hints) : NULL; + return; + } + + ant_ts_hints_entry_t *entry = calloc(1, sizeof(*entry)); + if (!entry) return; + entry->filename = strdup(filename); + entry->hints = (hints && hints[0]) ? strdup(hints) : NULL; + if (!entry->filename) { + free(entry->hints); + free(entry); + return; + } + entry->next = ant_ts_hints_head; + ant_ts_hints_head = entry; +} + +const char *ant_ts_hints_find(const char *filename) { + if (!filename || !filename[0]) return NULL; + for (ant_ts_hints_entry_t *entry = ant_ts_hints_head; entry; entry = entry->next) { + if (strcmp(entry->filename, filename) == 0) return entry->hints; + } + return NULL; +} + static const char *ant_home_dir(void) { #ifdef _WIN32 const char *home = getenv("USERPROFILE"); @@ -283,11 +322,14 @@ int strip_typescript_inplace( char *input = *buffer; char error_buf[256] = {0}; size_t stripped_len = 0; + char *hints = NULL; + size_t hints_len = 0; int strip_error = OXC_ERR_TRANSFORM_FAILED; - char *stripped = OXC_strip_types_owned( + char *stripped = OXC_strip_types_with_hints_owned( input, filename, is_module, &stripped_len, &strip_error, + &hints, &hints_len, error_buf, sizeof(error_buf) ); @@ -315,6 +357,8 @@ int strip_typescript_inplace( memcpy(next, stripped, stripped_len + 1); free(stripped); + ant_ts_hints_store(filename, (hints && hints_len > 0) ? hints : NULL); + free(hints); *buffer = next; if (out_len) *out_len = stripped_len; diff --git a/tests/bench_typescript_type_hints_compare.cjs b/tests/bench_typescript_type_hints_compare.cjs new file mode 100644 index 0000000..9127382 --- /dev/null +++ b/tests/bench_typescript_type_hints_compare.cjs @@ -0,0 +1,44 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function run(file, rounds) { + const result = spawnSync(process.execPath, [file, String(rounds)], { + encoding: 'utf8', + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + `${path.basename(file)} exited ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}` + ); + } + + const out = Object.create(null); + for (const line of result.stdout.trim().split(/\n/)) { + const eq = line.indexOf('='); + if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1); + } + return { + ms: Number(out.best_ms), + checksum: out.checksum, + stdout: result.stdout, + }; +} + +const rounds = Number(process.argv[2]) > 0 ? Number(process.argv[2]) | 0 : 5000000; +const dir = __dirname; +const ts = run(path.join(dir, 'bench_typescript_type_hints_compare.ts'), rounds); +const js = run(path.join(dir, 'bench_typescript_type_hints_compare.js'), rounds); + +if (ts.checksum !== js.checksum) { + throw new Error(`checksum mismatch: ts=${ts.checksum} js=${js.checksum}`); +} + +const speedup = js.ms / ts.ms; +const delta = js.ms - ts.ms; + +console.log(`rounds=${rounds}`); +console.log(`ts_best_ms=${ts.ms.toFixed(3)}`); +console.log(`js_best_ms=${js.ms.toFixed(3)}`); +console.log(`speedup=${speedup.toFixed(2)}x`); +console.log(`delta_ms=${delta.toFixed(3)}`); +console.log(`checksum=${ts.checksum}`); diff --git a/tests/bench_typescript_type_hints_compare.js b/tests/bench_typescript_type_hints_compare.js new file mode 100644 index 0000000..c227525 --- /dev/null +++ b/tests/bench_typescript_type_hints_compare.js @@ -0,0 +1,44 @@ +const now = () => (typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()); + +function readRounds() { + const raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw | 0 : 5000000; +} + +function hot( + a, b, c, d, + e, f, g, h +) { + return ( + a + b + c + d + e + f + g + h + + a + c + e + g + b + d + f + h + + a + d + g + b + e + h + c + f + + h + g + f + e + d + c + b + a + ); +} + +function run(rounds) { + let total = 0; + for (let i = 0; i < rounds; i++) { + const n = i & 1023; + total += hot(n, n + 1, n + 2, n + 3, n + 4, n + 5, n + 6, n + 7); + } + return total; +} + +const rounds = readRounds(); +run(2000); + +let best = Infinity; +let checksum = 0; +for (let i = 0; i < 5; i++) { + const t0 = now(); + checksum = run(rounds); + const elapsed = now() - t0; + if (elapsed < best) best = elapsed; +} + +console.log("kind=js"); +console.log("rounds=" + rounds); +console.log("best_ms=" + best.toFixed(3)); +console.log("checksum=" + checksum); diff --git a/tests/bench_typescript_type_hints_compare.ts b/tests/bench_typescript_type_hints_compare.ts new file mode 100644 index 0000000..602236f --- /dev/null +++ b/tests/bench_typescript_type_hints_compare.ts @@ -0,0 +1,44 @@ +const now = () => (typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()); + +function readRounds(): number { + const raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw | 0 : 5000000; +} + +function hot( + a: number, b: number, c: number, d: number, + e: number, f: number, g: number, h: number +): number { + return ( + a + b + c + d + e + f + g + h + + a + c + e + g + b + d + f + h + + a + d + g + b + e + h + c + f + + h + g + f + e + d + c + b + a + ); +} + +function run(rounds: number): number { + let total = 0; + for (let i = 0; i < rounds; i++) { + const n = i & 1023; + total += hot(n, n + 1, n + 2, n + 3, n + 4, n + 5, n + 6, n + 7); + } + return total; +} + +const rounds = readRounds(); +run(2000); + +let best = Infinity; +let checksum = 0; +for (let i = 0; i < 5; i++) { + const t0 = now(); + checksum = run(rounds); + const elapsed = now() - t0; + if (elapsed < best) best = elapsed; +} + +console.log("kind=ts"); +console.log("rounds=" + rounds); +console.log("best_ms=" + best.toFixed(3)); +console.log("checksum=" + checksum); diff --git a/tests/fixtures/type_hints_compare.js b/tests/fixtures/type_hints_compare.js new file mode 100644 index 0000000..76a712b --- /dev/null +++ b/tests/fixtures/type_hints_compare.js @@ -0,0 +1,27 @@ +function addStep(value, delta) { + return value + delta; +} + +function mix(a, b, c) { + return (a + b) * c - a / (b + 1); +} + +function readRounds() { + const raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw | 0 : 10000; +} + +const rounds = readRounds(); +let checksum = 0; + +for (let i = 0; i < 240; i++) { + checksum = addStep(checksum, mix(i, i + 1, 3)); +} + +for (let i = 0; i < rounds; i++) { + checksum = addStep(checksum, (i % 13) + (i % 7)); +} + +console.log("rounds=" + rounds); +console.log("checksum=" + checksum.toFixed(3)); +console.log("fallback=" + addStep("type", "script")); diff --git a/tests/fixtures/type_hints_compare.ts b/tests/fixtures/type_hints_compare.ts new file mode 100644 index 0000000..30a5630 --- /dev/null +++ b/tests/fixtures/type_hints_compare.ts @@ -0,0 +1,27 @@ +function addStep(value: number, delta: number): number { + return value + delta; +} + +function mix(a: number, b: number, c: number): number { + return (a + b) * c - a / (b + 1); +} + +function readRounds(): number { + const raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw | 0 : 10000; +} + +const rounds = readRounds(); +let checksum = 0; + +for (let i = 0; i < 240; i++) { + checksum = addStep(checksum, mix(i, i + 1, 3)); +} + +for (let i = 0; i < rounds; i++) { + checksum = addStep(checksum, (i % 13) + (i % 7)); +} + +console.log("rounds=" + rounds); +console.log("checksum=" + checksum.toFixed(3)); +console.log("fallback=" + addStep("type" as any, "script" as any)); diff --git a/tests/test_typescript_type_hint_compare.cjs b/tests/test_typescript_type_hint_compare.cjs new file mode 100644 index 0000000..43c639e --- /dev/null +++ b/tests/test_typescript_type_hint_compare.cjs @@ -0,0 +1,40 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function runFixture(file) { + const result = spawnSync(process.execPath, [file], { + encoding: 'utf8', + }); + + if (result.error) throw result.error; + + const stdout = result.stdout.replace(/\x1b\[[0-9;]*m/g, ''); + assert( + result.status === 0, + `expected ${path.basename(file)} to exit 0, got ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}` + ); + + return stdout; +} + +const fixtureDir = path.join(__dirname, 'fixtures'); +const tsPath = path.join(fixtureDir, 'type_hints_compare.ts'); +const jsPath = path.join(fixtureDir, 'type_hints_compare.js'); + +const tsOut = runFixture(tsPath); +const jsOut = runFixture(jsPath); + +assert( + tsOut === jsOut, + `expected TypeScript and JavaScript comparison fixtures to match\n.ts:\n${tsOut}\n.js:\n${jsOut}` +); +assert( + tsOut === 'rounds=10000\nchecksum=262549.128\nfallback=typescript\n', + `unexpected comparison output: ${JSON.stringify(tsOut)}` +); + +console.log('TypeScript type-hint fixture matches JavaScript fixture'); diff --git a/tests/test_typescript_type_hints.cjs b/tests/test_typescript_type_hints.cjs new file mode 100644 index 0000000..448e684 --- /dev/null +++ b/tests/test_typescript_type_hints.cjs @@ -0,0 +1,55 @@ +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-ts-type-hints-')); +const scriptPath = path.join(tmpRoot, 'entry.ts'); + +fs.writeFileSync( + scriptPath, + [ + 'function add(a: number, b: number): number {', + ' return a + b;', + '}', + '', + 'function observedMismatch(a: number, b: number): number {', + ' return a + b;', + '}', + '', + 'let total = 0;', + 'for (let i = 0; i < 160; i++) total += add(i, 1);', + 'console.log(total);', + 'console.log(add("x" as any, "y" as any));', + 'console.log(observedMismatch("pre" as any, "jit" as any));', + 'console.log(observedMismatch("type" as any, "feedback" as any));', + 'let mismatchTotal = 0;', + 'for (let i = 0; i < 160; i++) mismatchTotal += observedMismatch(i, 2);', + 'console.log(mismatchTotal);', + 'console.log(observedMismatch("post" as any, "warmup" as any));', + '', + ].join('\n') +); + +const result = spawnSync(process.execPath, [scriptPath], { + encoding: 'utf8', +}); + +if (result.error) throw result.error; + +const stdout = result.stdout.replace(/\x1b\[[0-9;]*m/g, ''); + +assert( + result.status === 0, + `expected TypeScript type-hint optimization to preserve runtime semantics, got ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}` +); +assert( + stdout === '12880\nxy\nprejit\ntypefeedback\n13040\npostwarmup\n', + `expected numeric warmup and string fallback output, got ${JSON.stringify(stdout)}` +); + +console.log('TypeScript type hints preserve guarded runtime semantics'); -- 2.51.2