diff --git a/include/ant.h b/include/ant.h index 9a8c97d..74414b6 100644 --- a/include/ant.h +++ b/include/ant.h @@ -97,6 +97,7 @@ const char *js_sym_key(ant_value_t sym); ant_value_t js_mksym_for(ant_t *, const char *key); ant_value_t js_symbol_to_string(ant_t *js, ant_value_t sym); ant_value_t js_get_sym(ant_t *, ant_value_t obj, ant_value_t sym); +ant_value_t js_get_symbol(ant_t *, ant_value_t obj, const char *key); ant_value_t js_get_sym_with_receiver(ant_t *, ant_value_t obj, ant_value_t sym, ant_value_t receiver); ant_value_t js_mkobj(ant_t *); @@ -116,6 +117,7 @@ ant_offset_t js_mkprop_fast_off(ant_t *js, ant_value_t obj, const char *key, siz void js_set(ant_t *, ant_value_t, const char *, ant_value_t); void js_set_sym(ant_t *, ant_value_t obj, ant_value_t sym, ant_value_t val); +void js_set_symbol(ant_t *, ant_value_t obj, const char *key, ant_value_t val); void js_saveval(ant_t *js, ant_offset_t off, ant_value_t v); void js_merge_obj(ant_t *, ant_value_t dst, ant_value_t src); void js_arr_push(ant_t *, ant_value_t arr, ant_value_t val); diff --git a/include/modules/generator.h b/include/modules/generator.h index b3e6ba3..1d176d9 100644 --- a/include/modules/generator.h +++ b/include/modules/generator.h @@ -5,6 +5,9 @@ #include "types.h" void init_generator_module(void); +void generator_mark_for_gc(ant_t *js, ant_value_t gen); + +bool generator_resume_pending_request(ant_t *js, coroutine_t *coro, ant_value_t result); coroutine_t *generator_get_coro_for_gc(ant_value_t gen); #endif diff --git a/include/modules/stream.h b/include/modules/stream.h index 41226b5..c2628c2 100644 --- a/include/modules/stream.h +++ b/include/modules/stream.h @@ -3,6 +3,20 @@ #include "types.h" +typedef void (*stream_finalize_fn)( + ant_t *js, + ant_value_t stream_obj, + void *state +); + +typedef struct { + bool writing; + bool pending_final; + bool final_started; + void *attached_state; + stream_finalize_fn attached_state_finalize; +} stream_private_state_t; + void stream_init_constructors(ant_t *js); ant_value_t stream_library(ant_t *js); @@ -17,8 +31,18 @@ ant_value_t stream_writable_prototype(ant_t *js); ant_value_t stream_construct_readable(ant_t *js, ant_value_t base_proto, ant_value_t options); ant_value_t stream_construct_writable(ant_t *js, ant_value_t base_proto, ant_value_t options); ant_value_t stream_readable_push(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding); +ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj); +ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj); +ant_value_t stream_readable_push_value(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding); +ant_value_t stream_readable_continue_flowing(ant_t *js, ant_value_t *args, int nargs); +ant_value_t stream_readable_begin_flowing(ant_t *js, ant_value_t stream_obj); +ant_value_t stream_writable_begin_end(ant_t *js, ant_value_t stream_obj, ant_value_t callback); void stream_init_readable_object(ant_t *js, ant_value_t obj, ant_value_t options); void stream_init_writable_object(ant_t *js, ant_value_t obj, ant_value_t options); +void *stream_get_attached_state(ant_value_t stream_obj); +void stream_clear_attached_state(ant_value_t stream_obj); +void stream_set_attached_state(ant_value_t stream_obj, void *state, stream_finalize_fn finalize); + #endif diff --git a/include/modules/timer.h b/include/modules/timer.h index 4de1850..a945ff5 100644 --- a/include/modules/timer.h +++ b/include/modules/timer.h @@ -14,6 +14,9 @@ void queue_promise_trigger(ant_t *js, ant_value_t promise); void queue_microtask(ant_t *js, ant_value_t callback); void queue_microtask_with_args(ant_t *js, ant_value_t callback, ant_value_t *args, int nargs); +void queue_next_tick(ant_t *js, ant_value_t callback); +void queue_next_tick_with_args(ant_t *js, ant_value_t callback, ant_value_t *args, int nargs); + bool js_maybe_drain_microtasks(ant_t *js); bool js_maybe_drain_microtasks_after_async_settle(ant_t *js); diff --git a/include/sugar.h b/include/sugar.h index 42f842f..946347c 100644 --- a/include/sugar.h +++ b/include/sugar.h @@ -68,6 +68,7 @@ typedef struct coroutine { bool mco_started; bool is_ready; bool did_suspend; + bool free_pending; } coroutine_t; typedef struct { @@ -91,6 +92,7 @@ extern uint32_t coros_this_tick; void enqueue_coroutine(coroutine_t *coro); void remove_coroutine(coroutine_t *coro); void free_coroutine(coroutine_t *coro); +void reap_retired_coroutines(void); ant_value_t start_async_in_coroutine(ant_t *js, const char *code, size_t code_len, ant_value_t closure_scope, ant_value_t *args, int nargs); ant_value_t resume_coroutine_wrapper(ant_t *js, ant_value_t *args, int nargs); diff --git a/src/ant.c b/src/ant.c index 9f1a512..eceefdd 100644 --- a/src/ant.c +++ b/src/ant.c @@ -10819,7 +10819,9 @@ void js_process_promise_handlers(ant_t *js, ant_value_t promise) { if (!h) continue; if (h->await_coro) { - settle_and_resume_coroutine(js, h->await_coro, val, state != 1); + coroutine_t *await_coro = h->await_coro; + h->await_coro = NULL; + settle_and_resume_coroutine(js, await_coro, val, state != 1); continue; } @@ -10906,7 +10908,6 @@ void js_resolve_promise(ant_t *js, ant_value_t p, ant_value_t val) { if (src_pd->state == 0) gc_root_pending_promise(js_obj_ptr(js_as_obj(val))); else queue_promise_trigger(js, val); - GC_ROOT_RESTORE(js, root_mark); return; @@ -10953,7 +10954,7 @@ void js_resolve_promise(ant_t *js, ant_value_t p, ant_value_t val) { void js_reject_promise(ant_t *js, ant_value_t p, ant_value_t val) { if (vtype(val) == T_ERR) { if (vdata(val) != 0) val = mkval(T_OBJ, vdata(val)); - else if (js->thrown_exists && is_object_type(js->thrown_value)) val = js->thrown_value; + else if (js->thrown_exists) val = js->thrown_value; else val = js_make_error_silent(js, JS_ERR_INTERNAL, "unknown error"); } @@ -11038,7 +11039,15 @@ static ant_value_t builtin_Promise(ant_t *js, ant_value_t *args, int nargs) { } ant_value_t exec_args[] = { res_fn, rej_fn }; - sv_vm_call(js->vm, js, executor, js_mkundef(), exec_args, 2, NULL, false); + ant_value_t exec_result = sv_vm_call(js->vm, js, executor, js_mkundef(), exec_args, 2, NULL, false); + + if (is_err(exec_result) || js->thrown_exists) { + ant_value_t reject_val = js->thrown_exists ? js->thrown_value : exec_result; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); + js_reject_promise(js, p, reject_val); + } GC_ROOT_RESTORE(js, root_mark); return p; @@ -13776,6 +13785,12 @@ void js_set_sym(ant_t *js, ant_value_t obj, ant_value_t sym, ant_value_t val) { } else mkprop(js, obj, sym, val, 0); } +void js_set_symbol(ant_t *js, ant_value_t obj, const char *key, ant_value_t val) { + ant_value_t sym = js_mksym_for(js, key); + if (is_err(sym)) return; + js_set_sym(js, obj, sym, val); +} + ant_value_t js_get_sym_with_receiver(ant_t *js, ant_value_t obj, ant_value_t sym, ant_value_t receiver) { if (vtype(sym) != T_SYMBOL) return js_mkundef(); ant_offset_t sym_off = (ant_offset_t)vdata(sym); @@ -13827,6 +13842,12 @@ ant_value_t js_get_sym(ant_t *js, ant_value_t obj, ant_value_t sym) { return js_get_sym_with_receiver(js, obj, sym, obj); } +ant_value_t js_get_symbol(ant_t *js, ant_value_t obj, const char *key) { + ant_value_t sym = js_mksym_for(js, key); + if (is_err(sym)) return sym; + return js_get_sym(js, obj, sym); +} + static bool js_try_get(ant_t *js, ant_value_t obj, const char *key, ant_value_t *out) { size_t key_len = strlen(key); diff --git a/src/gc/objects.c b/src/gc/objects.c index 0c5b451..fe820f2 100644 --- a/src/gc/objects.c +++ b/src/gc/objects.c @@ -235,6 +235,7 @@ static void gc_scan_obj(ant_t *js, ant_object_t *obj) { if (obj->type_tag == T_GENERATOR) { coroutine_t *coro = generator_get_coro_for_gc(js_obj_from_ptr(obj)); if (coro) gc_mark_coroutine(js, coro); + generator_mark_for_gc(js, js_obj_from_ptr(obj)); } if (obj->type_tag == T_MAP) { diff --git a/src/modules/child_process.c b/src/modules/child_process.c index 8f9d5ff..1cd1bd6 100644 --- a/src/modules/child_process.c +++ b/src/modules/child_process.c @@ -1256,6 +1256,78 @@ static ant_value_t exec_file_close_callback(ant_t *js, ant_value_t *args, int na return js_mkundef(); } +static ant_value_t exec_file_promisify_callback(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_object_type(state)) return js_mkundef(); + + ant_value_t settled = js_get_slot(state, SLOT_SETTLED); + if (vtype(settled) == T_BOOL && settled == js_true) return js_mkundef(); + js_set_slot(state, SLOT_SETTLED, js_true); + + ant_value_t promise = js_get_slot(state, SLOT_DATA); + if (vtype(promise) != T_PROMISE) return js_mkundef(); + + ant_value_t stdout_val = nargs > 1 ? args[1] : js_mkstr(js, "", 0); + ant_value_t stderr_val = nargs > 2 ? args[2] : js_mkstr(js, "", 0); + + if (nargs > 0 && !is_null(args[0]) && !is_undefined(args[0])) { + if (is_object_type(args[0])) { + js_set(js, args[0], "stdout", stdout_val); + js_set(js, args[0], "stderr", stderr_val); + } + js_reject_promise(js, promise, args[0]); + return js_mkundef(); + } + + ant_value_t result = js_mkobj(js); + js_set(js, result, "stdout", stdout_val); + js_set(js, result, "stderr", stderr_val); + js_resolve_promise(js, promise, result); + + return js_mkundef(); +} + +static ant_value_t exec_file_promisified_call(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t original = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_callable(original)) return js_mkerr(js, "execFile promisify target is not callable"); + + ant_value_t promise = js_mkpromise(js); + ant_value_t state = js_mkobj(js); + + js_set_slot(state, SLOT_DATA, promise); + js_set_slot(state, SLOT_SETTLED, js_false); + + ant_value_t callback = js_heavy_mkfun(js, exec_file_promisify_callback, state); + ant_value_t *call_args = malloc((size_t)(nargs + 1) * sizeof(ant_value_t)); + + if (!call_args) { + js_reject_promise(js, promise, js_mkerr(js, "Out of memory")); + return promise; + } + + for (int i = 0; i < nargs; i++) call_args[i] = args[i]; + call_args[nargs] = callback; + + ant_value_t call_result = sv_vm_call( + js->vm, js, original, js_getthis(js), + call_args, nargs + 1, NULL, false + ); free(call_args); + + ant_value_t settled = js_get_slot(state, SLOT_SETTLED); + bool is_settled = (vtype(settled) == T_BOOL && settled == js_true); + + if (!is_settled && (is_err(call_result) || js->thrown_exists)) { + ant_value_t ex = js->thrown_exists ? js->thrown_value : call_result; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); + js_set_slot(state, SLOT_SETTLED, js_true); + js_reject_promise(js, promise, ex); + } + + return promise; +} + static ant_value_t builtin_execFile(ant_t *js, ant_value_t *args, int nargs) { ant_value_t argv = js_mkundef(); ant_value_t options = js_mkundef(); @@ -1739,12 +1811,19 @@ static ant_value_t builtin_fork(ant_t *js, ant_value_t *args, int nargs) { ant_value_t child_process_library(ant_t *js) { ant_value_t lib = js_mkobj(js); + ant_value_t exec_fn = js_mkfun(builtin_exec); + ant_value_t exec_file_fn = js_heavy_mkfun(js, builtin_execFile, js_mkundef()); child_process_init_constructor(js); + js_set_symbol(js, exec_file_fn, + "nodejs.util.promisify.custom", + js_heavy_mkfun(js, exec_file_promisified_call, exec_file_fn) + ); + js_set(js, lib, "ChildProcess", g_child_process_ctor); js_set(js, lib, "spawn", js_mkfun(builtin_spawn)); - js_set(js, lib, "exec", js_mkfun(builtin_exec)); - js_set(js, lib, "execFile", js_mkfun(builtin_execFile)); + js_set(js, lib, "exec", exec_fn); + js_set(js, lib, "execFile", exec_file_fn); js_set(js, lib, "execSync", js_mkfun(builtin_execSync)); js_set(js, lib, "execFileSync", js_mkfun(builtin_execFileSync)); js_set(js, lib, "spawnSync", js_mkfun(builtin_spawnSync)); diff --git a/src/modules/events.c b/src/modules/events.c index a83d1b5..9e54c83 100644 --- a/src/modules/events.c +++ b/src/modules/events.c @@ -226,10 +226,31 @@ static int eventemitter_get_max_listeners_impl(ant_value_t target) { return EVENTS_DEFAULT_MAX_LISTENERS; } +static ant_value_t eventemitter_call_listener( + ant_t *js, + ant_value_t listener, + ant_value_t this_val, + ant_value_t *args, + int nargs +) { + if (!is_callable(listener)) return js_mkundef(); + if (sv_check_c_stack_overflow(js)) + return js_mkerr_typed(js, JS_ERR_RANGE | JS_ERR_NO_STACK, "Maximum call stack size exceeded"); + + sv_call_plan_t plan; + ant_value_t err = sv_prepare_call( + js->vm, js, listener, this_val, args, nargs, + NULL, SV_CALL_MODE_NORMAL, &plan + ); + + if (is_err(err)) return err; + return sv_execute_call_plan(js->vm, js, &plan, NULL); +} + static ant_value_t js_eventemitter_once_wrapper(ant_t *js, ant_value_t *args, int nargs) { ant_value_t listener = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); if (!is_callable(listener)) return js_mkundef(); - return sv_vm_call(js->vm, js, listener, js->this_val, args, nargs, NULL, false); + return eventemitter_call_listener(js, listener, js->this_val, args, nargs); } static ant_value_t eventemitter_get_listeners_array(ant_t *js, ant_value_t target, ant_value_t key, bool raw) { @@ -590,20 +611,20 @@ static ant_value_t dispatch_event_to(ant_t *js, ant_value_t event_obj, EventType for (unsigned int i = 0; i < n;) { EventListenerEntry *entry = (EventListenerEntry *)utarray_eltptr(evt->listeners, i); - + if (vtype(entry->signal) != T_UNDEF && abort_signal_is_aborted(entry->signal)) { utarray_erase(evt->listeners, i, 1); n--; continue; } - - ant_value_t cb = entry->callback; - bool once = entry->once; + + ant_value_t cb = entry->callback; + bool once = entry->once; if (once) { utarray_erase(evt->listeners, i, 1); n--; } else i++; uint8_t t = vtype(cb); if (t != T_FUNC && t != T_CFUNC) continue; - - sv_vm_call(js->vm, js, cb, js_mkundef(), call_args, 1, NULL, false); + + eventemitter_call_listener(js, cb, js_mkundef(), call_args, 1); if (data && data->stop_immediate) break; } @@ -809,7 +830,7 @@ static bool eventemitter_emit_args_impl( if (vtype(entry->signal) != T_UNDEF && abort_signal_is_aborted(entry->signal)) continue; if (vtype(cb) != T_FUNC && vtype(cb) != T_CFUNC) continue; - ant_value_t result = sv_vm_call(js->vm, js, cb, target, args, nargs, NULL, false); + ant_value_t result = eventemitter_call_listener(js, cb, target, args, nargs); if (vtype(result) == T_ERR) { if (vtype(key) == T_STR) fprintf(stderr, "Error in event listener for '%s': %s\n", js_str(js, key), js_str(js, result)); else fprintf(stderr, "Error in event listener: %s\n", js_str(js, result)); @@ -978,6 +999,13 @@ static ant_value_t js_eventemitter_rawListeners(ant_t *js, ant_value_t *args, in return eventemitter_get_listeners_array(js, js_getthis(js), key, true); } +static ant_value_t js_eventemitter_listeners(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkarr(js); + ant_value_t key = evt_key_from_arg(args[0]); + if (!key) return js_mkarr(js); + return eventemitter_get_listeners_array(js, js_getthis(js), key, false); +} + static ant_value_t js_eventemitter_eventNames(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = js_getthis(js); ant_value_t result = js_mkarr(js); @@ -1146,7 +1174,7 @@ static ant_value_t js_events_add_abort_listener(ant_t *js, ant_value_t *args, in if (already_aborted) { ant_value_t event = js_mkobj(js); js_set(js, event, "type", js_mkstr(js, "abort", 5)); - sv_vm_call(js->vm, js, args[1], args[0], &event, 1, NULL, false); + eventemitter_call_listener(js, args[1], args[0], &event, 1); } else abort_signal_add_listener(js, args[0], args[1]); ant_value_t state = js_mkobj(js); @@ -1219,6 +1247,7 @@ ant_value_t eventemitter_prototype(ant_t *js) { js_set(js, eventemitter_proto, "listenerCount", js_mkfun(js_eventemitter_listenerCount)); js_set(js, eventemitter_proto, "setMaxListeners", js_mkfun(js_eventemitter_setMaxListeners)); js_set(js, eventemitter_proto, "getMaxListeners", js_mkfun(js_eventemitter_getMaxListeners)); + js_set(js, eventemitter_proto, "listeners", js_mkfun(js_eventemitter_listeners)); js_set(js, eventemitter_proto, "rawListeners", js_mkfun(js_eventemitter_rawListeners)); js_set(js, eventemitter_proto, "eventNames", js_mkfun(js_eventemitter_eventNames)); js_set_sym(js, eventemitter_proto, get_toStringTag_sym(), js_mkstr(js, "EventEmitter", 12)); diff --git a/src/modules/fs.c b/src/modules/fs.c index ebe6116..14339b1 100644 --- a/src/modules/fs.c +++ b/src/modules/fs.c @@ -141,6 +141,7 @@ typedef struct { static ant_value_t g_dirent_proto = 0; static ant_value_t g_fswatcher_proto = 0; static ant_value_t g_fswatcher_ctor = 0; +static ant_value_t g_filehandle_proto = 0; static ant_value_t g_readstream_proto = 0; static ant_value_t g_readstream_ctor = 0; static ant_value_t g_writestream_proto = 0; @@ -149,7 +150,10 @@ static ant_value_t g_writestream_ctor = 0; static fs_watcher_t *active_watchers = NULL; static UT_array *pending_requests = NULL; -enum { FS_WATCHER_NATIVE_TAG = 0x46535754u }; // FSWT +enum { + FS_WATCHER_NATIVE_TAG = 0x46535754u, // FSWT + FS_FILEHANDLE_NATIVE_TAG = 0x46534648u // FSFH +}; static fs_watcher_t *fs_watcher_data(ant_value_t value) { if (!js_check_native_tag(value, FS_WATCHER_NATIVE_TAG)) return NULL; @@ -1329,6 +1333,256 @@ static void free_fs_request(fs_request_t *req) { free(req); } +static ant_value_t fs_rejected_promise(ant_t *js, ant_value_t err) { + ant_value_t promise = js_mkpromise(js); + js_reject_promise(js, promise, err); + return promise; +} + +static ant_value_t fs_resolved_promise(ant_t *js, ant_value_t value) { + ant_value_t promise = js_mkpromise(js); + js_resolve_promise(js, promise, value); + return promise; +} + +static ant_value_t fs_filehandle_require_this(ant_t *js) { + ant_value_t this_obj = js_getthis(js); + if (!is_object_type(this_obj) || !js_check_native_tag(this_obj, FS_FILEHANDLE_NATIVE_TAG)) + return js_mkerr_typed(js, JS_ERR_TYPE, "FileHandle method called on incompatible receiver"); + return this_obj; +} + +static ant_value_t fs_filehandle_fd_value(ant_t *js, ant_value_t handle_obj) { + ant_value_t fd_val = js_get_slot(handle_obj, SLOT_DATA); + if (vtype(fd_val) != T_NUM) fd_val = js_get(js, handle_obj, "fd"); + return fd_val; +} + +static ant_value_t fs_filehandle_get_fd(ant_t *js, ant_value_t handle_obj) { + ant_value_t fd_val = fs_filehandle_fd_value(js, handle_obj); + if (vtype(fd_val) != T_NUM || js_getnum(fd_val) < 0) + return js_mkerr_typed(js, JS_ERR_TYPE, "FileHandle is closed"); + return fd_val; +} + +static void fs_filehandle_mark_closed(ant_t *js, ant_value_t handle_obj) { + js_set_slot(handle_obj, SLOT_DATA, js_mkundef()); + js_set(js, handle_obj, "fd", js_mknum(-1)); +} + +static ant_value_t builtin_fs_filehandle_close(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t handle_obj = fs_filehandle_require_this(js); + if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj); + + ant_value_t fd_val = fs_filehandle_fd_value(js, handle_obj); + if (vtype(fd_val) != T_NUM || js_getnum(fd_val) < 0) + return fs_resolved_promise(js, js_mkundef()); + + uv_fs_t req; + int result = uv_fs_close(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), NULL); + uv_fs_req_cleanup(&req); + if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "close", NULL, NULL)); + + fs_filehandle_mark_closed(js, handle_obj); + return fs_resolved_promise(js, js_mkundef()); +} + +static ant_value_t builtin_fs_filehandle_stat(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t handle_obj = fs_filehandle_require_this(js); + if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj); + + ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj); + if (is_err(fd_val)) return fs_rejected_promise(js, fd_val); + + uv_fs_t req; + int result = uv_fs_fstat(NULL, &req, (uv_file)(int)js_getnum(fd_val), NULL); + if (result < 0) { + ant_value_t err = fs_mk_uv_error(js, result, "fstat", NULL, NULL); + uv_fs_req_cleanup(&req); + return fs_rejected_promise(js, err); + } + + ant_value_t stat_obj = fs_stats_object_from_uv(js, &req.statbuf); + uv_fs_req_cleanup(&req); + return fs_resolved_promise(js, stat_obj); +} + +static ant_value_t builtin_fs_filehandle_sync(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t handle_obj = fs_filehandle_require_this(js); + if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj); + + ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj); + if (is_err(fd_val)) return fs_rejected_promise(js, fd_val); + + uv_fs_t req; + int result = uv_fs_fsync(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), NULL); + uv_fs_req_cleanup(&req); + if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "fsync", NULL, NULL)); + + return fs_resolved_promise(js, js_mkundef()); +} + +static ant_value_t builtin_fs_filehandle_read(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t handle_obj = fs_filehandle_require_this(js); + if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj); + + ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj); + if (is_err(fd_val)) return fs_rejected_promise(js, fd_val); + if (nargs < 1) return fs_rejected_promise(js, js_mkerr(js, "FileHandle.read() requires a buffer argument")); + + TypedArrayData *ta_data = buffer_get_typedarray_data(args[0]); + if (!ta_data || !ta_data->buffer || !ta_data->buffer->data) + return fs_rejected_promise(js, js_mkerr(js, "FileHandle.read() buffer must be a Buffer or TypedArray")); + + size_t buf_len = ta_data->byte_length; + size_t offset = 0; + size_t length = buf_len; + int64_t position = -1; + + if (nargs >= 2 && vtype(args[1]) == T_NUM) offset = (size_t)js_getnum(args[1]); + if (nargs >= 3 && vtype(args[2]) == T_NUM) length = (size_t)js_getnum(args[2]); + if (nargs >= 4 && vtype(args[3]) == T_NUM) position = (int64_t)js_getnum(args[3]); + + if (offset > buf_len) return fs_rejected_promise(js, js_mkerr(js, "offset is out of bounds")); + if (offset + length > buf_len) return fs_rejected_promise(js, js_mkerr(js, "length extends beyond buffer")); + + uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset; + uv_buf_t buf = uv_buf_init((char *)(buf_data + offset), (unsigned int)length); + + uv_fs_t req; + int result = uv_fs_read(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), &buf, 1, position, NULL); + uv_fs_req_cleanup(&req); + if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "read", NULL, NULL)); + + ant_value_t out = js_mkobj(js); + js_set(js, out, "bytesRead", js_mknum((double)result)); + js_set(js, out, "buffer", args[0]); + + return fs_resolved_promise(js, out); +} + +static ant_value_t builtin_fs_filehandle_write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t handle_obj = fs_filehandle_require_this(js); + if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj); + + ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj); + if (is_err(fd_val)) return fs_rejected_promise(js, fd_val); + if (nargs < 1) return fs_rejected_promise(js, js_mkerr(js, "FileHandle.write() requires a data argument")); + + const char *write_data = NULL; + size_t write_len = 0; + int64_t position = -1; + + if (vtype(args[0]) == T_STR) { + write_data = js_getstr(js, args[0], &write_len); + if (!write_data) return fs_rejected_promise(js, js_mkerr(js, "Failed to get string")); + if (nargs >= 2 && vtype(args[1]) == T_NUM) position = (int64_t)js_getnum(args[1]); + } else { + TypedArrayData *ta_data = buffer_get_typedarray_data(args[0]); + if (!ta_data || !ta_data->buffer || !ta_data->buffer->data) + return fs_rejected_promise(js, js_mkerr(js, "FileHandle.write() data must be a Buffer, TypedArray, DataView, or string")); + + uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset; + size_t buf_len = ta_data->byte_length; + size_t offset = 0; + size_t length = buf_len; + + if (nargs >= 2) { + if (vtype(args[1]) == T_OBJ) { + ant_value_t off_val = js_get(js, args[1], "offset"); + ant_value_t len_val = js_get(js, args[1], "length"); + ant_value_t pos_val = js_get(js, args[1], "position"); + if (vtype(off_val) == T_NUM) offset = (size_t)js_getnum(off_val); + if (vtype(len_val) == T_NUM) length = (size_t)js_getnum(len_val); + else length = buf_len - offset; + if (vtype(pos_val) == T_NUM) position = (int64_t)js_getnum(pos_val); + } else if (vtype(args[1]) == T_NUM) { + offset = (size_t)js_getnum(args[1]); + length = buf_len - offset; + if (nargs >= 3 && vtype(args[2]) == T_NUM) length = (size_t)js_getnum(args[2]); + if (nargs >= 4 && vtype(args[3]) == T_NUM) position = (int64_t)js_getnum(args[3]); + }} + + if (offset > buf_len) return fs_rejected_promise(js, js_mkerr(js, "offset is out of bounds")); + if (offset + length > buf_len) return fs_rejected_promise(js, js_mkerr(js, "length extends beyond buffer")); + write_data = (const char *)(buf_data + offset); + write_len = length; + } + + uv_buf_t buf = uv_buf_init((char *)write_data, (unsigned int)write_len); + uv_fs_t req; + int result = uv_fs_write(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), &buf, 1, position, NULL); + uv_fs_req_cleanup(&req); + if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "write", NULL, NULL)); + + ant_value_t out = js_mkobj(js); + js_set(js, out, "bytesWritten", js_mknum((double)result)); + js_set(js, out, "buffer", args[0]); + + return fs_resolved_promise(js, out); +} + +static ant_value_t builtin_fs_filehandle_writeFile(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t handle_obj = fs_filehandle_require_this(js); + if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj); + + ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj); + if (is_err(fd_val)) return fs_rejected_promise(js, fd_val); + if (nargs < 1) return fs_rejected_promise(js, js_mkerr(js, "FileHandle.writeFile() requires data")); + + const char *data = NULL; + size_t len = 0; + + if (vtype(args[0]) == T_STR) { + data = js_getstr(js, args[0], &len); + if (!data) return fs_rejected_promise(js, js_mkerr(js, "Failed to get string")); + } else { + TypedArrayData *ta_data = buffer_get_typedarray_data(args[0]); + if (!ta_data || !ta_data->buffer || !ta_data->buffer->data) + return fs_rejected_promise(js, js_mkerr(js, "FileHandle.writeFile() data must be a Buffer, TypedArray, DataView, or string")); + data = (const char *)(ta_data->buffer->data + ta_data->byte_offset); + len = ta_data->byte_length; + } + + size_t written = 0; + while (written < len) { + uv_buf_t buf = uv_buf_init((char *)(data + written), (unsigned int)(len - written)); + uv_fs_t req; + int result = uv_fs_write(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), &buf, 1, -1, NULL); + uv_fs_req_cleanup(&req); + if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "write", NULL, NULL)); + if (result == 0) return fs_rejected_promise(js, js_mkerr(js, "short write")); + written += (size_t)result; + } + + return fs_resolved_promise(js, js_mkundef()); +} + +static void fs_init_filehandle_proto(ant_t *js) { + if (is_object_type(g_filehandle_proto)) return; + g_filehandle_proto = js_mkobj(js); + js_set_native_tag(g_filehandle_proto, FS_FILEHANDLE_NATIVE_TAG); + js_set(js, g_filehandle_proto, "close", js_mkfun(builtin_fs_filehandle_close)); + js_set(js, g_filehandle_proto, "stat", js_mkfun(builtin_fs_filehandle_stat)); + js_set(js, g_filehandle_proto, "sync", js_mkfun(builtin_fs_filehandle_sync)); + js_set(js, g_filehandle_proto, "read", js_mkfun(builtin_fs_filehandle_read)); + js_set(js, g_filehandle_proto, "write", js_mkfun(builtin_fs_filehandle_write)); + js_set(js, g_filehandle_proto, "writeFile", js_mkfun(builtin_fs_filehandle_writeFile)); + js_set_sym(js, g_filehandle_proto, get_toStringTag_sym(), js_mkstr(js, "FileHandle", 10)); +} + +static ant_value_t fs_make_filehandle(ant_t *js, int fd) { + fs_init_filehandle_proto(js); + ant_value_t handle = js_mkobj(js); + + js_set_native_tag(handle, FS_FILEHANDLE_NATIVE_TAG); + js_set_proto_init(handle, g_filehandle_proto); + js_set_slot(handle, SLOT_DATA, js_mknum((double)fd)); + js_set(js, handle, "fd", js_mknum((double)fd)); + + return handle; +} + static void remove_pending_request(fs_request_t *req) { if (!req || !pending_requests) return; unsigned int len = utarray_len(pending_requests); @@ -1367,8 +1621,8 @@ static void complete_request(fs_request_t *req) { } else reject_value = js_mkerr(req->js, "%s", err_msg); if (is_err(reject_value)) { - reject_value = req->js->thrown_value; - if (vtype(reject_value) == T_UNDEF) + reject_value = req->js->thrown_exists ? req->js->thrown_value : js_mkundef(); + if (!req->js->thrown_exists) reject_value = js_mkstr(req->js, err_msg, strlen(err_msg)); req->js->thrown_exists = false; req->js->thrown_value = js_mkundef(); @@ -3892,6 +4146,26 @@ static void on_open_fd_complete(uv_fs_t *uv_req) { free_fs_request(req); } +static void on_open_filehandle_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + fs_request_fail(req, (int)uv_req->result); + req->completed = 1; + complete_request(req); + return; + } + + req->completed = 1; + js_resolve_promise( + req->js, req->promise, + fs_make_filehandle(req->js, (int)uv_req->result) + ); + + remove_pending_request(req); + free_fs_request(req); +} + static ant_value_t builtin_fs_open_fd(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "open() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "open() path must be a string"); @@ -3924,6 +4198,41 @@ static ant_value_t builtin_fs_open_fd(ant_t *js, ant_value_t *args, int nargs) { return req->promise; } +static ant_value_t builtin_fs_open_filehandle(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "open() requires a path argument"); + if (vtype(args[0]) != T_STR) return js_mkerr(js, "open() path must be a string"); + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + int flags = (nargs >= 2) ? parse_open_flags(js, args[1]) : O_RDONLY; + int mode = (nargs >= 3 && vtype(args[2]) == T_NUM) ? (int)js_getnum(args[2]) : 0666; + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_OPEN; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + int result = uv_fs_open( + uv_default_loop(), &req->uv_req, req->path, + flags, mode, on_open_filehandle_complete + ); + + if (result < 0) { + fs_request_fail(req, result); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + static void on_close_fd_complete(uv_fs_t *uv_req) { fs_request_t *req = (fs_request_t *)uv_req->data; @@ -3973,6 +4282,7 @@ static ant_value_t builtin_fs_watch(ant_t *js, ant_value_t *args, int nargs) { fs_watch_options_t opts; fs_watcher_t *watcher = NULL; uv_handle_t *handle = NULL; + const char *path = NULL; size_t path_len = 0; int rc = 0; @@ -4287,7 +4597,7 @@ static void fs_set_promise_methods(ant_t *js, ant_value_t lib) { js_set(js, lib, "cp", js_mkfun(builtin_fs_cp)); js_set(js, lib, "copyFile", js_mkfun(builtin_fs_copyFile)); js_set(js, lib, "readFile", js_mkfun(builtin_fs_readFile)); - js_set(js, lib, "open", js_mkfun(builtin_fs_open_fd)); + js_set(js, lib, "open", js_mkfun(builtin_fs_open_filehandle)); js_set(js, lib, "close", js_mkfun(builtin_fs_close_fd)); js_set(js, lib, "writeFile", js_mkfun(builtin_fs_writeFile)); js_set(js, lib, "write", js_mkfun(builtin_fs_write_fd)); @@ -4475,6 +4785,7 @@ void gc_mark_fs(ant_t *js, gc_mark_fn mark) { if (g_fswatcher_proto) mark(js, g_fswatcher_proto); if (g_fswatcher_ctor) mark(js, g_fswatcher_ctor); + if (g_filehandle_proto) mark(js, g_filehandle_proto); if (g_readstream_proto) mark(js, g_readstream_proto); if (g_readstream_ctor) mark(js, g_readstream_ctor); if (g_writestream_proto) mark(js, g_writestream_proto); diff --git a/src/modules/generator.c b/src/modules/generator.c index 074546f..7805038 100644 --- a/src/modules/generator.c +++ b/src/modules/generator.c @@ -8,10 +8,13 @@ #include "runtime.h" #include "sugar.h" +#include "gc/roots.h" #include "silver/engine.h" #include "modules/generator.h" #include "modules/symbol.h" +enum { GENERATOR_NATIVE_TAG = 0x47454e52u }; // GENR + typedef enum { GEN_SUSPENDED_START = 0, GEN_SUSPENDED_YIELD = 1, @@ -19,14 +22,26 @@ typedef enum { GEN_COMPLETED = 3, } generator_state_t; -enum { GENERATOR_NATIVE_TAG = 0x47454e52u }; // GENR +typedef struct generator_request { + sv_resume_kind_t kind; + ant_value_t value; + ant_value_t promise; + struct generator_request *next; +} generator_request_t; typedef struct generator_data { coroutine_t *coro; generator_state_t state; + generator_request_t *queue_head; + generator_request_t *queue_tail; bool is_async; } generator_data_t; +static ant_value_t generator_resume_kind( + ant_t *js, ant_value_t gen, + ant_value_t resume_value, sv_resume_kind_t resume_kind +); + static ant_value_t generator_result(ant_t *js, bool done, ant_value_t value) { ant_value_t result = js_mkobj(js); js_set(js, result, "done", js_bool(done)); @@ -68,19 +83,203 @@ static ant_value_t generator_async_wrap_result(ant_t *js, ant_value_t result) { return promise; } +static generator_request_t *generator_dequeue_request(generator_data_t *data) { + if (!data || !data->queue_head) return NULL; + generator_request_t *req = data->queue_head; + data->queue_head = req->next; + if (!data->queue_head) data->queue_tail = NULL; + req->next = NULL; + return req; +} + +static void generator_free_queue(generator_data_t *data) { + if (!data) return; + generator_request_t *req = data->queue_head; + + while (req) { + generator_request_t *next = req->next; + free(req); + req = next; + } + + data->queue_head = NULL; + data->queue_tail = NULL; +} + +static ant_value_t generator_enqueue_request( + ant_t *js, ant_value_t gen, sv_resume_kind_t kind, ant_value_t value +) { + generator_data_t *data = generator_data(gen); + if (!data || !data->is_async) return js_mkerr_typed(js, JS_ERR_TYPE, "Generator is already executing"); + + ant_value_t promise = js_mkpromise(js); + if (is_err(promise)) return promise; + + generator_request_t *req = (generator_request_t *)calloc(1, sizeof(*req)); + if (!req) return js_mkerr(js, "out of memory for generator request"); + + *req = (generator_request_t){ + .kind = kind, + .value = value, + .promise = promise, + .next = NULL, + }; + + if (data->queue_tail) data->queue_tail->next = req; + else data->queue_head = req; + data->queue_tail = req; + + return promise; +} + +static void generator_settle_request_promise(ant_t *js, ant_value_t promise, ant_value_t result) { + if (is_err(result)) { + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js_reject_promise(js, promise, reject_value); + } else js_resolve_promise(js, promise, result); +} + +static void generator_process_queue(ant_t *js, ant_value_t gen) { + generator_data_t *data = generator_data(gen); + if (!data || !data->is_async) return; + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, gen); + + while (data->state != GEN_EXECUTING && data->queue_head) { + generator_request_t *req = generator_dequeue_request(data); + if (!req) break; + + GC_ROOT_PIN(js, req->value); + GC_ROOT_PIN(js, req->promise); + + coroutine_t *coro = data->coro; + if (coro) coro->async_promise = req->promise; + + ant_value_t result = generator_resume_kind(js, gen, req->value, req->kind); + GC_ROOT_PIN(js, result); + + if (vtype(result) != T_PROMISE || result != req->promise) { + generator_settle_request_promise(js, req->promise, result); + js_maybe_drain_microtasks_after_async_settle(js); + } + + free(req); + data = generator_data(gen); + if (!data) break; + } + + GC_ROOT_RESTORE(js, root_mark); +} + static coroutine_t *generator_coro(ant_value_t gen) { generator_data_t *data = generator_data(gen); return data ? data->coro : NULL; } +static void generator_clear_coro(ant_value_t gen, coroutine_t *coro) { + generator_data_t *data = generator_data(gen); + if (data && data->coro == coro) data->coro = NULL; + if (coro) free_coroutine(coro); +} + coroutine_t *generator_get_coro_for_gc(ant_value_t gen) { return generator_coro(gen); } -static void generator_clear_coro(ant_value_t gen, coroutine_t *coro) { +void generator_mark_for_gc(ant_t *js, ant_value_t gen) { generator_data_t *data = generator_data(gen); - if (data && data->coro == coro) data->coro = NULL; - if (coro) free_coroutine(coro); + if (!data) return; + for (generator_request_t *req = data->queue_head; req; req = req->next) { + gc_mark_value(js, req->value); + gc_mark_value(js, req->promise); + } +} + +static ant_value_t generator_find_owner_in_list(ant_object_t *head, coroutine_t *coro) { + for (ant_object_t *obj = head; obj; obj = obj->next) { + ant_value_t candidate = js_obj_from_ptr(obj); + generator_data_t *data = generator_data(candidate); + if (data && data->coro == coro) return candidate; + } + return js_mkundef(); +} + +static ant_value_t generator_find_owner(ant_t *js, coroutine_t *coro) { + ant_value_t gen = generator_find_owner_in_list(js->objects, coro); + if (vtype(gen) != T_UNDEF) return gen; + gen = generator_find_owner_in_list(js->objects_old, coro); + if (vtype(gen) != T_UNDEF) return gen; + return generator_find_owner_in_list(js->permanent_objects, coro); +} + +bool generator_resume_pending_request(ant_t *js, coroutine_t *coro, ant_value_t result) { + if (!coro || coro->type != CORO_GENERATOR || vtype(coro->async_promise) != T_PROMISE) return false; + + ant_value_t gen = generator_find_owner(js, coro); + generator_data_t *data = generator_data(gen); + if (!data || !data->is_async) return false; + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, gen); + GC_ROOT_PIN(js, result); + + ant_value_t pending = coro->async_promise; + GC_ROOT_PIN(js, pending); + + if (is_err(result)) { + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + + GC_ROOT_PIN(js, reject_value); + coro->async_promise = js_mkundef(); + generator_set_state(gen, GEN_COMPLETED); + generator_clear_coro(gen, coro); + + js_reject_promise(js, pending, reject_value); + js_maybe_drain_microtasks_after_async_settle(js); + generator_process_queue(js, gen); + GC_ROOT_RESTORE(js, root_mark); + + return true; + } + + if (coro->sv_vm && coro->sv_vm->suspended) { + if (vtype(coro->awaited_promise) != T_UNDEF) { + generator_set_state(gen, GEN_EXECUTING); + GC_ROOT_RESTORE(js, root_mark); + return true; + } + + ant_value_t out = generator_result(js, false, result); + GC_ROOT_PIN(js, out); + coro->async_promise = js_mkundef(); + generator_set_state(gen, GEN_SUSPENDED_YIELD); + + js_resolve_promise(js, pending, out); + js_maybe_drain_microtasks_after_async_settle(js); + generator_process_queue(js, gen); + GC_ROOT_RESTORE(js, root_mark); + + return true; + } + + ant_value_t out = generator_result(js, true, result); + GC_ROOT_PIN(js, out); + coro->async_promise = js_mkundef(); + + generator_set_state(gen, GEN_COMPLETED); + generator_clear_coro(gen, coro); + + js_resolve_promise(js, pending, out); + js_maybe_drain_microtasks_after_async_settle(js); + generator_process_queue(js, gen); + GC_ROOT_RESTORE(js, root_mark); + + return true; } static void generator_finalize(ant_t *js, ant_object_t *obj) { @@ -89,45 +288,89 @@ static void generator_finalize(ant_t *js, ant_object_t *obj) { if (!data) return; if (data->coro) { - free_coroutine(data->coro); + coroutine_t *coro = data->coro; + bool linked_active = false; + bool linked_pending = + coro->prev || coro->next || + pending_coroutines.head == coro || + pending_coroutines.tail == coro; + + for (coroutine_t *it = js->active_async_coro; it; it = it->active_parent) if (it == coro) { + linked_active = true; + break; + } + + bool awaiting = vtype(coro->awaited_promise) != T_UNDEF; + if (!linked_pending && !linked_active && !awaiting) free_coroutine(coro); data->coro = NULL; } js_set_native_ptr(gen, NULL); js_set_native_tag(gen, 0); + + generator_free_queue(data); free(data); } static ant_value_t generator_resume_kind( ant_t *js, ant_value_t gen, ant_value_t resume_value, sv_resume_kind_t resume_kind ) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, gen); + GC_ROOT_PIN(js, resume_value); coroutine_t *coro = generator_coro(gen); + if (!coro || !coro->sv_vm) { generator_set_state(gen, GEN_COMPLETED); - if (resume_kind == SV_RESUME_THROW) return js_throw(js, resume_value); - return generator_result(js, true, resume_kind == SV_RESUME_RETURN ? resume_value : js_mkundef()); + if (resume_kind == SV_RESUME_THROW) { + GC_ROOT_RESTORE(js, root_mark); + return js_throw(js, resume_value); + } + + ant_value_t out = generator_result( + js, true, resume_kind == SV_RESUME_RETURN + ? resume_value : js_mkundef() + ); + + GC_ROOT_RESTORE(js, root_mark); + return out; } generator_state_t state = generator_state(gen); - if (state == GEN_EXECUTING) return js_mkerr_typed( - js, JS_ERR_TYPE, "Generator is already executing" - ); + if (state == GEN_EXECUTING) { + ant_value_t queued = generator_enqueue_request(js, gen, resume_kind, resume_value); + GC_ROOT_RESTORE(js, root_mark); + return queued; + } if (state == GEN_COMPLETED) { - if (resume_kind == SV_RESUME_THROW) return js_throw(js, resume_value); - return generator_result(js, true, resume_kind == SV_RESUME_RETURN ? resume_value : js_mkundef()); + if (resume_kind == SV_RESUME_THROW) { + GC_ROOT_RESTORE(js, root_mark); + return js_throw(js, resume_value); + } + + ant_value_t out = generator_result( + js, true, resume_kind == SV_RESUME_RETURN + ? resume_value : js_mkundef() + ); + + GC_ROOT_RESTORE(js, root_mark); + return out; } if (state == GEN_SUSPENDED_START && resume_kind == SV_RESUME_THROW) { generator_set_state(gen, GEN_COMPLETED); generator_clear_coro(gen, coro); + GC_ROOT_RESTORE(js, root_mark); return js_throw(js, resume_value); } if (state == GEN_SUSPENDED_START && resume_kind == SV_RESUME_RETURN) { generator_set_state(gen, GEN_COMPLETED); generator_clear_coro(gen, coro); - return generator_result(js, true, resume_value); + ant_value_t out = generator_result(js, true, resume_value); + GC_ROOT_RESTORE(js, root_mark); + return out; } generator_set_state(gen, GEN_EXECUTING); @@ -151,25 +394,45 @@ static ant_value_t generator_resume_kind( coro->sv_vm->suspended_resume_pending = true; result = sv_resume_suspended(coro->sv_vm); } - + + GC_ROOT_PIN(js, result); js->active_async_coro = saved_active; coro->active_parent = NULL; if (is_err(result)) { generator_set_state(gen, GEN_COMPLETED); generator_clear_coro(gen, coro); + GC_ROOT_RESTORE(js, root_mark); return result; } if (coro->sv_vm && coro->sv_vm->suspended) { + if (generator_is_async(gen) && vtype(coro->awaited_promise) != T_UNDEF) { + generator_set_state(gen, GEN_EXECUTING); + if (vtype(coro->async_promise) != T_PROMISE) { + coro->async_promise = js_mkpromise(js); + GC_ROOT_PIN(js, coro->async_promise); + } + + ant_value_t out = coro->async_promise; + GC_ROOT_RESTORE(js, root_mark); + return out; + } + generator_set_state(gen, GEN_SUSPENDED_YIELD); - return generator_result(js, false, result); + ant_value_t out = generator_result(js, false, result); + GC_ROOT_RESTORE(js, root_mark); + + return out; } generator_set_state(gen, GEN_COMPLETED); generator_clear_coro(gen, coro); - return generator_result(js, true, result); + ant_value_t out = generator_result(js, true, result); + GC_ROOT_RESTORE(js, root_mark); + + return out; } static ant_value_t generator_resume(ant_t *js, ant_value_t gen, ant_value_t resume_value) { @@ -180,10 +443,11 @@ static ant_value_t generator_next(ant_t *js, ant_value_t *args, int nargs) { ant_value_t gen = js->this_val; if (vtype(gen) != T_GENERATOR) return js_mkerr_typed(js, JS_ERR_TYPE, "Generator.prototype.next called on incompatible receiver"); - + ant_value_t resume_value = nargs > 0 ? args[0] : js_mkundef(); ant_value_t result = generator_resume(js, gen, resume_value); + if (generator_is_async(gen) && vtype(result) == T_PROMISE) return result; return generator_is_async(gen) ? generator_async_wrap_result(js, result) : result; } @@ -193,12 +457,13 @@ static ant_value_t generator_return(ant_t *js, ant_value_t *args, int nargs) { return js_mkerr_typed(js, JS_ERR_TYPE, "Generator.prototype.return called on incompatible receiver"); generator_state_t state = generator_state(gen); - if (state == GEN_EXECUTING) + if (state == GEN_EXECUTING && !generator_is_async(gen)) return js_mkerr_typed(js, JS_ERR_TYPE, "Generator is already executing"); - + ant_value_t value = nargs > 0 ? args[0] : js_mkundef(); ant_value_t result = generator_resume_kind(js, gen, value, SV_RESUME_RETURN); + if (generator_is_async(gen) && vtype(result) == T_PROMISE) return result; return generator_is_async(gen) ? generator_async_wrap_result(js, result) : result; } @@ -206,14 +471,15 @@ static ant_value_t generator_throw(ant_t *js, ant_value_t *args, int nargs) { ant_value_t gen = js->this_val; if (vtype(gen) != T_GENERATOR) return js_mkerr_typed(js, JS_ERR_TYPE, "Generator.prototype.throw called on incompatible receiver"); - + generator_state_t state = generator_state(gen); - if (state == GEN_EXECUTING) + if (state == GEN_EXECUTING && !generator_is_async(gen)) return js_mkerr_typed(js, JS_ERR_TYPE, "Generator is already executing"); - + ant_value_t value = nargs > 0 ? args[0] : js_mkundef(); ant_value_t result = generator_resume_kind(js, gen, value, SV_RESUME_THROW); + if (generator_is_async(gen) && vtype(result) == T_PROMISE) return result; return generator_is_async(gen) ? generator_async_wrap_result(js, result) : result; } diff --git a/src/modules/process.c b/src/modules/process.c index 5be2147..cf02de6 100644 --- a/src/modules/process.c +++ b/src/modules/process.c @@ -31,6 +31,7 @@ #include "errors.h" #include "output.h" #include "utils.h" +#include "tty_ctrl.h" #include "internal.h" #include "descriptors.h" #include "runtime.h" @@ -783,21 +784,29 @@ static ant_value_t js_stdin_remove_listener(ant_t *js, ant_value_t *args, int na return this_obj; } -static ant_value_t js_stdout_write(ant_t *js, ant_value_t *args, int nargs) { +static ant_value_t process_write_stream(ant_t *js, ant_value_t *args, int nargs, FILE *stream, int fd) { if (nargs < 1) return js_false; + size_t len = 0; char *data = js_getstr(js, args[0], &len); - ant_output_stream_t *out = NULL; + if (!data) return js_false; - - out = ant_output_stream(stdout); + if (uv_guess_handle(fd) == UV_TTY) { + return js_bool(tty_ctrl_write_fd(fd, data, len)); + } + + out = ant_output_stream(stream); ant_output_stream_begin(out); - + if (!ant_output_stream_append(out, data, len)) return js_false; return ant_output_stream_flush(out) ? js_true : js_false; } +static ant_value_t js_stdout_write(ant_t *js, ant_value_t *args, int nargs) { + return process_write_stream(js, args, nargs, stdout, STDOUT_FILENO); +} + static ant_value_t js_stdout_on(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = js_getthis(js); if (nargs < 2) return this_obj; @@ -886,25 +895,13 @@ static ant_value_t js_stdout_rows_getter(ant_t *js, ant_value_t *args, int nargs } static ant_value_t js_stdout_columns_getter(ant_t *js, ant_value_t *args, int nargs) { - (void)args; (void)nargs; int rows = 0, cols = 0; get_tty_size(STDOUT_FILENO, &rows, &cols); return js_mknum(cols); } static ant_value_t js_stderr_write(ant_t *js, ant_value_t *args, int nargs) { - if (nargs < 1) return js_false; - size_t len = 0; - char *data = js_getstr(js, args[0], &len); - - ant_output_stream_t *out = NULL; - if (!data) return js_false; - - out = ant_output_stream(stderr); - ant_output_stream_begin(out); - - if (!ant_output_stream_append(out, data, len)) return js_false; - return ant_output_stream_flush(out) ? js_true : js_false; + return process_write_stream(js, args, nargs, stderr, STDERR_FILENO); } static ant_value_t js_stderr_on(ant_t *js, ant_value_t *args, int nargs) { @@ -1762,8 +1759,8 @@ static ant_value_t process_next_tick(ant_t *js, ant_value_t *args, int nargs) { if (vtype(cb) != T_FUNC && vtype(cb) != T_CFUNC) return js_mkerr_typed(js, JS_ERR_TYPE, "process.nextTick callback is not a function"); - if (nargs <= 1) queue_microtask(js, cb); - else queue_microtask_with_args(js, cb, args + 1, nargs - 1); + if (nargs <= 1) queue_next_tick(js, cb); + else queue_next_tick_with_args(js, cb, args + 1, nargs - 1); return js_mkundef(); } diff --git a/src/modules/stream.c b/src/modules/stream.c index 7be3736..b96f80f 100644 --- a/src/modules/stream.c +++ b/src/modules/stream.c @@ -37,11 +37,6 @@ static ant_value_t g_passthrough_ctor = 0; static double g_default_high_water_mark = 16384.0; static double g_default_object_high_water_mark = 16.0; -static ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj); -static ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj); -static ant_value_t stream_readable_push_value(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding); -static ant_value_t stream_readable_continue_flowing(ant_t *js, ant_value_t *args, int nargs); - static ant_value_t stream_noop(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); } @@ -50,6 +45,15 @@ static bool stream_is_instance(ant_value_t value) { return is_object_type(value) && js_check_native_tag(value, STREAM_NATIVE_TAG); } +static inline void stream_set_end_callback(ant_t *js, ant_value_t stream_obj, ant_value_t callback) { + js_set_slot_wb(js, stream_obj, SLOT_AUX, callback); +} + +static stream_private_state_t *stream_private_state(ant_value_t stream_obj) { + if (!stream_is_instance(stream_obj)) return NULL; + return (stream_private_state_t *)js_get_native_ptr(stream_obj); +} + static ant_value_t stream_require_this(ant_t *js, ant_value_t value, const char *label) { if (!stream_is_instance(value)) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid %s", label); @@ -74,6 +78,39 @@ static ant_value_t stream_event_key(ant_t *js, ant_value_t value) { return js_mkerr(js, "event must be a string or Symbol"); } +void *stream_get_attached_state(ant_value_t stream_obj) { + stream_private_state_t *priv = stream_private_state(stream_obj); + return priv ? priv->attached_state : NULL; +} + +void stream_set_attached_state( + ant_value_t stream_obj, + void *state, + stream_finalize_fn finalize +) { + stream_private_state_t *priv = stream_private_state(stream_obj); + if (!priv) return; + priv->attached_state = state; + priv->attached_state_finalize = finalize; +} + +void stream_clear_attached_state(ant_value_t stream_obj) { + stream_private_state_t *priv = stream_private_state(stream_obj); + if (!priv) return; + priv->attached_state = NULL; + priv->attached_state_finalize = NULL; +} + +static void stream_finalize(ant_t *js, ant_object_t *obj) { + ant_value_t stream_obj = js_obj_from_ptr(obj); + stream_private_state_t *priv = stream_private_state(stream_obj); + if (!priv) return; + js_set_native_ptr(stream_obj, NULL); + if (priv->attached_state && priv->attached_state_finalize) + priv->attached_state_finalize(js, stream_obj, priv->attached_state); + free(priv); +} + static ant_value_t stream_call( ant_t *js, ant_value_t fn, @@ -83,7 +120,15 @@ static ant_value_t stream_call( bool is_ctor ) { if (!is_callable(fn)) return js_mkundef(); - return sv_vm_call(js->vm, js, fn, this_val, args, nargs, NULL, is_ctor); + if (sv_check_c_stack_overflow(js)) + return js_mkerr_typed(js, JS_ERR_RANGE | JS_ERR_NO_STACK, "Maximum call stack size exceeded"); + + sv_call_mode_t mode = is_ctor ? SV_CALL_MODE_CONSTRUCT : SV_CALL_MODE_NORMAL; + sv_call_plan_t plan; + ant_value_t err = sv_prepare_call(js->vm, js, fn, this_val, args, nargs, NULL, mode, &plan); + if (is_err(err)) return err; + + return sv_execute_call_plan(js->vm, js, &plan, NULL); } static ant_value_t stream_call_prop( @@ -276,8 +321,15 @@ static double stream_high_water_mark_from_options(ant_t *js, ant_value_t options static ant_value_t stream_make_base_object(ant_t *js, ant_value_t proto) { ant_value_t obj = js_mkobj(js); + stream_private_state_t *priv = calloc(1, sizeof(*priv)); + if (is_object_type(proto)) js_set_proto_init(obj, proto); js_set_native_tag(obj, STREAM_NATIVE_TAG); + + if (priv) js_set_native_ptr(obj, priv); + js_set_slot(obj, SLOT_AUX, js_mkundef()); + js_set_finalizer(obj, stream_finalize); + return obj; } @@ -596,7 +648,7 @@ static ant_value_t js_stream_destroy(ant_t *js, ant_value_t *args, int nargs) { done_state = js_mkobj(js); js_set(js, done_state, "stream", stream_obj); done = stream_make_once(js, js_heavy_mkfun(js, stream_destroy_done, done_state), js_mkundef()); - destroy_fn = js_get(js, stream_obj, "_destroy"); + destroy_fn = js_getprop_fallback(js, stream_obj, "_destroy"); if (is_callable(destroy_fn)) { destroy_args[0] = is_undefined(error) ? js_mknull() : error; @@ -615,33 +667,39 @@ static ant_value_t js_readable__read(ant_t *js, ant_value_t *args, int nargs) { } static ant_value_t stream_readable_start_flowing(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + return stream_readable_begin_flowing(js, stream_obj); +} + +ant_value_t stream_readable_continue_flowing(ant_t *js, ant_value_t *args, int nargs) { ant_value_t stream_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); ant_value_t state = stream_readable_state(js, stream_obj); if (!is_object_type(state)) return js_mkundef(); + js_set(js, state, "flowingReadScheduled", js_false); + if (!js_truthy(js, js_get(js, state, "flowing"))) return js_mkundef(); + if (js_truthy(js, js_get(js, stream_obj, "destroyed"))) return js_mkundef(); + if (js_truthy(js, js_get(js, state, "ended"))) return js_mkundef(); - { - ant_value_t saved_this = js->this_val; - js->this_val = stream_obj; - js_stream_resume(js, NULL, 0); - js->this_val = saved_this; - } stream_readable_maybe_read(js, stream_obj); stream_readable_flush(js, stream_obj); + return js_mkundef(); } -static ant_value_t stream_readable_continue_flowing(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t stream_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); +ant_value_t stream_readable_begin_flowing(ant_t *js, ant_value_t stream_obj) { ant_value_t state = stream_readable_state(js, stream_obj); if (!is_object_type(state)) return js_mkundef(); - js_set(js, state, "flowingReadScheduled", js_false); - if (!js_truthy(js, js_get(js, state, "flowing"))) return js_mkundef(); - if (js_truthy(js, js_get(js, stream_obj, "destroyed"))) return js_mkundef(); - if (js_truthy(js, js_get(js, state, "ended"))) return js_mkundef(); + + { + ant_value_t saved_this = js->this_val; + js->this_val = stream_obj; + js_stream_resume(js, NULL, 0); + js->this_val = saved_this; + } stream_readable_maybe_read(js, stream_obj); stream_readable_flush(js, stream_obj); @@ -649,7 +707,7 @@ static ant_value_t stream_readable_continue_flowing(ant_t *js, ant_value_t *args return js_mkundef(); } -static ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj) { +ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj) { ant_value_t state = stream_readable_state(js, stream_obj); bool emitted_data = false; @@ -675,7 +733,7 @@ static ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj) { return js_mkundef(); } -static ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj) { +ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj) { ant_value_t state = stream_readable_state(js, stream_obj); ant_value_t read_fn = 0; ant_value_t args[1]; @@ -686,15 +744,17 @@ static ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj) if (js_truthy(js, js_get(js, state, "ended"))) return js_mkundef(); if (stream_readable_buffer_len(js, stream_obj) > 0) return js_mkundef(); - read_fn = js_get(js, stream_obj, "_read"); + read_fn = js_getprop_fallback(js, stream_obj, "_read"); js_set(js, state, "reading", js_true); args[0] = js_get(js, state, "highWaterMark"); + if (is_callable(read_fn)) stream_call(js, read_fn, stream_obj, args, 1, false); js_set(js, state, "reading", js_false); + return js_mkundef(); } -static ant_value_t stream_readable_push_value( +ant_value_t stream_readable_push_value( ant_t *js, ant_value_t stream_obj, ant_value_t chunk, @@ -713,8 +773,7 @@ static ant_value_t stream_readable_push_value( } normalized = stream_normalize_chunk( - js, - chunk, + js, chunk, js_truthy(js, js_get(js, state, "objectMode")), is_undefined(encoding) ? js_mkstr(js, "utf8", 4) : encoding ); @@ -722,6 +781,7 @@ static ant_value_t stream_readable_push_value( stream_buffer_push(js, stream_obj, normalized); if (js_truthy(js, js_get(js, state, "flowing"))) stream_readable_flush(js, stream_obj); + return js_bool(js_truthy(js, js_get(js, state, "flowing"))); } @@ -747,6 +807,7 @@ static ant_value_t js_readable_read(ant_t *js, ant_value_t *args, int nargs) { chunk = stream_buffer_shift(js, stream_obj); if (js_truthy(js, js_get(js, state, "flowing"))) stream_readable_flush(js, stream_obj); + return chunk; } @@ -758,10 +819,11 @@ static ant_value_t js_readable_on(ant_t *js, ant_value_t *args, int nargs) { if (is_err(stream_obj)) return stream_obj; if (nargs < 2) return js_mkerr(js, "on requires 2 arguments (event, listener)"); key = stream_event_key(js, args[0]); + if (is_err(key)) return key; if (!eventemitter_add_listener_val(js, stream_obj, key, args[1], false)) return js_mkerr(js, "listener must be a function"); - + if (stream_key_is_cstr(js, key, "data")) { state = stream_readable_state(js, stream_obj); if (is_object_type(state)) js_set(js, state, "flowing", js_true); @@ -778,9 +840,11 @@ static ant_value_t js_readable_resume(ant_t *js, ant_value_t *args, int nargs) { state = stream_readable_state(js, stream_obj); if (is_object_type(state)) js_set(js, state, "flowing", js_true); + js_stream_resume(js, NULL, 0); stream_readable_maybe_read(js, stream_obj); stream_readable_flush(js, stream_obj); + return stream_obj; } @@ -810,46 +874,61 @@ static ant_value_t stream_writable_write_done(ant_t *js, ant_value_t *args, int ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); ant_value_t stream_obj = js_get(js, state_obj, "stream"); ant_value_t callback = js_get(js, state_obj, "callback"); + + stream_private_state_t *priv = stream_private_state(stream_obj); ant_value_t err = nargs > 0 ? args[0] : js_mkundef(); + if (priv) priv->writing = false; + if (!is_undefined(err) && !is_null(err)) { ant_value_t destroy_args[1] = { err }; js_set(js, state_obj, "done", js_true); - { + if (priv) priv->pending_final = false; { ant_value_t saved_this = js->this_val; js->this_val = stream_obj; js_stream_destroy(js, destroy_args, 1); js->this_val = saved_this; } + if (is_callable(callback)) stream_call_callback(js, callback, &err, 1); return js_mkundef(); } if (is_callable(callback)) stream_call_callback(js, callback, NULL, 0); stream_emit_named(js, stream_obj, "drain"); + + if (priv && priv->pending_final && !priv->final_started) { + ant_value_t end_callback = js_get_slot(stream_obj, SLOT_AUX); + priv->pending_final = false; + stream_set_end_callback(js, stream_obj, js_mkundef()); + return stream_writable_begin_end(js, stream_obj, end_callback); + } + return js_mkundef(); } -static ant_value_t js_writable_write(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Writable"); +static ant_value_t stream_writable_write_impl( + ant_t *js, + ant_value_t stream_obj, + ant_value_t chunk, + ant_value_t encoding, + ant_value_t callback, + bool allow_after_end +) { ant_value_t state = 0; - ant_value_t callback = js_mkundef(); - ant_value_t encoding = js_mkstr(js, "utf8", 4); ant_value_t normalized = 0; ant_value_t write_fn = 0; ant_value_t done_state = 0; ant_value_t done = 0; ant_value_t write_args[3]; - if (is_err(stream_obj)) return stream_obj; state = stream_writable_state(js, stream_obj); if (!is_object_type(state)) return js_false; - if (nargs > 1 && is_callable(args[1])) callback = args[1]; - else if (nargs > 1 && !is_undefined(args[1])) encoding = args[1]; - if (nargs > 2 && is_callable(args[2])) callback = args[2]; - - if (js_truthy(js, js_get(js, stream_obj, "writableEnded")) || js_truthy(js, js_get(js, stream_obj, "destroyed"))) { + if ( + (!allow_after_end && js_truthy(js, js_get(js, stream_obj, "writableEnded"))) || + js_truthy(js, js_get(js, stream_obj, "destroyed")) + ) { ant_value_t err = js_mkerr(js, "write after end"); if (is_callable(callback)) stream_call_callback(js, callback, &err, 1); else stream_emit_error(js, stream_obj, err); @@ -857,35 +936,56 @@ static ant_value_t js_writable_write(ant_t *js, ant_value_t *args, int nargs) { } normalized = stream_normalize_chunk( - js, - nargs > 0 ? args[0] : js_mkundef(), + js, chunk, js_truthy(js, js_get(js, state, "objectMode")), encoding ); + if (is_err(normalized)) return normalized; - done_state = js_mkobj(js); + js_set(js, done_state, "stream", stream_obj); js_set(js, done_state, "callback", callback); js_set(js, done_state, "done", js_false); + done = stream_make_once(js, js_heavy_mkfun(js, stream_writable_write_done, done_state), js_mkundef()); - write_fn = js_get(js, stream_obj, "_write"); + write_fn = js_getprop_fallback(js, stream_obj, "_write"); + stream_private_state_t *priv = stream_private_state(stream_obj); + if (priv) priv->writing = true; write_args[0] = normalized; write_args[1] = encoding; write_args[2] = done; + if (is_callable(write_fn)) { - ant_value_t result = stream_call(js, write_fn, stream_obj, write_args, 3, false); - if (is_err(result)) { - ant_value_t err_args[1] = { result }; - stream_call_callback(js, done, err_args, 1); - return js_false; - } - } + ant_value_t result = stream_call(js, write_fn, stream_obj, write_args, 3, false); + if (is_err(result)) { + ant_value_t err_args[1] = { result }; + stream_call_callback(js, done, err_args, 1); + return js_false; + }} return js_bool(!js_truthy(js, js_get(js, stream_obj, "destroyed"))); } +static ant_value_t js_writable_write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Writable"); + ant_value_t callback = js_mkundef(); + ant_value_t encoding = js_mkstr(js, "utf8", 4); + + if (is_err(stream_obj)) return stream_obj; + + if (nargs > 1 && is_callable(args[1])) callback = args[1]; + else if (nargs > 1 && !is_undefined(args[1])) encoding = args[1]; + if (nargs > 2 && is_callable(args[2])) callback = args[2]; + + return stream_writable_write_impl( + js, stream_obj, + nargs > 0 ? args[0] : js_mkundef(), + encoding, callback, false + ); +} + static ant_value_t stream_writable_end_done(ant_t *js, ant_value_t *args, int nargs) { ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); ant_value_t stream_obj = js_get(js, state_obj, "stream"); @@ -905,23 +1005,73 @@ static ant_value_t stream_writable_end_done(ant_t *js, ant_value_t *args, int na js_set(js, stream_obj, "writableFinished", js_true); js_set(js, stream_writable_state(js, stream_obj), "finished", js_true); stream_emit_named(js, stream_obj, "finish"); + if (is_callable(callback)) stream_call_callback(js, callback, NULL, 0); if (!js_truthy(js, js_get(js, stream_obj, "readable"))) stream_emit_named(js, stream_obj, "close"); + return js_mkundef(); } +ant_value_t stream_writable_begin_end(ant_t *js, ant_value_t stream_obj, ant_value_t callback) { + ant_value_t final_fn = 0; + ant_value_t final_args[1]; + ant_value_t done_state = 0; + ant_value_t done = 0; + stream_private_state_t *priv = stream_private_state(stream_obj); + + done_state = js_mkobj(js); + js_set(js, done_state, "stream", stream_obj); + js_set(js, done_state, "callback", callback); + + done = stream_make_once(js, js_heavy_mkfun(js, stream_writable_end_done, done_state), js_mkundef()); + if (priv) { + priv->final_started = true; + priv->pending_final = false; + } + + stream_set_end_callback(js, stream_obj, js_mkundef()); + final_fn = js_getprop_fallback(js, stream_obj, "_final"); + final_args[0] = done; + + if (is_callable(final_fn)) stream_call(js, final_fn, stream_obj, final_args, 1, false); + else stream_call_callback(js, done, NULL, 0); + + return stream_obj; +} + +static ant_value_t stream_writable_end_after_write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + ant_value_t callback = js_get(js, state_obj, "callback"); + stream_private_state_t *priv = stream_private_state(stream_obj); + ant_value_t err = nargs > 0 ? args[0] : js_mkundef(); + + if (!is_undefined(err) && !is_null(err)) { + ant_value_t destroy_args[1] = { err }; + ant_value_t saved_this = js->this_val; + js->this_val = stream_obj; + js_stream_destroy(js, destroy_args, 1); + js->this_val = saved_this; + if (is_callable(callback)) stream_call_callback(js, callback, &err, 1); + return js_mkundef(); + } + + if (priv) priv->pending_final = false; + stream_set_end_callback(js, stream_obj, js_mkundef()); + + return stream_writable_begin_end(js, stream_obj, callback); +} + static ant_value_t js_writable_end(ant_t *js, ant_value_t *args, int nargs) { ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Writable"); ant_value_t callback = js_mkundef(); ant_value_t chunk = js_mkundef(); ant_value_t encoding = js_mkundef(); - ant_value_t final_fn = 0; - ant_value_t final_args[1]; - ant_value_t done_state = 0; - ant_value_t done = 0; + ant_value_t after_write_state = 0; + ant_value_t after_write = 0; + stream_private_state_t *priv = NULL; if (is_err(stream_obj)) return stream_obj; - if (nargs > 0 && is_callable(args[0])) callback = args[0]; else { if (nargs > 0) chunk = args[0]; @@ -930,17 +1080,6 @@ static ant_value_t js_writable_end(ant_t *js, ant_value_t *args, int nargs) { if (nargs > 2 && is_callable(args[2])) callback = args[2]; } - if (!is_undefined(chunk) && !is_null(chunk)) { - ant_value_t write_args[3]; - ant_value_t saved_this = js->this_val; - write_args[0] = chunk; - write_args[1] = encoding; - write_args[2] = js_mkundef(); - js->this_val = stream_obj; - js_writable_write(js, write_args, 3); - js->this_val = saved_this; - } - if (js_truthy(js, js_get(js, stream_obj, "writableEnded"))) { if (is_callable(callback)) stream_call_callback(js, callback, NULL, 0); return stream_obj; @@ -948,18 +1087,29 @@ static ant_value_t js_writable_end(ant_t *js, ant_value_t *args, int nargs) { js_set(js, stream_obj, "writableEnded", js_true); js_set(js, stream_writable_state(js, stream_obj), "ended", js_true); + priv = stream_private_state(stream_obj); - done_state = js_mkobj(js); - js_set(js, done_state, "stream", stream_obj); - js_set(js, done_state, "callback", callback); - done = stream_make_once(js, js_heavy_mkfun(js, stream_writable_end_done, done_state), js_mkundef()); + if (!is_undefined(chunk) && !is_null(chunk)) { + after_write_state = js_mkobj(js); + js_set(js, after_write_state, "stream", stream_obj); + js_set(js, after_write_state, "callback", callback); + + after_write = stream_make_once( + js, js_heavy_mkfun(js, stream_writable_end_after_write, after_write_state), + js_mkundef() + ); + + stream_writable_write_impl(js, stream_obj, chunk, encoding, after_write, true); + return stream_obj; + } - final_fn = js_get(js, stream_obj, "_final"); - final_args[0] = done; - if (is_callable(final_fn)) stream_call(js, final_fn, stream_obj, final_args, 1, false); - else stream_call_callback(js, done, NULL, 0); + if (priv && priv->writing && !priv->final_started) { + priv->pending_final = true; + stream_set_end_callback(js, stream_obj, callback); + return stream_obj; + } - return stream_obj; + return stream_writable_begin_end(js, stream_obj, callback); } static ant_value_t js_transform__transform(ant_t *js, ant_value_t *args, int nargs) { @@ -977,14 +1127,14 @@ static ant_value_t stream_transform_write_callback(ant_t *js, ant_value_t *args, ant_value_t outer_callback = js_get(js, state_obj, "callback"); if (nargs > 0 && !is_null(args[0]) && !is_undefined(args[0])) { - stream_call_callback(js, outer_callback, &args[0], 1); + if (is_callable(outer_callback)) stream_call(js, outer_callback, stream_obj, &args[0], 1, false); return js_mkundef(); } if (nargs > 1 && !is_null(args[1]) && !is_undefined(args[1])) stream_readable_push_value(js, stream_obj, args[1], js_mkundef()); - stream_call_callback(js, outer_callback, NULL, 0); + if (is_callable(outer_callback)) stream_call(js, outer_callback, stream_obj, NULL, 0, false); return js_mkundef(); } @@ -996,7 +1146,7 @@ static ant_value_t js_transform__write(ant_t *js, ant_value_t *args, int nargs) ant_value_t call_args[3]; if (is_err(stream_obj)) return stream_obj; - transform_fn = js_get(js, stream_obj, "_transform"); + transform_fn = js_getprop_fallback(js, stream_obj, "_transform"); cb_state = js_mkobj(js); js_set(js, cb_state, "stream", stream_obj); @@ -1016,14 +1166,15 @@ static ant_value_t stream_transform_final_callback(ant_t *js, ant_value_t *args, ant_value_t callback = js_get(js, state_obj, "callback"); if (nargs > 0 && !is_null(args[0]) && !is_undefined(args[0])) { - stream_call_callback(js, callback, &args[0], 1); + if (is_callable(callback)) stream_call(js, callback, stream_obj, &args[0], 1, false); return js_mkundef(); } if (nargs > 1 && !is_null(args[1]) && !is_undefined(args[1])) stream_readable_push_value(js, stream_obj, args[1], js_mkundef()); stream_readable_push_value(js, stream_obj, js_mknull(), js_mkundef()); - stream_call_callback(js, callback, NULL, 0); + if (is_callable(callback)) stream_call(js, callback, stream_obj, NULL, 0, false); + return js_mkundef(); } @@ -1035,7 +1186,8 @@ static ant_value_t js_transform__final(ant_t *js, ant_value_t *args, int nargs) ant_value_t call_args[1]; if (is_err(stream_obj)) return stream_obj; - flush_fn = js_get(js, stream_obj, "_flush"); + flush_fn = js_getprop_fallback(js, stream_obj, "_flush"); + if (!is_callable(flush_fn)) { stream_readable_push_value(js, stream_obj, js_mknull(), js_mkundef()); stream_call_callback(js, nargs > 0 ? args[0] : js_mkundef(), NULL, 0); diff --git a/src/modules/timer.c b/src/modules/timer.c index 615f879..23d584e 100644 --- a/src/modules/timer.c +++ b/src/modules/timer.c @@ -48,6 +48,9 @@ static struct { ant_t *js; timer_entry_t *timers; + microtask_entry_t *next_ticks; + microtask_entry_t *next_ticks_tail; + microtask_entry_t *next_ticks_processing; microtask_entry_t *microtasks; microtask_entry_t *microtasks_tail; microtask_entry_t *microtasks_processing; @@ -57,7 +60,21 @@ static struct { int next_timer_id; int next_immediate_id; int active_timer_count; -} timer_state = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, 1, 0}; +} timer_state = { + .js = NULL, + .timers = NULL, + .next_ticks = NULL, + .next_ticks_tail = NULL, + .next_ticks_processing = NULL, + .microtasks = NULL, + .microtasks_tail = NULL, + .microtasks_processing = NULL, + .immediates = NULL, + .immediates_tail = NULL, + .next_timer_id = 1, + .next_immediate_id = 1, + .active_timer_count = 0, +}; static void add_timer_entry(timer_entry_t *entry) { entry->next = timer_state.timers; @@ -515,14 +532,21 @@ static ant_value_t js_timers_promises_scheduler_yield(ant_t *js, ant_value_t *ar return js_timers_promises_setImmediate(js, args, nargs); } -static void queue_microtask_entry(microtask_entry_t *entry) { -if (timer_state.microtasks_tail == NULL) { - timer_state.microtasks = entry; - timer_state.microtasks_tail = entry; -} else { - timer_state.microtasks_tail->next = entry; - timer_state.microtasks_tail = entry; -}} +static void queue_microtask_entry( + microtask_entry_t **head, + microtask_entry_t **tail, + microtask_entry_t *entry +) { + if (*tail == NULL) goto empty; + + (*tail)->next = entry; + *tail = entry; + return; + +empty: + *head = entry; + *tail = entry; +} void queue_microtask(ant_t *js, ant_value_t callback) { microtask_entry_t *entry = ant_calloc(sizeof(microtask_entry_t)); @@ -533,7 +557,7 @@ void queue_microtask(ant_t *js, ant_value_t callback) { entry->next = NULL; entry->argc = 0; - queue_microtask_entry(entry); + queue_microtask_entry(&timer_state.microtasks, &timer_state.microtasks_tail, entry); } void queue_microtask_with_args(ant_t *js, ant_value_t callback, ant_value_t *args, int nargs) { @@ -548,7 +572,34 @@ void queue_microtask_with_args(ant_t *js, ant_value_t callback, ant_value_t *arg entry->argc = (uint8_t)nargs; for (int i = 0; i < nargs; i++) entry->argv[i] = args[i]; - queue_microtask_entry(entry); + queue_microtask_entry(&timer_state.microtasks, &timer_state.microtasks_tail, entry); +} + +void queue_next_tick(ant_t *js, ant_value_t callback) { + microtask_entry_t *entry = ant_calloc(sizeof(microtask_entry_t)); + if (entry == NULL) return; + + entry->callback = callback; + entry->promise = js_mkundef(); + entry->next = NULL; + entry->argc = 0; + + queue_microtask_entry(&timer_state.next_ticks, &timer_state.next_ticks_tail, entry); +} + +void queue_next_tick_with_args(ant_t *js, ant_value_t callback, ant_value_t *args, int nargs) { + if (nargs <= 0) { queue_next_tick(js, callback); return; } + + microtask_entry_t *entry = ant_calloc(sizeof(microtask_entry_t) + (size_t)nargs * sizeof(ant_value_t)); + if (entry == NULL) return; + + entry->callback = callback; + entry->promise = js_mkundef(); + entry->next = NULL; + entry->argc = (uint8_t)nargs; + + for (int i = 0; i < nargs; i++) entry->argv[i] = args[i]; + queue_microtask_entry(&timer_state.next_ticks, &timer_state.next_ticks_tail, entry); } void queue_promise_trigger(ant_t *js, ant_value_t promise) { @@ -564,13 +615,7 @@ void queue_promise_trigger(ant_t *js, ant_value_t promise) { entry->promise = promise; entry->next = NULL; - if (timer_state.microtasks_tail == NULL) { - timer_state.microtasks = entry; - timer_state.microtasks_tail = entry; - } else { - timer_state.microtasks_tail->next = entry; - timer_state.microtasks_tail = entry; - } + queue_microtask_entry(&timer_state.microtasks, &timer_state.microtasks_tail, entry); } static inline void process_microtask_entry(ant_t *js, microtask_entry_t *entry) { @@ -592,6 +637,7 @@ static inline void process_microtask_entry(ant_t *js, microtask_entry_t *entry) ant_value_t callback = entry->callback; GC_ROOT_PIN(js, callback); + for (uint8_t i = 0; i < entry->argc; i++) GC_ROOT_PIN(js, entry->argv[i]); sv_vm_call(js->vm, js, callback, js_mkundef(), entry->argv, entry->argc, NULL, false); GC_ROOT_RESTORE(js, root_mark); } @@ -602,6 +648,17 @@ static inline microtask_entry_t *take_microtask_batch(void) { timer_state.microtasks = NULL; timer_state.microtasks_tail = NULL; timer_state.microtasks_processing = batch; + + return batch; +} + +static inline microtask_entry_t *take_next_tick_batch(void) { + microtask_entry_t *batch = timer_state.next_ticks; + + timer_state.next_ticks = NULL; + timer_state.next_ticks_tail = NULL; + timer_state.next_ticks_processing = batch; + return batch; } @@ -614,20 +671,36 @@ while (batch != NULL) { free(entry); }} +static inline void process_next_tick_batch(ant_t *js, microtask_entry_t *batch) { +while (batch != NULL) { + microtask_entry_t *entry = batch; + batch = entry->next; + timer_state.next_ticks_processing = batch; + process_microtask_entry(js, entry); + free(entry); +}} + static void process_microtasks_internal(ant_t *js, bool check_unhandled_rejections) { microtask_entry_t *batch = NULL; if (!js || js->microtasks_draining) return; js->microtasks_draining = true; + while (timer_state.next_ticks != NULL || timer_state.microtasks != NULL) { + while ((batch = timer_state.next_ticks) != NULL) { + batch = take_next_tick_batch(); + process_next_tick_batch(js, batch); + } while ((batch = timer_state.microtasks) != NULL) { batch = take_microtask_batch(); process_microtask_batch(js, batch); - } + }} + timer_state.next_ticks_processing = NULL; timer_state.microtasks_processing = NULL; if (check_unhandled_rejections) js_check_unhandled_rejections(js); js->microtasks_draining = false; + reap_retired_coroutines(); } void process_microtasks(ant_t *js) { @@ -684,7 +757,7 @@ int has_pending_timers(void) { } int has_pending_microtasks(void) { - return timer_state.microtasks != NULL ? 1 : 0; + return (timer_state.next_ticks != NULL || timer_state.microtasks != NULL) ? 1 : 0; } static void timers_define_common(ant_t *js, ant_value_t obj) { @@ -736,10 +809,22 @@ void gc_mark_timers(ant_t *js, gc_mark_fn mark) { for (microtask_entry_t *m = timer_state.microtasks; m; m = m->next) { mark(js, m->callback); mark(js, m->promise); + for (uint8_t i = 0; i < m->argc; i++) mark(js, m->argv[i]); } for (microtask_entry_t *m = timer_state.microtasks_processing; m; m = m->next) { mark(js, m->callback); mark(js, m->promise); + for (uint8_t i = 0; i < m->argc; i++) mark(js, m->argv[i]); + } + for (microtask_entry_t *m = timer_state.next_ticks; m; m = m->next) { + mark(js, m->callback); + mark(js, m->promise); + for (uint8_t i = 0; i < m->argc; i++) mark(js, m->argv[i]); + } + for (microtask_entry_t *m = timer_state.next_ticks_processing; m; m = m->next) { + mark(js, m->callback); + mark(js, m->promise); + for (uint8_t i = 0; i < m->argc; i++) mark(js, m->argv[i]); } for (immediate_entry_t *i = timer_state.immediates; i; i = i->next) { mark(js, i->callback); diff --git a/src/modules/tty.c b/src/modules/tty.c index 69910ab..47e95b2 100644 --- a/src/modules/tty.c +++ b/src/modules/tty.c @@ -38,6 +38,8 @@ #include "silver/engine.h" #include "modules/stream.h" +#include "modules/buffer.h" +#include "modules/events.h" #include "modules/symbol.h" #include "modules/tty.h" @@ -46,6 +48,23 @@ static ant_value_t g_tty_readstream_ctor = 0; static ant_value_t g_tty_writestream_proto = 0; static ant_value_t g_tty_writestream_ctor = 0; +typedef struct tty_read_stream_state { + ant_t *js; + ant_value_t stream_obj; + uv_tty_t tty; + + int fd; + bool initialized; + bool reading; + bool closing; +} tty_read_stream_state_t; + +static void invoke_callback_if_needed(ant_t *js, ant_value_t cb, ant_value_t arg) { + if (!is_callable(cb)) return; + ant_value_t cb_args[1] = { arg }; + sv_vm_call(js->vm, js, cb, js_mkundef(), cb_args, 1, NULL, false); +} + static bool parse_fd(ant_value_t value, int *fd_out) { int fd = 0; if (!tty_ctrl_parse_int_value(value, &fd)) return false; @@ -69,6 +88,156 @@ static int stream_fd_from_this(ant_t *js, int fallback_fd) { return fd; } +static tty_read_stream_state_t *tty_read_stream_state_from_obj(ant_value_t stream_obj) { + return (tty_read_stream_state_t *)stream_get_attached_state(stream_obj); +} + +static ant_value_t make_stream_error(ant_t *js, const char *op, int fd, int uv_code) { + if (uv_code != 0) return js_mkerr_typed( + js, JS_ERR_GENERIC, + "tty stream %s failed for fd %d: %s", + op, fd, uv_strerror(uv_code) + ); + + return js_mkerr_typed(js, JS_ERR_GENERIC, "tty stream %s failed for fd %d", op, fd); +} + +static void tty_read_stream_emit_error(tty_read_stream_state_t *state, const char *op, int uv_code) { + ant_value_t err = 0; + if (!state || !state->js || !is_object_type(state->stream_obj)) return; + err = make_stream_error(state->js, op, state->fd, uv_code); + eventemitter_emit_args(state->js, state->stream_obj, "error", &err, 1); +} + +static void tty_read_stream_push_chunk(tty_read_stream_state_t *state, const char *data, size_t len) { + ArrayBufferData *ab = NULL; + ant_value_t chunk = 0; + + if (!state || !state->js || !data || len == 0) return; + ab = create_array_buffer_data(len); + if (ab) memcpy(ab->data, data, len); + + chunk = ab + ? create_typed_array(state->js, TYPED_ARRAY_UINT8, ab, 0, len, "Buffer") + : js_mkstr(state->js, data, len); + (void)stream_readable_push(state->js, state->stream_obj, chunk, js_mkundef()); +} + +static void tty_read_stream_alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { + buf->base = malloc(suggested_size); +#ifdef _WIN32 + buf->len = (ULONG)suggested_size; +#else + buf->len = suggested_size; +#endif +} + +static void tty_read_stream_stop(tty_read_stream_state_t *state) { + if (!state || !state->initialized || !state->reading) return; + uv_read_stop((uv_stream_t *)&state->tty); + state->reading = false; +} + +static void tty_read_stream_close_cb(uv_handle_t *handle) { + tty_read_stream_state_t *state = (tty_read_stream_state_t *)handle->data; + free(state); +} + +static void tty_read_stream_finalize(ant_t *js, ant_value_t stream_obj, void *data) { + tty_read_stream_state_t *state = (tty_read_stream_state_t *)data; + + if (!state) return; + tty_read_stream_stop(state); + + if (state->initialized && !state->closing) { + state->closing = true; + uv_close((uv_handle_t *)&state->tty, tty_read_stream_close_cb); + return; + } + + if (!state->initialized) free(state); +} + +static void tty_read_stream_on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + tty_read_stream_state_t *state = (tty_read_stream_state_t *)stream->data; + + if (!state || !state->js) goto cleanup; + + if (nread > 0) { + tty_read_stream_push_chunk(state, buf->base, (size_t)nread); + goto cleanup; + } + + if (nread == UV_EOF) { + tty_read_stream_stop(state); + (void)stream_readable_push(state->js, state->stream_obj, js_mknull(), js_mkundef()); + goto cleanup; + } + + if (nread < 0) { + tty_read_stream_stop(state); + tty_read_stream_emit_error(state, "read", (int)nread); + } + +cleanup: + if (buf->base) free(buf->base); +} + +static ant_value_t tty_readstream__read(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + tty_read_stream_state_t *state = tty_read_stream_state_from_obj(stream_obj); + int rc = 0; + + if (!state) return js_mkundef(); + if (state->closing || js_truthy(js, js_get(js, stream_obj, "destroyed"))) return js_mkundef(); + + if (!state->initialized) { + rc = uv_tty_init(uv_default_loop(), &state->tty, state->fd, 0); + if (rc != 0) { + tty_read_stream_emit_error(state, "open", rc); + return js_mkundef(); + } + #ifndef _WIN32 + uv_tty_set_mode(&state->tty, tty_is_raw_mode(state->fd) ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); + #endif + state->tty.data = state; + state->initialized = true; + } + + if (state->reading) return js_mkundef(); + rc = uv_read_start((uv_stream_t *)&state->tty, tty_read_stream_alloc_buffer, tty_read_stream_on_read); + + if (rc != 0) { + tty_read_stream_emit_error(state, "read", rc); + return js_mkundef(); + } + + state->reading = true; + return js_mkundef(); +} + +static ant_value_t tty_readstream__destroy(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + tty_read_stream_state_t *state = tty_read_stream_state_from_obj(stream_obj); + ant_value_t cb = nargs > 1 ? args[1] : js_mkundef(); + + if (!state) { + invoke_callback_if_needed(js, cb, js_mknull()); + return js_mkundef(); + } + + tty_read_stream_stop(state); + stream_clear_attached_state(stream_obj); + + if (state->initialized && !state->closing) { + state->closing = true; + uv_close((uv_handle_t *)&state->tty, tty_read_stream_close_cb); + } else if (!state->initialized) free(state); + + invoke_callback_if_needed(js, cb, js_mknull()); + return js_mkundef(); +} + static void get_tty_size(int fd, int *rows, int *cols) { int out_rows = 24; int out_cols = 80; @@ -229,34 +398,20 @@ static int palette_size_for_depth(int depth) { } } -static ant_value_t make_stream_error(ant_t *js, const char *op, int fd) { - return js_mkerr_typed(js, JS_ERR_GENERIC, "tty stream %s failed for fd %d", op, fd); -} - -static void invoke_callback_if_needed(ant_t *js, ant_value_t cb, ant_value_t arg) { - if (!is_callable(cb)) return; - ant_value_t cb_args[1] = { arg }; - sv_vm_call(js->vm, js, cb, js_mkundef(), cb_args, 1, NULL, false); -} - static ant_value_t maybe_callback_or_throw( - ant_t *js, - ant_value_t this_obj, - ant_value_t cb, - bool ok, - const char *op, - int fd + ant_t *js, ant_value_t this_obj, + ant_value_t cb, bool ok, + const char *op, int fd ) { if (is_callable(cb)) { - if (ok) { - invoke_callback_if_needed(js, cb, js_mknull()); - } else { - ant_value_t err = make_stream_error(js, op, fd); + if (ok) invoke_callback_if_needed(js, cb, js_mknull()); + else { + ant_value_t err = make_stream_error(js, op, fd, 0); invoke_callback_if_needed(js, cb, err); } return this_obj; } - if (!ok) return make_stream_error(js, op, fd); + if (!ok) return make_stream_error(js, op, fd, 0); return this_obj; } @@ -368,7 +523,6 @@ static ant_value_t tty_isatty(ant_t *js, ant_value_t *args, int nargs) { } static ant_value_t tty_stream_write(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t this_obj = js_getthis(js); if (nargs < 1) return js_false; size_t len = 0; @@ -383,17 +537,14 @@ static ant_value_t tty_stream_write(ant_t *js, ant_value_t *args, int nargs) { if (is_callable(cb)) { if (ok) invoke_callback_if_needed(js, cb, js_mknull()); - else invoke_callback_if_needed(js, cb, make_stream_error(js, "write", fd)); + else invoke_callback_if_needed(js, cb, make_stream_error(js, "write", fd, 0)); } if (!ok) return js_false; - (void)this_obj; return js_true; } static ant_value_t tty_write_stream_rows_getter(ant_t *js, ant_value_t *args, int nargs) { - (void)args; (void)nargs; - int fd = stream_fd_from_this(js, ANT_STDOUT_FD); if (!is_tty_fd(fd)) return js_mkundef(); @@ -404,8 +555,6 @@ static ant_value_t tty_write_stream_rows_getter(ant_t *js, ant_value_t *args, in } static ant_value_t tty_write_stream_columns_getter(ant_t *js, ant_value_t *args, int nargs) { - (void)args; (void)nargs; - int fd = stream_fd_from_this(js, ANT_STDOUT_FD); if (!is_tty_fd(fd)) return js_mkundef(); @@ -416,8 +565,6 @@ static ant_value_t tty_write_stream_columns_getter(ant_t *js, ant_value_t *args, } static ant_value_t tty_write_stream_get_window_size(ant_t *js, ant_value_t *args, int nargs) { - (void)args; (void)nargs; - int fd = stream_fd_from_this(js, ANT_STDOUT_FD); int rows = 0; int cols = 0; @@ -442,8 +589,10 @@ static ant_value_t tty_write_stream_clear_line(ant_t *js, ant_value_t *args, int int fd = stream_fd_from_this(js, ANT_STDOUT_FD); size_t seq_len = 0; + const char *seq = tty_ctrl_clear_line_seq(dir, &seq_len); bool ok = tty_ctrl_write_fd(fd, seq, seq_len); + return maybe_callback_or_throw(js, this_obj, cb, ok, "clearLine", fd); } @@ -455,14 +604,17 @@ static ant_value_t tty_write_stream_clear_screen_down(ant_t *js, ant_value_t *ar int fd = stream_fd_from_this(js, ANT_STDOUT_FD); size_t seq_len = 0; + const char *seq = tty_ctrl_clear_screen_down_seq(&seq_len); bool ok = tty_ctrl_write_fd(fd, seq, seq_len); + return maybe_callback_or_throw(js, this_obj, cb, ok, "clearScreenDown", fd); } static ant_value_t tty_write_stream_cursor_to(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = js_getthis(js); int x = 0; + if (nargs < 1 || !tty_ctrl_parse_int_value(args[0], &x)) { return js_mkerr_typed(js, JS_ERR_TYPE, "cursorTo(x[, y][, callback]) requires numeric x"); } @@ -575,6 +727,7 @@ static ant_value_t tty_write_stream_has_colors(ant_t *js, ant_value_t *args, int static ant_value_t tty_read_stream_set_raw_mode(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = js_getthis(js); + tty_read_stream_state_t *state = tty_read_stream_state_from_obj(this_obj); if (!is_special_object(this_obj)) { return js_mkerr_typed(js, JS_ERR_TYPE, "setRawMode() requires a ReadStream receiver"); } @@ -584,11 +737,17 @@ static ant_value_t tty_read_stream_set_raw_mode(ant_t *js, ant_value_t *args, in if (!tty_set_raw_mode(fd, enable)) { return js_mkerr_typed(js, JS_ERR_GENERIC, "Failed to set raw mode for fd %d", fd); } +#ifndef _WIN32 + if (state && state->initialized && !state->closing) { + uv_tty_set_mode(&state->tty, enable ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); + } +#endif js_set(js, this_obj, "isRaw", js_bool(enable)); return this_obj; } static ant_value_t tty_read_stream_constructor(ant_t *js, ant_value_t *args, int nargs) { + tty_read_stream_state_t *state = NULL; if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream(fd) requires a file descriptor"); int fd = 0; @@ -600,19 +759,29 @@ static ant_value_t tty_read_stream_constructor(ant_t *js, ant_value_t *args, int } if (fd == ANT_STDIN_FD) { - ant_value_t stdin_obj = get_process_stream(js, "stdin"); - if (is_special_object(stdin_obj)) { - ensure_stream_common_props(js, stdin_obj, fd); - if (vtype(js_get(js, stdin_obj, "isRaw")) == T_UNDEF) js_set(js, stdin_obj, "isRaw", js_false); - return stdin_obj; - } - } + ant_value_t stdin_obj = get_process_stream(js, "stdin"); + if (is_special_object(stdin_obj)) { + ensure_stream_common_props(js, stdin_obj, fd); + if (vtype(js_get(js, stdin_obj, "isRaw")) == T_UNDEF) js_set(js, stdin_obj, "isRaw", js_false); + return stdin_obj; + }} ant_value_t obj = stream_construct_readable(js, g_tty_readstream_proto, js_mkundef()); if (is_err(obj)) return obj; + state = calloc(1, sizeof(*state)); + if (!state) return js_mkerr(js, "Out of memory"); + state->js = js; + state->stream_obj = obj; + state->fd = fd; + ensure_stream_common_props(js, obj, fd); + stream_set_attached_state(obj, state, tty_read_stream_finalize); + js_set(js, obj, "isRaw", js_false); + js_set(js, obj, "_read", js_mkfun(tty_readstream__read)); + js_set(js, obj, "_destroy", js_mkfun(tty_readstream__destroy)); + return obj; } diff --git a/src/modules/util.c b/src/modules/util.c index 9281d42..db134f4 100644 --- a/src/modules/util.c +++ b/src/modules/util.c @@ -1269,6 +1269,10 @@ static ant_value_t util_promisify(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1 || !is_callable(args[0])) { return js_mkerr(js, "promisify(fn) requires a function"); } + + ant_value_t custom = js_get_symbol(js, args[0], "nodejs.util.promisify.custom"); + if (is_callable(custom)) return custom; + return js_heavy_mkfun(js, util_promisified_call, args[0]); } @@ -1324,9 +1328,12 @@ static ant_value_t util_is_deep_strict_equal(ant_t *js, ant_value_t *args, int n return js_bool(js_deep_equal(js, args[0], args[1], true)); } -ant_value_t util_library(ant_t *js) { +ant_value_t util_library(ant_t *js) { ant_value_t lib = js_mkobj(js); ant_value_t types = util_get_types_object(js); + + ant_value_t promisify = js_heavy_mkfun(js, util_promisify, js_mkundef()); + js_set(js, promisify, "custom", js_mksym_for(js, "nodejs.util.promisify.custom")); js_set(js, lib, "format", js_mkfun(util_format)); js_set(js, lib, "formatWithOptions", js_mkfun(util_format_with_options)); @@ -1337,7 +1344,7 @@ ant_value_t util_library(ant_t *js) { js_set(js, lib, "isDeepStrictEqual", js_mkfun(util_is_deep_strict_equal)); js_set(js, lib, "parseArgs", js_mkfun(util_parse_args)); js_set(js, lib, "parseEnv", js_mkfun(util_parse_env)); - js_set(js, lib, "promisify", js_mkfun(util_promisify)); + js_set(js, lib, "promisify", promisify); js_set(js, lib, "callbackify", js_mkfun(util_callbackify)); js_set(js, lib, "aborted", js_mkfun(util_aborted)); js_set(js, lib, "stripVTControlCharacters", js_mkfun(util_strip_vt_control_characters)); diff --git a/src/reactor.c b/src/reactor.c index abd5b0f..feac718 100644 --- a/src/reactor.c +++ b/src/reactor.c @@ -72,6 +72,7 @@ void js_run_event_loop(ant_t *js) { drain: while (event_loop_alive()) { js_poll_events(js); + reap_retired_coroutines(); work_flags_t work = get_pending_work(); if (work & WORK_BLOCKING) uv_run(uv_default_loop(), UV_RUN_NOWAIT); @@ -81,6 +82,7 @@ drain: } js_poll_events(js); + reap_retired_coroutines(); ant_value_t code = js_mknum(0); emit_process_event("beforeExit", &code, 1); diff --git a/src/silver/ops/async.h b/src/silver/ops/async.h index f005cc1..44082e8 100644 --- a/src/silver/ops/async.h +++ b/src/silver/ops/async.h @@ -68,8 +68,7 @@ static void sv_mco_async_entry(mco_coro *mco) { ant_value_t promise = ctx->coro->async_promise; if (is_err(result)) { - ant_value_t reject_value = js->thrown_value; - if (vtype(reject_value) == T_UNDEF) reject_value = result; + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; js->thrown_exists = false; js->thrown_value = js_mkundef(); js_reject_promise(js, promise, reject_value); @@ -98,8 +97,7 @@ static void sv_mco_tla_entry(mco_coro *mco) { ant_value_t promise = ctx->coro->async_promise; if (is_err(result)) { - ant_value_t reject_value = js->thrown_value; - if (vtype(reject_value) == T_UNDEF) reject_value = result; + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; js->thrown_exists = false; js->thrown_value = js_mkundef(); js_reject_promise(js, promise, reject_value); @@ -180,15 +178,17 @@ static inline ant_value_t sv_start_tla(ant_t *js, sv_func_t *func, ant_value_t t } if (is_err(result)) { - ant_value_t reject_value = js->thrown_value; - if (vtype(reject_value) == T_UNDEF) reject_value = result; + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; js->thrown_exists = false; js->thrown_value = js_mkundef(); + js->active_async_coro = coro->active_parent; + coro->active_parent = NULL; js_reject_promise(js, promise, reject_value); - } else js_resolve_promise(js, promise, result); - - js->active_async_coro = coro->active_parent; - coro->active_parent = NULL; + } else { + js->active_async_coro = coro->active_parent; + coro->active_parent = NULL; + js_resolve_promise(js, promise, result); + } free_coroutine(coro); GC_ROOT_RESTORE(js, root_mark); @@ -306,8 +306,7 @@ static inline ant_value_t sv_start_async_closure( ); if (is_err(result)) { - ant_value_t reject_value = js->thrown_value; - if (vtype(reject_value) == T_UNDEF) reject_value = result; + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; js->thrown_exists = false; js->thrown_value = js_mkundef(); js_reject_promise(js, promise, reject_value); @@ -385,15 +384,17 @@ static inline ant_value_t sv_start_async_closure( } if (is_err(result)) { - ant_value_t reject_value = js->thrown_value; - if (vtype(reject_value) == T_UNDEF) reject_value = result; + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; js->thrown_exists = false; js->thrown_value = js_mkundef(); + js->active_async_coro = coro->active_parent; + coro->active_parent = NULL; js_reject_promise(js, promise, reject_value); - } else js_resolve_promise(js, promise, result); - - js->active_async_coro = coro->active_parent; - coro->active_parent = NULL; + } else { + js->active_async_coro = coro->active_parent; + coro->active_parent = NULL; + js_resolve_promise(js, promise, result); + } free_coroutine(coro); GC_ROOT_RESTORE(js, root_mark); diff --git a/src/silver/swarm.c b/src/silver/swarm.c index 0afad95..d5e86a0 100644 --- a/src/silver/swarm.c +++ b/src/silver/swarm.c @@ -515,6 +515,61 @@ static void mir_emit_get_closure(MIR_context_t ctx, MIR_item_t fn, MIR_new_uint_op(ctx, NANBOX_DATA_MASK))); } +static void mir_emit_resolve_call_this(MIR_context_t ctx, MIR_item_t fn, + MIR_reg_t dst, MIR_reg_t r_closure, + MIR_reg_t fallback_this, + MIR_reg_t r_flags, MIR_reg_t r_bound) { + MIR_label_t not_arrow = MIR_new_label(ctx); + MIR_label_t done = MIR_new_label(ctx); + + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, r_flags), + MIR_new_mem_op(ctx, MIR_T_U32, + (MIR_disp_t)offsetof(sv_closure_t, call_flags), + r_closure, 0, 1))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_AND, + MIR_new_reg_op(ctx, r_flags), + MIR_new_reg_op(ctx, r_flags), + MIR_new_uint_op(ctx, SV_CALL_IS_ARROW))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_BEQ, + MIR_new_label_op(ctx, not_arrow), + MIR_new_reg_op(ctx, r_flags), + MIR_new_uint_op(ctx, 0))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, dst), + MIR_new_mem_op(ctx, MIR_JSVAL, + (MIR_disp_t)offsetof(sv_closure_t, bound_this), + r_closure, 0, 1))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_JMP, MIR_new_label_op(ctx, done))); + + MIR_append_insn(ctx, fn, not_arrow); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, dst), + MIR_new_reg_op(ctx, fallback_this))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, r_bound), + MIR_new_mem_op(ctx, MIR_JSVAL, + (MIR_disp_t)offsetof(sv_closure_t, bound_this), + r_closure, 0, 1))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_BEQ, + MIR_new_label_op(ctx, done), + MIR_new_reg_op(ctx, r_bound), + MIR_new_uint_op(ctx, mkval(T_UNDEF, 0)))); + MIR_append_insn(ctx, fn, + MIR_new_insn(ctx, MIR_MOV, + MIR_new_reg_op(ctx, dst), + MIR_new_reg_op(ctx, r_bound))); + MIR_append_insn(ctx, fn, done); +} + static void mir_emit_value_to_objptr_or_jmp( MIR_context_t ctx, MIR_item_t fn, MIR_reg_t v, MIR_reg_t out_ptr, @@ -4526,6 +4581,16 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos MIR_label_t inl_join = MIR_new_label(ctx); MIR_reg_t r_inl_cl = 0; + char inl_this_rn[32], inl_flags_rn[32], inl_bound_rn[32]; + snprintf(inl_this_rn, sizeof(inl_this_rn), "inl%d_this", cn); + snprintf(inl_flags_rn, sizeof(inl_flags_rn), "inl%d_flags", cn); + snprintf(inl_bound_rn, sizeof(inl_bound_rn), "inl%d_bound", cn); + MIR_reg_t r_inl_this = MIR_new_func_reg(ctx, jit_func->u.func, + MIR_JSVAL, inl_this_rn); + MIR_reg_t r_inl_flags = MIR_new_func_reg(ctx, jit_func->u.func, + MIR_T_I64, inl_flags_rn); + MIR_reg_t r_inl_bound = MIR_new_func_reg(ctx, jit_func->u.func, + MIR_JSVAL, inl_bound_rn); { char cl_rn[32]; snprintf(cl_rn, sizeof(cl_rn), "inl%d_cl", cn); r_inl_cl = MIR_new_func_reg(ctx, jit_func->u.func, MIR_T_I64, cl_rn); @@ -4567,12 +4632,15 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos MIR_new_uint_op(ctx, (uint64_t)(uintptr_t)inline_callee))); } + mir_emit_resolve_call_this(ctx, jit_func, r_inl_this, r_inl_cl, + r_this, r_inl_flags, r_inl_bound); + bool inlined = jit_emit_inline_body( ctx, jit_func, inline_callee, inl_arg_regs, (int)call_argc, r_call_res, inl_slow, inl_join, r_bool, &r_d_slot, cn, - r_inl_cl, r_this, + r_inl_cl, r_inl_this, r_vm, r_js, helper2_proto, imp_seq, imp_sne, imp_eq, imp_ne, gf_proto, imp_get_field, @@ -4729,6 +4797,8 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos MIR_new_mem_op(ctx, MIR_T_P, (MIR_disp_t)offsetof(sv_closure_t, func), r_callee_cl, 0, 1))); + mir_emit_resolve_call_this(ctx, jit_func, r_call_this, r_callee_cl, + r_call_this, r_bool, r_tmp2); MIR_append_insn(ctx, jit_func, MIR_new_insn(ctx, MIR_BEQ, @@ -7054,6 +7124,8 @@ sv_jit_func_t sv_jit_compile(ant_t *js, sv_func_t *func, sv_closure_t *hint_clos MIR_new_mem_op(ctx, MIR_T_P, (MIR_disp_t)offsetof(sv_closure_t, func), r_callee_cl, 0, 1))); + mir_emit_resolve_call_this(ctx, jit_func, r_call_this, r_callee_cl, + r_call_this, r_bool, r_tmp2); MIR_append_insn(ctx, jit_func, MIR_new_insn(ctx, MIR_BEQ, diff --git a/src/sugar.c b/src/sugar.c index 93a83c9..a363ff6 100644 --- a/src/sugar.c +++ b/src/sugar.c @@ -1,8 +1,9 @@ #include "internal.h" #include "sugar.h" + +#include "modules/generator.h" #include "modules/timer.h" #include "silver/engine.h" -#include "silver/vm.h" #define MCO_USE_VMEM_ALLOCATOR #define MCO_ZERO_MEMORY @@ -12,7 +13,9 @@ uint32_t coros_this_tick = 0; coroutine_queue_t pending_coroutines = {NULL, NULL}; + static bool coro_stack_size_initialized = false; +static coroutine_t *retired_coroutines = NULL; bool has_pending_coroutines(void) { return pending_coroutines.head != NULL; @@ -53,8 +56,70 @@ void remove_coroutine(coroutine_t *coro) { coro->next = NULL; } -void free_coroutine(coroutine_t *coro) { +static void clear_await_coro_from_promise_state(ant_promise_state_t *pd, coroutine_t *coro) { + if (!pd || !coro || pd->handler_count == 0) return; + + if (pd->handler_count == 1) { + if (pd->inline_handler.await_coro == coro) pd->inline_handler.await_coro = NULL; + return; + } + + if (!pd->handlers) return; + promise_handler_t *h = NULL; + + while ((h = (promise_handler_t *)utarray_next(pd->handlers, h))) + if (h->await_coro == coro) h->await_coro = NULL; +} + +static void clear_await_coro_from_object_list(ant_object_t *head, coroutine_t *coro) { + for (ant_object_t *obj = head; obj; obj = obj->next) + if (obj->promise_state) clear_await_coro_from_promise_state(obj->promise_state, coro); +} + +static inline bool coroutine_is_queued(coroutine_t *coro) { + return coro && ( + coro->prev || coro->next || + pending_coroutines.head == coro || + pending_coroutines.tail == coro + ); +} + +static void retire_coroutine_storage(coroutine_t *coro) { if (!coro) return; + coro->next = retired_coroutines; + coro->prev = NULL; + retired_coroutines = coro; +} + +void reap_retired_coroutines(void) { + coroutine_t *coro = retired_coroutines; + retired_coroutines = NULL; + while (coro) { + coroutine_t *next = coro->next; + CORO_FREE(coro); + coro = next; + } +} + +void free_coroutine(coroutine_t *coro) { + if (!coro || coro->free_pending) return; + coro->free_pending = true; + + ant_t *js = coro->js; + if (js) { + clear_await_coro_from_object_list(js->objects, coro); + clear_await_coro_from_object_list(js->objects_old, coro); + clear_await_coro_from_object_list(js->permanent_objects, coro); + + if ( + coro->prev || coro->next || + pending_coroutines.head == coro || + pending_coroutines.tail == coro + ) remove_coroutine(coro); + + if (js->active_async_coro == coro) js->active_async_coro = coro->active_parent; + coro->active_parent = NULL; + } if (coro->mco) { void *ctx = mco_get_user_data(coro->mco); @@ -63,10 +128,19 @@ void free_coroutine(coroutine_t *coro) { coro->mco = NULL; } - if (coro->args) CORO_FREE(coro->args); - if (coro->sv_vm) { sv_vm_destroy(coro->sv_vm); coro->sv_vm = NULL; } + if (coro->args) { + CORO_FREE(coro->args); + coro->args = NULL; + } + + if (coro->sv_vm) { + sv_vm_destroy(coro->sv_vm); + coro->sv_vm = NULL; + } - CORO_FREE(coro); + coro->js = NULL; + coro->active_parent = NULL; + retire_coroutine_storage(coro); } static size_t calculate_coro_stack_size(void) { @@ -109,26 +183,27 @@ static void resume_coroutine_if_suspended(ant_t *js, coroutine_t *coro) { ant_value_t result = sv_resume_suspended(coro->sv_vm); coro->is_settled = false; - coro->awaited_promise = js_mkundef(); - if (coro->sv_vm->suspended) { js->active_async_coro = coro->active_parent; coro->active_parent = NULL; + if (generator_resume_pending_request(js, coro, result)) return; return; - } remove_coroutine(coro); + } + + js->active_async_coro = coro->active_parent; + coro->active_parent = NULL; + + if (generator_resume_pending_request(js, coro, result)) return; + if (coroutine_is_queued(coro)) remove_coroutine(coro); if (is_err(result)) { - ant_value_t reject_value = js->thrown_value; - if (vtype(reject_value) == T_UNDEF) reject_value = result; + ant_value_t reject_value = js->thrown_exists ? js->thrown_value : result; js->thrown_exists = false; js->thrown_value = js_mkundef(); js_reject_promise(js, coro->async_promise, reject_value); } else js_resolve_promise(js, coro->async_promise, result); js_maybe_drain_microtasks_after_async_settle(js); - js->active_async_coro = coro->active_parent; - - coro->active_parent = NULL; free_coroutine(coro); return; @@ -180,5 +255,6 @@ void settle_and_resume_coroutine(ant_t *js, coroutine_t *coro, ant_value_t value if (!coro) return; ant_value_t args[1] = { value }; settle_coroutine(coro, args, 1, is_error); + coro->awaited_promise = js_mkundef(); resume_coroutine_if_suspended(js, coro); } diff --git a/src/types/modules/child_process.d.ts b/src/types/modules/child_process.d.ts index 1f0b11d..54749a9 100644 --- a/src/types/modules/child_process.d.ts +++ b/src/types/modules/child_process.d.ts @@ -36,6 +36,11 @@ declare module 'child_process' { cwd?: string; } + interface ExecFileResult { + stdout: string; + stderr: string; + } + interface SpawnSyncOptions { input?: string; } @@ -46,6 +51,13 @@ declare module 'child_process' { function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess & Promise; function exec(command: string, options?: ExecOptions): Promise; + function execFile(file: string, args?: string[], options?: ExecOptions): ChildProcess; + function execFile( + file: string, + args: string[] | undefined, + options: ExecOptions | undefined, + callback: (err: Error | null, stdout: string, stderr: string) => void + ): ChildProcess; function execSync(command: string): string; function spawnSync(command: string, args?: string[], options?: SpawnSyncOptions): SpawnResult; function fork(modulePath: string, options?: ForkOptions): ChildProcess & Promise; diff --git a/src/types/modules/events.d.ts b/src/types/modules/events.d.ts index a2c5a88..481d4f4 100644 --- a/src/types/modules/events.d.ts +++ b/src/types/modules/events.d.ts @@ -16,6 +16,7 @@ declare module 'events' { emit(event: string, ...args: unknown[]): boolean; removeAllListeners(event?: string): this; listenerCount(event: string): number; + listeners(event: string): Array<(...args: unknown[]) => void>; eventNames(): string[]; } diff --git a/src/types/modules/fs.d.ts b/src/types/modules/fs.d.ts index d01385f..98c863e 100644 --- a/src/types/modules/fs.d.ts +++ b/src/types/modules/fs.d.ts @@ -199,6 +199,26 @@ declare module 'node:fs' { } declare module 'fs/promises' { + interface FileHandle { + readonly fd: number; + close(): Promise; + stat(): Promise; + sync(): Promise; + read( + buffer: ArrayBufferView, + offset?: number, + length?: number, + position?: number | null + ): Promise<{ bytesRead: number; buffer: ArrayBufferView }>; + write( + data: string | ArrayBufferView, + offsetOrOptions?: number | { offset?: number; length?: number; position?: number | null }, + length?: number, + position?: number | null + ): Promise<{ bytesWritten: number; buffer: string | ArrayBufferView }>; + writeFile(data: string | ArrayBufferView): Promise; + } + interface Stats { size: number; mode: number; @@ -227,7 +247,7 @@ declare module 'fs/promises' { function readFile(path: string, encoding: Encoding): Promise; function readFile(path: string): Promise; - function open(path: string, flags?: string, mode?: number): Promise; + function open(path: string, flags?: string, mode?: number): Promise; function close(fd: number): Promise; function writeFile(path: string, data: string | ArrayBufferView): Promise; function write(fd: number, data: string | ArrayBufferView, offset?: number, length?: number, position?: number | null): Promise; diff --git a/src/types/modules/util.d.ts b/src/types/modules/util.d.ts index e22707a..a70889e 100644 --- a/src/types/modules/util.d.ts +++ b/src/types/modules/util.d.ts @@ -67,6 +67,9 @@ declare module 'util' { }): { values: Record; positionals: string[] }; function parseEnv(content: string): Record; function promisify(fn: (...args: unknown[]) => unknown): (...args: unknown[]) => Promise; + namespace promisify { + const custom: symbol; + } function callbackify(fn: (...args: unknown[]) => Promise): (...args: unknown[]) => void; function aborted(signal: AbortSignal, resource: object): Promise; function stripVTControlCharacters(str: string): string; diff --git a/tests/fixtures/execfile_promisify_child.cjs b/tests/fixtures/execfile_promisify_child.cjs new file mode 100644 index 0000000..e656f19 --- /dev/null +++ b/tests/fixtures/execfile_promisify_child.cjs @@ -0,0 +1,2 @@ +process.stdout.write('OUT'); +process.stderr.write('ERR'); diff --git a/tests/fixtures/tty_readstream_da_timeout_child.cjs b/tests/fixtures/tty_readstream_da_timeout_child.cjs new file mode 100644 index 0000000..5bebacb --- /dev/null +++ b/tests/fixtures/tty_readstream_da_timeout_child.cjs @@ -0,0 +1,31 @@ +const { openSync } = require('fs'); +const tty = require('tty'); + +const fd = openSync('/dev/tty', 'r'); +const stream = new tty.ReadStream(fd); +stream.setRawMode(true); + +let settled = false; + +stream.on('data', (data) => { + if (settled) { + return; + } + settled = true; + try { stream.setRawMode(false); } catch {} + console.log('DATA', JSON.stringify(Buffer.from(data).toString('latin1'))); + process.exit(0); +}); + +process.stdout.write('READY\n'); +process.stdout.write('\x1b[c'); + +setTimeout(() => { + if (settled) { + return; + } + settled = true; + try { stream.setRawMode(false); } catch {} + console.log('TIMEOUT'); + process.exit(2); +}, 100); diff --git a/tests/fixtures/tty_readstream_dev_tty_child.cjs b/tests/fixtures/tty_readstream_dev_tty_child.cjs new file mode 100644 index 0000000..3973227 --- /dev/null +++ b/tests/fixtures/tty_readstream_dev_tty_child.cjs @@ -0,0 +1,20 @@ +const fs = require('fs'); +const tty = require('tty'); + +const fd = fs.openSync('/dev/tty', 'r'); +const stream = new tty.ReadStream(fd); + +console.log('READY'); + +const timeout = setTimeout(() => { + console.log('TIMEOUT'); + process.exit(3); +}, 2000); + +stream.on('data', chunk => { + clearTimeout(timeout); + console.log('DATA', JSON.stringify(Buffer.from(chunk).toString('utf8'))); + process.exit(0); +}); + +stream.resume(); diff --git a/tests/fixtures/tty_readstream_dev_tty_on_data_flowing_child.cjs b/tests/fixtures/tty_readstream_dev_tty_on_data_flowing_child.cjs new file mode 100644 index 0000000..10a81d2 --- /dev/null +++ b/tests/fixtures/tty_readstream_dev_tty_on_data_flowing_child.cjs @@ -0,0 +1,18 @@ +const fs = require('fs'); +const tty = require('tty'); + +const fd = fs.openSync('/dev/tty', 'r'); +const stream = new tty.ReadStream(fd); + +console.log('READY'); + +const timeout = setTimeout(() => { + console.log('TIMEOUT'); + process.exit(3); +}, 2000); + +stream.on('data', chunk => { + clearTimeout(timeout); + console.log('DATA', JSON.stringify(Buffer.from(chunk).toString('utf8'))); + process.exit(0); +}); diff --git a/tests/repro_amp_startup_probe.cjs b/tests/repro_amp_startup_probe.cjs new file mode 100644 index 0000000..39205cb --- /dev/null +++ b/tests/repro_amp_startup_probe.cjs @@ -0,0 +1,142 @@ +const { openSync } = require('fs'); +const tty = require('tty'); + +const QUERY_KITTY = '\x1b[?u'; +const QUERY_DA1 = '\x1b[c'; +const ENABLE_FOCUS = '\x1b[?1004h'; +const ENABLE_INBAND_RESIZE = '\x1b[?2048h'; +const ENABLE_BRACKETED_PASTE = '\x1b[?2004h'; +const ENABLE_KITTY_KEYBOARD = '\x1b[>7u'; + +const startedAt = Date.now(); + +function nowMs() { + return Date.now() - startedAt; +} + +function writeLog(record) { + process.stderr.write(`${JSON.stringify({ t: nowMs(), ...record })}\n`); +} + +function toHex(data) { + return Buffer.from(data).toString('hex'); +} + +function cleanup(stream) { + if (!stream) { + return; + } + try { + stream.setRawMode(false); + } catch {} + try { + stream.removeAllListeners('data'); + } catch {} + try { + stream.destroy(); + } catch {} +} + +function parseDeviceAttributes(data) { + const text = Buffer.from(data).toString('latin1'); + const match = text.match(/\x1b\[\?(\d+)((?:;\d+)*)c/); + if (!match) { + return null; + } + + const primary = Number(match[1]); + const secondary = match[2] + ? match[2].split(';').filter(Boolean).map((value) => Number(value)) + : []; + + return { primary, secondary }; +} + +async function main() { + const fd = openSync('/dev/tty', 'r'); + const stream = new tty.ReadStream(fd); + stream.on('error', (error) => { + writeLog({ type: 'stream-error', message: error && error.message ? error.message : String(error) }); + process.exitCode = 1; + }); + stream.setRawMode(true); + + let probe = null; + + function settle(reason) { + if (!probe) { + return; + } + + const current = probe; + probe = null; + clearTimeout(current.timeout); + writeLog({ + type: 'probe-finish', + stage: current.stage, + reason, + elapsed: Date.now() - current.startedAt, + kittyQueryResponse: current.kittyQueryResponse, + deviceAttributes: current.deviceAttributes, + }); + current.resolve({ + reason, + kittyQueryResponse: current.kittyQueryResponse, + deviceAttributes: current.deviceAttributes, + }); + } + + stream.on('data', (data) => { + writeLog({ type: 'read', bytes: toHex(data) }); + + if (!probe) { + return; + } + + const da = parseDeviceAttributes(data); + if (da) { + probe.deviceAttributes = da; + writeLog({ type: 'device-attributes', stage: probe.stage, deviceAttributes: da }); + if (probe.kittyQueryResponse !== null) { + settle('complete'); + } + } + }); + + async function runProbe(stage) { + if (probe) { + settle('preempt'); + } + + return new Promise((resolve) => { + probe = { + stage, + startedAt: Date.now(), + kittyQueryResponse: null, + deviceAttributes: null, + resolve, + timeout: setTimeout(() => settle('timeout'), 200), + }; + + writeLog({ type: 'probe-start', stage }); + process.stdout.write(QUERY_KITTY); + process.stdout.write(QUERY_DA1); + }); + } + + try { + await runProbe('before_enable'); + process.stdout.write(ENABLE_FOCUS); + process.stdout.write(ENABLE_INBAND_RESIZE); + process.stdout.write(ENABLE_BRACKETED_PASTE); + process.stdout.write(ENABLE_KITTY_KEYBOARD); + await runProbe('after_enable'); + } finally { + cleanup(stream); + } +} + +main().catch((error) => { + writeLog({ type: 'fatal', message: error && error.message ? error.message : String(error) }); + process.exitCode = 1; +}); diff --git a/tests/repro_async_generator_await_yield.cjs b/tests/repro_async_generator_await_yield.cjs new file mode 100644 index 0000000..2bfe64a --- /dev/null +++ b/tests/repro_async_generator_await_yield.cjs @@ -0,0 +1,50 @@ +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +async function* delayedSingle() { + await Promise.resolve(); + yield 42; +} + +async function* delayedDouble() { + await Promise.resolve(); + yield 1; + await Promise.resolve(); + yield 2; +} + +async function main() { + { + const it = delayedSingle(); + const first = await it.next(); + console.log("single:first", JSON.stringify(first)); + assert(first.done === false, "single first result should not be done"); + assert(first.value === 42, "single first result should yield 42"); + } + + { + const it = delayedDouble(); + const first = await it.next(); + const second = await it.next(); + const third = await it.next(); + console.log( + "double", + JSON.stringify(first), + JSON.stringify(second), + JSON.stringify(third), + ); + assert(first.done === false, "double first result should not be done"); + assert(first.value === 1, "double first result should yield 1"); + assert(second.done === false, "double second result should not be done"); + assert(second.value === 2, "double second result should yield 2"); + assert(third.done === true, "double third result should be done"); + } + + console.log("async generator await+yield repro passed"); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + throw err; +}); diff --git a/tests/repro_async_generator_crash_stress.cjs b/tests/repro_async_generator_crash_stress.cjs new file mode 100644 index 0000000..1901384 --- /dev/null +++ b/tests/repro_async_generator_crash_stress.cjs @@ -0,0 +1,43 @@ +async function* delayedPair(tag) { + await new Promise((resolve) => setTimeout(resolve, 0)); + yield tag + ":one"; + await new Promise((resolve) => setTimeout(resolve, 0)); + yield tag + ":two"; +} + +async function stressOverlap(iteration) { + const it = delayedPair("overlap:" + iteration); + const p1 = it.next(); + const p2 = it.next(); + const p3 = it.next(); + return Promise.allSettled([p1, p2, p3]); +} + +async function stressReturn(iteration) { + const it = delayedPair("return:" + iteration); + const p1 = it.next(); + const p2 = it.return("done:" + iteration); + return Promise.allSettled([p1, p2]); +} + +async function stressThrow(iteration) { + const it = delayedPair("throw:" + iteration); + const p1 = it.next(); + const p2 = it.throw(new Error("boom:" + iteration)); + return Promise.allSettled([p1, p2]); +} + +async function main() { + for (let i = 0; i < 512; i++) { + if ((i & 31) === 0) console.log("iter", i); + await stressOverlap(i); + await stressReturn(i); + await stressThrow(i); + } + console.log("async generator crash stress completed"); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + throw err; +}); diff --git a/tests/repro_async_generator_overlap.cjs b/tests/repro_async_generator_overlap.cjs new file mode 100644 index 0000000..cc793e2 --- /dev/null +++ b/tests/repro_async_generator_overlap.cjs @@ -0,0 +1,46 @@ +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +async function* delayedPair() { + await new Promise((resolve) => setTimeout(resolve, 0)); + yield 1; + await new Promise((resolve) => setTimeout(resolve, 0)); + yield 2; +} + +async function runOnce(iteration) { + const it = delayedPair(); + + const p1 = it.next(); + const p2 = it.next(); + const p3 = it.next(); + + const r1 = await p1; + const r2 = await p2; + const r3 = await p3; + + console.log( + "iter", + iteration, + JSON.stringify(r1), + JSON.stringify(r2), + JSON.stringify(r3), + ); + + assert(r1.done === false, "first next should not be done"); + assert(r1.value === 1, "first next should yield 1"); + assert(r2.done === false, "second next should not be done"); + assert(r2.value === 2, "second next should yield 2"); + assert(r3.done === true, "third next should complete the iterator"); +} + +async function main() { + for (let i = 0; i < 64; i++) await runOnce(i); + console.log("async generator overlap repro passed"); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + throw err; +}); diff --git a/tests/repro_async_generator_reentrant_then.cjs b/tests/repro_async_generator_reentrant_then.cjs new file mode 100644 index 0000000..0c756c2 --- /dev/null +++ b/tests/repro_async_generator_reentrant_then.cjs @@ -0,0 +1,42 @@ +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +async function* delayedPair(tag) { + await new Promise((resolve) => setTimeout(resolve, 0)); + yield tag + ":one"; + await new Promise((resolve) => setTimeout(resolve, 0)); + yield tag + ":two"; +} + +function runChain(iteration) { + const it = delayedPair("chain:" + iteration); + + return it.next().then((first) => { + console.log("first", iteration, JSON.stringify(first)); + assert(first.done === false, "first result should not be done"); + assert(first.value === "chain:" + iteration + ":one", "first result should yield first value"); + return it.next().then((second) => { + console.log("second", iteration, JSON.stringify(second)); + assert(second.done === false, "second result should not be done"); + assert(second.value === "chain:" + iteration + ":two", "second result should yield second value"); + return it.next().then((third) => { + console.log("third", iteration, JSON.stringify(third)); + assert(third.done === true, "third result should finish the iterator"); + }); + }); + }); +} + +async function main() { + for (let i = 0; i < 128; i++) { + if ((i & 31) === 0) console.log("iter", i); + await runChain(i); + } + console.log("async generator reentrant then repro completed"); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + throw err; +}); diff --git a/tests/test_async_generator_gc_liveness.cjs b/tests/test_async_generator_gc_liveness.cjs new file mode 100644 index 0000000..5d046dc --- /dev/null +++ b/tests/test_async_generator_gc_liveness.cjs @@ -0,0 +1,46 @@ +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +function churnGarbage(rounds, width) { + const ring = new Array(8); + for (let r = 0; r < rounds; r++) { + const batch = new Array(width); + for (let i = 0; i < width; i++) { + batch[i] = { + i, + label: "item-" + i, + arr: [i, i + 1, i + 2, i + 3], + }; + } + ring[r & 7] = batch; + } + return ring.length; +} + +async function* delayedCounter() { + await new Promise((resolve) => setTimeout(resolve, 0)); + yield 42; +} + +async function runOnce() { + let iter = delayedCounter(); + const nextPromise = iter.next(); + iter = null; + + churnGarbage(128, 256); + + const result = await nextPromise; + assert(result && result.value === 42, "expected yielded value"); + assert(result.done === false, "expected unfinished iterator result"); +} + +async function main() { + for (let i = 0; i < 64; i++) await runOnce(); + console.log("async generator survives GC after iterator becomes unreachable"); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + throw err; +}); diff --git a/tests/test_child_process_execfile_git_options.cjs b/tests/test_child_process_execfile_git_options.cjs new file mode 100644 index 0000000..191268b --- /dev/null +++ b/tests/test_child_process_execfile_git_options.cjs @@ -0,0 +1,38 @@ +const { promisify } = require('node:util'); +const { execFile } = require('node:child_process'); +const path = require('node:path'); + +async function main() { + const execFileAsync = promisify(execFile); + const cwd = path.join(__dirname, '..'); + + const revParse = await execFileAsync( + 'git', + ['rev-parse', '--show-toplevel'], + { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 } + ); + + if (!revParse || typeof revParse.stdout !== 'string') { + throw new Error('expected rev-parse stdout string'); + } + if (!revParse.stdout.trim().endsWith('/ant')) { + throw new Error(`unexpected rev-parse output: ${JSON.stringify(revParse.stdout)}`); + } + + const status = await execFileAsync( + 'git', + ['status', '--porcelain=v1', '-z', '--untracked-files=normal'], + { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 } + ); + + if (!status || typeof status.stdout !== 'string') { + throw new Error('expected status stdout string'); + } + + console.log('util.promisify(execFile) handles git with options'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_child_process_execfile_promisify.cjs b/tests/test_child_process_execfile_promisify.cjs new file mode 100644 index 0000000..7411fcf --- /dev/null +++ b/tests/test_child_process_execfile_promisify.cjs @@ -0,0 +1,36 @@ +const { promisify } = require('util'); +const { execFile } = require('child_process'); +const path = require('path'); + +async function main() { + if (typeof promisify.custom !== 'symbol') { + throw new Error('expected util.promisify.custom to be a symbol'); + } + + if (typeof execFile[promisify.custom] !== 'function') { + throw new Error('expected execFile to define util.promisify.custom'); + } + + const execFileAsync = promisify(execFile); + const child = path.join(__dirname, 'fixtures', 'execfile_promisify_child.cjs'); + const result = await execFileAsync(process.execPath, [child]); + + if (!result || typeof result !== 'object' || Array.isArray(result)) { + throw new Error(`expected object result, got ${Object.prototype.toString.call(result)}`); + } + + if (result.stdout !== 'OUT') { + throw new Error(`expected stdout to be OUT, got ${JSON.stringify(result.stdout)}`); + } + + if (result.stderr !== 'ERR') { + throw new Error(`expected stderr to be ERR, got ${JSON.stringify(result.stderr)}`); + } + + console.log('util.promisify(execFile) resolves { stdout, stderr }'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_closure_forwarder_reentrant_callbacks.cjs b/tests/test_closure_forwarder_reentrant_callbacks.cjs new file mode 100644 index 0000000..e86fa67 --- /dev/null +++ b/tests/test_closure_forwarder_reentrant_callbacks.cjs @@ -0,0 +1,189 @@ +function afterTransform(er, data) { + const ts = this._transformState; + ts.transforming = false; + const cb = ts.writecb; + if (cb === null) throw new Error('multiple callback'); + ts.writechunk = null; + ts.writecb = null; + cb(er); + const rs = this._readableState; + rs.reading = false; + if (rs.needReadable || rs.length < rs.highWaterMark) { + this._read(rs.highWaterMark); + } +} + +function doWrite(stream, state, len, chunk, encoding, cb) { + state.writelen = len; + state.writecb = cb; + state.writing = true; + stream._write(chunk, encoding, state.onwrite); +} + +function onwriteStateUpdate(state) { + state.writing = false; + state.writecb = null; + state.length -= state.writelen; + state.writelen = 0; +} + +function afterWrite(stream, state, cb) { + state.pendingcb--; + cb(); +} + +function clearBuffer(stream, state) { + state.bufferProcessing = true; + let entry = state.bufferedRequest; + while (entry) { + const chunk = entry.chunk; + const encoding = entry.encoding; + const cb = entry.callback; + const len = chunk.length; + doWrite(stream, state, len, chunk, encoding, cb); + entry = entry.next; + state.bufferedRequestCount--; + if (state.writing) break; + } + if (entry === null) state.lastBufferedRequest = null; + state.bufferedRequest = entry; + state.bufferProcessing = false; +} + +function onwrite(stream, er) { + const state = stream._writableState; + const cb = state.writecb; + if (typeof cb !== 'function') throw new Error('bad cb'); + onwriteStateUpdate(state); + if (er) throw er; + if (!state.corked && !state.bufferProcessing && state.bufferedRequest) { + clearBuffer(stream, state); + } + afterWrite(stream, state, cb); +} + +function onwriteForward(er) { + return onwrite(onwriteForward.owner, er); +} + +function afterTransformForward(er, data) { + return afterTransform.call(afterTransformForward.owner, er, data); +} + +function writeOrBuffer(stream, state, chunk, encoding, cb) { + const len = chunk.length; + state.length += len; + if (state.writing || state.corked) { + const req = { chunk, encoding, callback: cb, next: null }; + const last = state.lastBufferedRequest; + state.lastBufferedRequest = req; + if (last) last.next = req; + else state.bufferedRequest = req; + state.bufferedRequestCount++; + } else { + doWrite(stream, state, len, chunk, encoding, cb); + } + return true; +} + +function makeState(owner, onwriteFn, afterTransformFn) { + owner._readableState = { + needReadable: true, + reading: false, + length: 0, + highWaterMark: 16, + }; + owner._transformState = { + afterTransform: afterTransformFn, + needTransform: false, + transforming: false, + writecb: null, + writechunk: null, + writeencoding: null, + }; + owner._writableState = { + length: 0, + highWaterMark: 16, + writing: false, + corked: 0, + bufferProcessing: false, + writecb: null, + writelen: 0, + bufferedRequest: null, + lastBufferedRequest: null, + bufferedRequestCount: 0, + pendingcb: 0, + onwrite: onwriteFn, + }; +} + +function ClosureForwardingTransformLike() { + makeState( + this, + (er) => onwrite(this, er), + (er, data) => afterTransform.call(this, er, data), + ); +} + +function OwnerForwardingTransformLike() { + makeState(this, onwriteForward, afterTransformForward); + this._writableState.onwrite.owner = this; + this._transformState.afterTransform.owner = this; +} + +ClosureForwardingTransformLike.prototype.write = OwnerForwardingTransformLike.prototype.write = +function(chunk, cb) { + const state = this._writableState; + state.pendingcb++; + return writeOrBuffer(this, state, chunk, 'utf8', cb); +}; + +ClosureForwardingTransformLike.prototype._write = OwnerForwardingTransformLike.prototype._write = +function(chunk, encoding, cb) { + const ts = this._transformState; + ts.writecb = cb; + ts.writechunk = chunk; + ts.writeencoding = encoding; + if (!ts.transforming) { + const rs = this._readableState; + if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) { + this._read(rs.highWaterMark); + } + } +}; + +ClosureForwardingTransformLike.prototype._read = OwnerForwardingTransformLike.prototype._read = +function() { + const ts = this._transformState; + if (ts.writechunk !== null && !ts.transforming) { + ts.transforming = true; + this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); + } else { + ts.needTransform = true; + } +}; + +ClosureForwardingTransformLike.prototype._transform = OwnerForwardingTransformLike.prototype._transform = +function(chunk, encoding, cb) { + cb(null, chunk); +}; + +function exercise(instance, count) { + let callbacks = 0; + + for (let i = 0; i < count; i++) { + instance.write('x', (err) => { + if (err) throw err; + callbacks++; + }); + } + + if (callbacks !== count) { + throw new Error(`expected ${count} callbacks, got ${callbacks}`); + } +} + +exercise(new OwnerForwardingTransformLike(), 512); +exercise(new ClosureForwardingTransformLike(), 128); + +console.log('closure forwarder reentrant callbacks ok'); diff --git a/tests/test_events_listeners_api.cjs b/tests/test_events_listeners_api.cjs new file mode 100644 index 0000000..249e2aa --- /dev/null +++ b/tests/test_events_listeners_api.cjs @@ -0,0 +1,42 @@ +const { EventEmitter } = require('events'); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const ee = new EventEmitter(); +const calls = []; + +function onFoo(value) { + calls.push(['on', value]); +} + +function onceFoo(value) { + calls.push(['once', value]); +} + +ee.on('foo', onFoo); +ee.once('foo', onceFoo); + +const listenersBefore = ee.listeners('foo'); +assert(Array.isArray(listenersBefore), 'listeners() should return an array'); +assert(listenersBefore.length === 2, `expected 2 listeners, got ${listenersBefore.length}`); +assert(listenersBefore[0] === onFoo, 'listeners() should return the original persistent listener'); +assert(listenersBefore[1] === onceFoo, 'listeners() should unwrap once listeners to the original callback'); + +const rawBefore = ee.rawListeners('foo'); +assert(rawBefore.length === 2, `expected 2 raw listeners, got ${rawBefore.length}`); +assert(rawBefore[0] === onFoo, 'rawListeners() should return the original persistent listener'); +assert(rawBefore[1] !== onceFoo, 'rawListeners() should expose the once wrapper'); + +ee.emit('foo', 1); + +const listenersAfter = ee.listeners('foo'); +assert(listenersAfter.length === 1, `expected 1 listener after once emission, got ${listenersAfter.length}`); +assert(listenersAfter[0] === onFoo, 'persistent listener should remain after once emission'); + +assert(calls.length === 2, `expected 2 calls, got ${calls.length}`); +assert(calls[0][0] === 'on' && calls[0][1] === 1, 'persistent listener should fire first'); +assert(calls[1][0] === 'once' && calls[1][1] === 1, 'once listener should fire once'); + +console.log('events listeners api ok'); diff --git a/tests/test_fs_promises_filehandle.cjs b/tests/test_fs_promises_filehandle.cjs new file mode 100644 index 0000000..3d1dbc6 --- /dev/null +++ b/tests/test_fs_promises_filehandle.cjs @@ -0,0 +1,57 @@ +const fs = require('node:fs/promises'); +const path = require('node:path'); + +async function main() { + const file = path.join(__dirname, 'tmp_filehandle.txt'); + + try { + await fs.unlink(file); + } catch {} + + const writer = await fs.open(file, 'w'); + if (!writer || typeof writer !== 'object') { + throw new Error(`expected FileHandle object, got ${Object.prototype.toString.call(writer)}`); + } + if (typeof writer.fd !== 'number') { + throw new Error(`expected numeric fd, got ${typeof writer.fd}`); + } + if (typeof writer.writeFile !== 'function') { + throw new Error('expected FileHandle.writeFile()'); + } + if (typeof writer.sync !== 'function') { + throw new Error('expected FileHandle.sync()'); + } + if (typeof writer.close !== 'function') { + throw new Error('expected FileHandle.close()'); + } + + await writer.writeFile('hello'); + await writer.sync(); + const writerStat = await writer.stat(); + if (!writerStat || typeof writerStat.size !== 'number' || writerStat.size !== 5) { + throw new Error(`expected stat.size === 5, got ${writerStat && writerStat.size}`); + } + await writer.close(); + + const reader = await fs.open(file, 'r'); + const buf = Buffer.alloc(5); + const readResult = await reader.read(buf, 0, buf.length, 0); + if (!readResult || readResult.bytesRead !== 5) { + throw new Error(`expected bytesRead === 5, got ${readResult && readResult.bytesRead}`); + } + if (readResult.buffer !== buf) { + throw new Error('expected read() to return the original buffer'); + } + if (buf.toString('utf8') !== 'hello') { + throw new Error(`expected "hello", got ${JSON.stringify(buf.toString('utf8'))}`); + } + await reader.close(); + await fs.unlink(file); + + console.log('fs/promises FileHandle works'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_process_nexttick_bound_receiver.cjs b/tests/test_process_nexttick_bound_receiver.cjs new file mode 100644 index 0000000..3d5ba81 --- /dev/null +++ b/tests/test_process_nexttick_bound_receiver.cjs @@ -0,0 +1,34 @@ +async function main() { + const receiver = { + tag: 'receiver', + seen: [], + callback(arg) { + this.seen.push({ thisTag: this.tag, arg }); + }, + }; + + const bound = receiver.callback.bind(receiver); + process.nextTick(bound, 'queued'); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + if (receiver.seen.length !== 1) { + throw new Error(`expected 1 callback invocation, got ${receiver.seen.length}`); + } + + const [entry] = receiver.seen; + if (entry.thisTag !== 'receiver') { + throw new Error(`expected bound receiver, got ${JSON.stringify(entry.thisTag)}`); + } + + if (entry.arg !== 'queued') { + throw new Error(`expected queued arg, got ${JSON.stringify(entry.arg)}`); + } + + console.log('process.nextTick preserves bound receiver and queued args'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_process_nexttick_order.cjs b/tests/test_process_nexttick_order.cjs new file mode 100644 index 0000000..77f3ac0 --- /dev/null +++ b/tests/test_process_nexttick_order.cjs @@ -0,0 +1,11 @@ +const out = []; + +Promise.resolve().then(() => out.push("promise")); +process.nextTick(() => out.push("tick")); + +setTimeout(() => { + const got = JSON.stringify(out); + const want = JSON.stringify(["tick", "promise"]); + if (got !== want) throw new Error(`expected ${want}, got ${got}`); + console.log("process.nextTick ordering ok"); +}, 0); diff --git a/tests/test_stream_readable_data_listener_order.cjs b/tests/test_stream_readable_data_listener_order.cjs new file mode 100644 index 0000000..c0a11be --- /dev/null +++ b/tests/test_stream_readable_data_listener_order.cjs @@ -0,0 +1,35 @@ +const { Readable } = require('node:stream'); + +async function main() { + const seen = []; + + const readable = new Readable({ + read() { + this.push('x'); + this.push(null); + }, + }); + + readable.on('data', (chunk) => { + seen.push(['data', chunk.toString('utf8')]); + }); + + readable.on('end', () => { + seen.push(['end']); + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + const got = JSON.stringify(seen); + const want = JSON.stringify([['data', 'x'], ['end']]); + if (got !== want) { + throw new Error(`expected ${want}, got ${got}`); + } + + console.log('stream data listeners keep deferred flowing semantics'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_stream_transform_async_callback.cjs b/tests/test_stream_transform_async_callback.cjs new file mode 100644 index 0000000..4d7b574 --- /dev/null +++ b/tests/test_stream_transform_async_callback.cjs @@ -0,0 +1,37 @@ +const { Transform } = require('node:stream'); + +async function main() { + let output = ''; + + const tr = new Transform({ + transform(chunk, encoding, cb) { + Promise.resolve().then(() => { + cb(null, chunk.toString('utf8').toUpperCase()); + }); + }, + }); + + tr.on('data', (chunk) => { + output += chunk.toString('utf8'); + }); + + const finished = new Promise((resolve, reject) => { + tr.on('finish', resolve); + tr.on('error', reject); + }); + + tr.write('abc'); + tr.end(); + await finished; + + if (output !== 'ABC') { + throw new Error(`expected "ABC", got ${JSON.stringify(output)}`); + } + + console.log('stream Transform async callback preserves receiver state'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_tty_readstream_da_timeout.cjs b/tests/test_tty_readstream_da_timeout.cjs new file mode 100644 index 0000000..6ae39e0 --- /dev/null +++ b/tests/test_tty_readstream_da_timeout.cjs @@ -0,0 +1,111 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function fail(message) { + throw new Error(message); +} + +function runInPty() { + const helper = path.join(__dirname, 'fixtures', 'tty_readstream_da_timeout_child.cjs'); + const script = ` +import os, select, signal, sys, time + +exec_path, helper = sys.argv[1], sys.argv[2] +pid, master = os.forkpty() + +if pid == 0: + os.execv(exec_path, [exec_path, helper]) + +buf = bytearray() +replied = False +exit_code = None +deadline = time.time() + 7.0 + +while time.time() < deadline: + done, status = os.waitpid(pid, os.WNOHANG) + if done == pid: + exit_code = os.waitstatus_to_exitcode(status) + break + + r, _, _ = select.select([master], [], [], 0.1) + if master not in r: + continue + + try: + chunk = os.read(master, 4096) + except OSError: + break + + if not chunk: + break + + buf.extend(chunk) + if (not replied) and b'\\x1b[c' in bytes(buf): + os.write(master, b'\\x1b[?1;2c') + replied = True + +if exit_code is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) + exit_code = os.waitstatus_to_exitcode(status) + +while True: + r, _, _ = select.select([master], [], [], 0.05) + if master not in r: + break + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + buf.extend(chunk) + +sys.stdout.buffer.write(bytes(buf)) +sys.exit(exit_code) +`; + + if (process.platform === 'win32') { + console.log('skipping tty DA timeout test on win32'); + process.exit(0); + } + + return spawnSync('python3', ['-c', script, process.execPath, helper], { + encoding: 'utf8', + timeout: 9000, + }); +} + +const result = runInPty(); + +if (result.error && result.error.code === 'ENOENT') { + console.log('skipping tty DA timeout test because `python3` is unavailable'); + process.exit(0); +} + +if (result.error) throw result.error; + +const output = `${result.stdout || ''}${result.stderr || ''}`; + +if (output.includes('setRawMode EPERM') || output.includes('Failed to set raw mode')) { + console.log('skipping tty DA timeout test because raw mode is unavailable in this PTY environment'); + process.exit(0); +} + +if (result.status !== 0) { + fail(`child exited ${result.status}\n${output}`); +} + +if (!output.includes('READY')) { + fail(`expected READY banner\n${output}`); +} + +if (!output.includes('DATA "\\u001b[?1;2c"')) { + fail(`expected immediate DA response before timeout\n${output}`); +} + +if (output.includes('TIMEOUT')) { + fail(`did not expect timeout before DA response\n${output}`); +} + +console.log('tty.ReadStream(/dev/tty) receives DA1 before short timeout'); diff --git a/tests/test_tty_readstream_dev_tty.cjs b/tests/test_tty_readstream_dev_tty.cjs new file mode 100644 index 0000000..b40a982 --- /dev/null +++ b/tests/test_tty_readstream_dev_tty.cjs @@ -0,0 +1,102 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function fail(message) { + throw new Error(message); +} + +function runInPty() { + const helper = path.join(__dirname, 'fixtures', 'tty_readstream_dev_tty_child.cjs'); + const script = ` +import os, select, signal, sys, time + +exec_path, helper = sys.argv[1], sys.argv[2] +pid, master = os.forkpty() + +if pid == 0: + os.execv(exec_path, [exec_path, helper]) + +buf = bytearray() +sent = False +exit_code = None +deadline = time.time() + 7.0 + +while time.time() < deadline: + done, status = os.waitpid(pid, os.WNOHANG) + if done == pid: + exit_code = os.waitstatus_to_exitcode(status) + break + + r, _, _ = select.select([master], [], [], 0.1) + if master not in r: + continue + + try: + chunk = os.read(master, 4096) + except OSError: + break + + if not chunk: + break + + buf.extend(chunk) + if (not sent) and b'READY' in buf: + os.write(master, b'hello\\n') + sent = True + +if exit_code is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) + exit_code = os.waitstatus_to_exitcode(status) + +while True: + r, _, _ = select.select([master], [], [], 0.05) + if master not in r: + break + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + buf.extend(chunk) + +sys.stdout.buffer.write(bytes(buf)) +sys.exit(exit_code) +`; + + if (process.platform === 'win32') { + console.log('skipping tty.ReadStream(/dev/tty) pty test on win32'); + process.exit(0); + } + + return spawnSync('python3', ['-c', script, process.execPath, helper], { + encoding: 'utf8', + timeout: 9000, + }); +} + +const result = runInPty(); + +if (result.error && result.error.code === 'ENOENT') { + console.log('skipping tty.ReadStream(/dev/tty) pty test because `python3` is unavailable'); + process.exit(0); +} + +if (result.error) throw result.error; + +const output = `${result.stdout || ''}${result.stderr || ''}`; + +if (result.status !== 0) { + fail(`child exited ${result.status}\n${output}`); +} + +if (!output.includes('READY')) { + fail(`expected READY banner\n${output}`); +} + +if (!output.includes('DATA "hello')) { + fail(`expected tty data payload in output\n${output}`); +} + +console.log('tty.ReadStream(/dev/tty) receives data via pty'); diff --git a/tests/test_tty_readstream_dev_tty_on_data_flowing.cjs b/tests/test_tty_readstream_dev_tty_on_data_flowing.cjs new file mode 100644 index 0000000..6216298 --- /dev/null +++ b/tests/test_tty_readstream_dev_tty_on_data_flowing.cjs @@ -0,0 +1,102 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function fail(message) { + throw new Error(message); +} + +function runInPty() { + const helper = path.join(__dirname, 'fixtures', 'tty_readstream_dev_tty_on_data_flowing_child.cjs'); + const script = ` +import os, select, signal, sys, time + +exec_path, helper = sys.argv[1], sys.argv[2] +pid, master = os.forkpty() + +if pid == 0: + os.execv(exec_path, [exec_path, helper]) + +buf = bytearray() +sent = False +exit_code = None +deadline = time.time() + 7.0 + +while time.time() < deadline: + done, status = os.waitpid(pid, os.WNOHANG) + if done == pid: + exit_code = os.waitstatus_to_exitcode(status) + break + + r, _, _ = select.select([master], [], [], 0.1) + if master not in r: + continue + + try: + chunk = os.read(master, 4096) + except OSError: + break + + if not chunk: + break + + buf.extend(chunk) + if (not sent) and b'READY' in buf: + os.write(master, b'probe\\n') + sent = True + +if exit_code is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) + exit_code = os.waitstatus_to_exitcode(status) + +while True: + r, _, _ = select.select([master], [], [], 0.05) + if master not in r: + break + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + buf.extend(chunk) + +sys.stdout.buffer.write(bytes(buf)) +sys.exit(exit_code) +`; + + if (process.platform === 'win32') { + console.log('skipping tty.ReadStream(/dev/tty) flowing listener test on win32'); + process.exit(0); + } + + return spawnSync('python3', ['-c', script, process.execPath, helper], { + encoding: 'utf8', + timeout: 9000, + }); +} + +const result = runInPty(); + +if (result.error && result.error.code === 'ENOENT') { + console.log('skipping tty.ReadStream(/dev/tty) flowing listener test because `python3` is unavailable'); + process.exit(0); +} + +if (result.error) throw result.error; + +const output = `${result.stdout || ''}${result.stderr || ''}`; + +if (result.status !== 0) { + fail(`child exited ${result.status}\n${output}`); +} + +if (!output.includes('READY')) { + fail(`expected READY banner\n${output}`); +} + +if (!output.includes('DATA "probe')) { + fail(`expected tty data payload without explicit resume()\n${output}`); +} + +console.log('tty.ReadStream(/dev/tty) starts flowing on data listener');