diff --git a/docs/exec-plans/tech-debt.md b/docs/exec-plans/tech-debt.md index e2a80ff..abc1d94 100644 --- a/docs/exec-plans/tech-debt.md +++ b/docs/exec-plans/tech-debt.md @@ -29,3 +29,51 @@ scheduled. - Impact: One-shot CLI compiles are fine, but a REPL, watch mode, embedder, or other long-lived process cannot yet recycle compiler scratch space across compiles. - Proposed fix: Add a real `compile_pool` scratch allocator after the `compile_ctx` extraction. Pool the resizable arrays for `locals`, `local_lookup_heads`, `code`, `constants`, `atoms`, `upval_descs`, `loops`, `srcpos`, and potentially `slot_types`. Keep `line_table` separate or make it poolable scratch, since it is derived from the current source buffer rather than a semantic cache. - Status: backlog + +- Area: Shared helper utilities + - Issue: Small helper logic such as ASCII character classification, casing, and similar utility code is duplicated across multiple runtime and support modules with local one-off implementations. + - Impact: Repeated copies drift over time, make bug fixes harder to apply consistently, and add noise when adding or reviewing new modules. + - Proposed fix: Audit duplicated helper patterns across `src/` and `include/`, identify the stable cross-cutting utilities, and centralize them in a small shared header or utility module with repo-wide call sites migrated incrementally. + - Status: backlog + +- Area: `src/modules/intl.c` + - Issue: `Intl` is now present and passes the current compat-table target, but several behaviors are still simplified compatibility implementations rather than fuller ECMA-402 semantics. + - Impact: `Intl.Collator`, `Intl.NumberFormat`, `Intl.DateTimeFormat`, and `Intl.Segmenter` can still diverge from web or Node behavior for anything beyond the currently covered compat surface. + - Proposed fix: Continue expanding `Intl` incrementally: replace `strcoll`-only collation, deepen `resolvedOptions()`, make `DateTimeFormat` actually honor stored timezone and locale options, and move `Segmenter` closer to the expected iterable/result object shape. + - Status: backlog + +- Area: `src/modules/timer.c` + - Issue: `node:timers/promises setInterval()` is still explicitly unimplemented. + - Impact: Promise-based timer APIs remain incomplete and can block compatibility with code that expects the Node timers/promises interval surface. + - Proposed fix: Implement `setInterval()` on top of the existing timer promise scheduling machinery, including cancellation and signal handling behavior consistent with the existing `setTimeout()` and `setImmediate()` support. + - Status: backlog + +- Area: `src/modules/dns.c` + - Issue: `node:dns` is still a minimal shim centered on `dns.promises.lookup`. + - Impact: Tooling or apps that expect more of the Node DNS surface still need polyfills or will fail outright. + - Proposed fix: Expand the module incrementally from the existing lookup path, prioritizing the most commonly used sync, callback, and `promises` APIs needed by current ecosystem packages. + - Status: backlog + +- Area: `src/modules/crypto.c` + - Issue: `crypto.subtle` is only partially implemented and still marked for extension beyond the current digest-oriented support. + - Impact: Web Crypto compatibility is incomplete, which blocks packages and runtime features that expect a broader `SubtleCrypto` surface. + - Proposed fix: Extend `crypto.subtle` method coverage incrementally, starting with the highest-value operations after digest and preserving the existing algorithm parsing entrypoints. + - Status: backlog + +- Area: `src/modules/worker_threads.c` + - Issue: `node:worker_threads` is still a minimal compatibility implementation, and `Worker.postMessage` remains explicitly unimplemented. + - Impact: Build tools and libraries that rely on real worker thread messaging or broader worker lifecycle behavior still cannot use the native surface directly. + - Proposed fix: Expand worker thread support incrementally, starting with message passing and the most commonly used worker APIs, while preserving the existing lightweight process-backed architecture where practical. + - Status: backlog + +- Area: `src/modules/async_hooks.c` + - Issue: `node:async_hooks` is still a minimal compatibility layer intended mainly to satisfy framework expectations. + - Impact: Async context tracking semantics remain shallow, which can break libraries that rely on realistic async IDs, resources, or hook lifecycle behavior. + - Proposed fix: Replace the placeholder async ID and resource behavior with real runtime-backed tracking, while keeping `AsyncLocalStorage` compatibility stable during the transition. + - Status: backlog + +- Area: `src/streams/readable.c` + - Issue: `ReadableStreamBYOBReader` is still explicitly unimplemented, and byte-source support is still called out as incomplete. + - Impact: Web Streams byte-oriented consumers cannot rely on BYOB reader semantics, leaving an important platform feature gap for stream-heavy or browser-compatible code. + - Proposed fix: Add real byte-source plumbing and implement `ReadableStreamBYOBReader` on top of it instead of routing byte sources through the default reader path. + - Status: backlog diff --git a/include/modules/intl.h b/include/modules/intl.h new file mode 100644 index 0000000..274769b --- /dev/null +++ b/include/modules/intl.h @@ -0,0 +1,6 @@ +#ifndef ANT_INTL_MODULE_H +#define ANT_INTL_MODULE_H + +void init_intl_module(void); + +#endif diff --git a/src/main.c b/src/main.c index 43c8601..eefddf1 100644 --- a/src/main.c +++ b/src/main.c @@ -80,6 +80,7 @@ #include "modules/domexception.h" #include "modules/abort.h" #include "modules/globals.h" +#include "modules/intl.h" #include "modules/wasm.h" #include "modules/string_decoder.h" #include "modules/stream.h" @@ -592,6 +593,7 @@ int main(int argc, char *argv[]) { init_timer_module(); init_domexception_module(); init_globals_module(); + init_intl_module(); init_wasm_module(); init_builtin_module(); init_buffer_module(); diff --git a/src/modules/globals.c b/src/modules/globals.c index 6ab1c68..87bb6ca 100644 --- a/src/modules/globals.c +++ b/src/modules/globals.c @@ -1,10 +1,7 @@ #include // IWYU pragma: keep #include -#include -#include #include -#include #include "ant.h" #include "errors.h" @@ -104,170 +101,10 @@ void js_fire_rejection_handled(ant_t *js, ant_value_t promise_val, ant_value_t r sv_vm_call(js->vm, js, handler, global, call_args, 1, NULL, false); } -// stub: minimal Intl -static ant_value_t intl_dtf_format(ant_t *js, ant_value_t *args, int nargs) { - time_t t; - - if (nargs >= 1 && vtype(args[0]) == T_NUM) { - t = (time_t)(js_getnum(args[0]) / 1000.0); - } else t = time(NULL); - - struct tm local; -#ifdef _WIN32 - localtime_s(&local, &t); -#else - localtime_r(&t, &local); -#endif - - char buf[64]; - int hour12 = local.tm_hour % 12; - if (hour12 == 0) hour12 = 12; - - const char *ampm = local.tm_hour < 12 ? "AM" : "PM"; - snprintf(buf, sizeof(buf), "%d:%02d:%02d %s", hour12, local.tm_min, local.tm_sec, ampm); - - return js_mkstr(js, buf, strlen(buf)); -} - -static ant_value_t intl_dtf_resolvedOptions(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t obj = js_mkobj(js); - js_set(js, obj, "locale", js_mkstr(js, "en-US", 5)); - js_set(js, obj, "timeZone", js_mkstr(js, "UTC", 3)); - return obj; -} - -static ant_value_t intl_dtf_formatToParts(ant_t *js, ant_value_t *args, int nargs) { - return js_mkarr(js); -} - -static ant_value_t intl_dtf_constructor(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t this = js_getthis(js); - - ant_value_t format_fn = js_heavy_mkfun(js, intl_dtf_format, js_mkundef()); - js_set(js, this, "format", format_fn); - js_set(js, this, "resolvedOptions", js_mkfun(intl_dtf_resolvedOptions)); - js_set(js, this, "formatToParts", js_mkfun(intl_dtf_formatToParts)); - - return this; -} - -static size_t intl_utf8_segment_len(const char *input, size_t remaining) { - if (remaining == 0) return 0; - - const unsigned char *s = (const unsigned char *)input; - unsigned char c = s[0]; - size_t len = 1; - - if ((c & 0x80) == 0) return 1; - if ((c & 0xe0) == 0xc0) len = 2; - else if ((c & 0xf0) == 0xe0) len = 3; - else if ((c & 0xf8) == 0xf0) len = 4; - - if (len > remaining) return 1; - for (size_t i = 1; i < len; i++) if ((s[i] & 0xc0) != 0x80) return 1; - - return len; -} - -static bool intl_ascii_is_word_byte(const char *segment, size_t len) { - if (len != 1) return true; - - unsigned char c = (unsigned char)segment[0]; - return - (c >= '0' && c <= '9') || - (c >= 'A' && c <= 'Z') || - (c >= 'a' && c <= 'z') || - c == '_'; -} - -static const char *intl_segmenter_granularity(ant_t *js, ant_value_t segmenter, size_t *len) { - ant_value_t granularity = js_get(js, segmenter, "granularity"); - if (vtype(granularity) != T_STR) { - if (len) *len = 8; - return "grapheme"; - } - - return js_getstr(js, granularity, len); -} - -static ant_value_t intl_segmenter_segment(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t input = nargs > 0 ? js_tostring_val(js, args[0]) : js_mkstr(js, "", 0); - if (is_err(input)) return input; - - size_t input_len = 0; - char *input_str = js_getstr(js, input, &input_len); - ant_value_t segments = js_mkarr(js); - - ant_value_t this = js_getthis(js); - size_t granularity_len = 0; - const char *granularity = intl_segmenter_granularity(js, this, &granularity_len); - bool word_granularity = granularity_len == 4 && memcmp(granularity, "word", 4) == 0; - - for (size_t offset = 0; offset < input_len;) { - size_t segment_len = intl_utf8_segment_len(input_str + offset, input_len - offset); - ant_value_t record = js_mkobj(js); - - js_set(js, record, "segment", js_mkstr(js, input_str + offset, segment_len)); - js_set(js, record, "index", js_mknum((double)offset)); - js_set(js, record, "input", input); - - if (word_granularity) js_set( - js, record, "isWordLike", - js_bool(intl_ascii_is_word_byte(input_str + offset, segment_len)) - ); - - js_arr_push(js, segments, record); - offset += segment_len; - } - - return segments; -} - -static ant_value_t intl_segmenter_resolvedOptions(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t obj = js_mkobj(js); - ant_value_t this = js_getthis(js); - - size_t granularity_len = 0; - const char *granularity = intl_segmenter_granularity(js, this, &granularity_len); - - js_set(js, obj, "locale", js_mkstr(js, "en-US", 5)); - js_set(js, obj, "granularity", js_mkstr(js, granularity, granularity_len)); - - return obj; -} - -static ant_value_t intl_segmenter_constructor(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t this = js_getthis(js); - const char *granularity = "grapheme"; - size_t granularity_len = 8; - - if (nargs >= 2 && vtype(args[1]) == T_OBJ) { - ant_value_t option = js_get(js, args[1], "granularity"); - if (vtype(option) == T_STR) granularity = js_getstr(js, option, &granularity_len); - } - - js_set(js, this, "granularity", js_mkstr(js, granularity, granularity_len)); - js_set(js, this, "segment", js_mkfun(intl_segmenter_segment)); - js_set(js, this, "resolvedOptions", js_mkfun(intl_segmenter_resolvedOptions)); - - return this; -} - void init_globals_module(void) { ant_t *js = rt->js; ant_value_t global = js_glob(js); js_set(js, global, "reportError", js_mkfun(js_report_error)); js_set_descriptor(js, global, "reportError", 11, JS_DESC_W | JS_DESC_C); - - ant_value_t intl = js_mkobj(js); - ant_value_t dtf_ctor = js_heavy_mkfun(js, intl_dtf_constructor, js_mkundef()); - ant_value_t segmenter_ctor = js_heavy_mkfun(js, intl_segmenter_constructor, js_mkundef()); - - js_mark_constructor(dtf_ctor, true); - js_mark_constructor(segmenter_ctor, true); - - js_set(js, intl, "DateTimeFormat", dtf_ctor); - js_set(js, intl, "Segmenter", segmenter_ctor); - js_set(js, global, "Intl", intl); } diff --git a/src/modules/intl.c b/src/modules/intl.c new file mode 100644 index 0000000..6d21655 --- /dev/null +++ b/src/modules/intl.c @@ -0,0 +1,478 @@ +#include // IWYU pragma: keep + +#include +#include +#include +#include +#include + +#include "ant.h" +#include "errors.h" +#include "runtime.h" +#include "internal.h" +#include "descriptors.h" + +#include "modules/intl.h" +#include "modules/symbol.h" + +static ant_value_t g_intl_collator_proto = 0; +static ant_value_t g_intl_numberformat_proto = 0; +static ant_value_t g_intl_datetimeformat_proto = 0; +static ant_value_t g_intl_segmenter_proto = 0; + +typedef struct { + int hour12; + int minute; + int second; + const char *day_period; +} intl_dtf_fields_t; + +static ant_value_t intl_create_instance(ant_t *js, ant_value_t fallback_proto) { + ant_value_t obj = js_mkobj(js); + ant_value_t proto = js_instance_proto_from_new_target(js, fallback_proto); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + return obj; +} + +// TODO: docs/exec-plans/tech-debt.md +static inline bool intl_ascii_is_alpha(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} + +static inline bool intl_ascii_is_digit(char c) { + return c >= '0' && c <= '9'; +} + +static inline bool intl_ascii_is_alnum(char c) { + return intl_ascii_is_alpha(c) || intl_ascii_is_digit(c); +} + +static inline char intl_ascii_lower(char c) { + return (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : c; +} + +static bool intl_ascii_all(const char *s, size_t len, bool (*pred)(char)) { + if (!s || len == 0) return false; + for (size_t i = 0; i < len; i++) if (!pred(s[i])) return false; + return true; +} + +static bool intl_is_valid_language_tag(const char *tag, size_t len) { + if (!tag || len == 0) return false; + + size_t start = 0; + size_t end = 0; + while (end < len && tag[end] != '-') end++; + + size_t first_len = end - start; + if (first_len < 2 || first_len > 8) return false; + if (!intl_ascii_all(tag, first_len, intl_ascii_is_alpha)) return false; + + bool need_extension_subtag = false; + bool in_private_use = false; + bool saw_private_use_subtag = false; + + while (end < len) { + start = end + 1; + if (start >= len) return false; + + end = start; + while (end < len && tag[end] != '-') end++; + + size_t subtag_len = end - start; + if (subtag_len == 0 || subtag_len > 8) return false; + + const char *subtag = tag + start; + if (!intl_ascii_all(subtag, subtag_len, intl_ascii_is_alnum)) return false; + + if (in_private_use) { + saw_private_use_subtag = true; + continue; + } + + if (need_extension_subtag) { + if (subtag_len < 2) return false; + need_extension_subtag = false; + continue; + } + + if (subtag_len == 1) { + char singleton = intl_ascii_lower(subtag[0]); + if (singleton == 'x') in_private_use = true; + else need_extension_subtag = true; + } + } + + if (need_extension_subtag) return false; + if (in_private_use && !saw_private_use_subtag) return false; + + return true; +} + +static ant_value_t intl_resolve_locale(ant_t *js, ant_value_t input) { + if (vtype(input) == T_ARR) input = js_get(js, input, "0"); + if (vtype(input) == T_UNDEF) return js_mkstr(js, "en-US", 5); + + ant_value_t locale = js_tostring_val(js, input); + if (is_err(locale)) return locale; + + size_t len = 0; + const char *tag = js_getstr(js, locale, &len); + if (!intl_is_valid_language_tag(tag, len)) + return js_mkerr_typed(js, JS_ERR_RANGE, "Invalid language tag"); + + return locale; +} + +static ant_value_t intl_get_option_string(ant_t *js, ant_value_t options, const char *key, const char *fallback) { + if (vtype(options) != T_OBJ) return js_mkstr(js, fallback, strlen(fallback)); + + ant_value_t value = js_get(js, options, key); + if (vtype(value) == T_UNDEF) return js_mkstr(js, fallback, strlen(fallback)); + + ant_value_t str = js_tostring_val(js, value); + if (is_err(str)) return str; + + size_t len = 0; + const char *ptr = js_getstr(js, str, &len); + if (!ptr || len == 0) return js_mkstr(js, fallback, strlen(fallback)); + + return str; +} + +static ant_value_t intl_collator_compare(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t left = js_tostring_val(js, nargs > 0 ? args[0] : js_mkstr(js, "", 0)); + if (is_err(left)) return left; + + ant_value_t right = js_tostring_val(js, nargs > 1 ? args[1] : js_mkstr(js, "", 0)); + if (is_err(right)) return right; + + const char *left_str = js_getstr(js, left, NULL); + const char *right_str = js_getstr(js, right, NULL); + + int result = strcoll(left_str ? left_str : "", right_str ? right_str : ""); + if (result < 0) return js_mknum(-1); + if (result > 0) return js_mknum(1); + + return js_mknum(0); +} + +static ant_value_t intl_collator_resolved_options(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t obj = js_mkobj(js); + ant_value_t this_obj = js_getthis(js); + + ant_value_t locale = is_object_type(this_obj) + ? js_get(js, this_obj, "locale") + : js_mkundef(); + + if (vtype(locale) != T_STR) locale = js_mkstr(js, "en-US", 5); + js_set(js, obj, "locale", locale); + + return obj; +} + +static ant_value_t intl_numberformat_format(ant_t *js, ant_value_t *args, int nargs) { + double number = nargs > 0 ? js_to_number(js, args[0]) : 0.0; + ant_value_t raw_val = js_tostring_val(js, js_mknum(number)); + if (is_err(raw_val)) return raw_val; + + size_t raw_len = 0; + const char *raw = js_getstr(js, raw_val, &raw_len); + if (!raw || raw_len == 0) return js_mkstr(js, "0", 1); + + if ( + !isfinite(number) || + memchr(raw, 'e', raw_len) || + memchr(raw, 'E', raw_len) + ) return raw_val; + + const char *dot = memchr(raw, '.', raw_len); + size_t int_len = dot ? (size_t)(dot - raw) : raw_len; + size_t start = raw[0] == '-' ? 1 : 0; + size_t frac_len = dot ? (raw_len - int_len) : 0; + + char buf[128]; + size_t pos = 0; + if (start) buf[pos++] = '-'; + + for (size_t i = start; i < int_len; i++) { + buf[pos++] = raw[i]; + size_t remaining = int_len - 1 - i; + if (remaining > 0 && remaining % 3 == 0) buf[pos++] = ','; + } + + if (dot && frac_len > 0) { + memcpy(buf + pos, dot, frac_len); + pos += frac_len; + } + + buf[pos] = '\0'; + return js_mkstr(js, buf, pos); +} + +static ant_value_t intl_numberformat_resolved_options(ant_t *js, ant_value_t *args, int nargs) { + return intl_collator_resolved_options(js, args, nargs); +} + +static void intl_dtf_extract_fields(ant_t *js, ant_value_t *args, int nargs, intl_dtf_fields_t *out) { + time_t t = time(NULL); + if (nargs >= 1) t = (time_t)(js_to_number(js, args[0]) / 1000.0); + + struct tm local; +#ifdef _WIN32 + localtime_s(&local, &t); +#else + localtime_r(&t, &local); +#endif + + out->hour12 = local.tm_hour % 12; + if (out->hour12 == 0) out->hour12 = 12; + out->minute = local.tm_min; + out->second = local.tm_sec; + out->day_period = local.tm_hour < 12 ? "AM" : "PM"; +} + +static ant_value_t intl_dtf_format(ant_t *js, ant_value_t *args, int nargs) { + intl_dtf_fields_t fields; + intl_dtf_extract_fields(js, args, nargs, &fields); + + char buf[64]; + snprintf( + buf, sizeof(buf), "%d:%02d:%02d %s", + fields.hour12, fields.minute, fields.second, fields.day_period + ); + + return js_mkstr(js, buf, strlen(buf)); +} + +static ant_value_t intl_dtf_resolved_options(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t obj = js_mkobj(js); + ant_value_t this_obj = js_getthis(js); + + ant_value_t locale = is_object_type(this_obj) ? js_get(js, this_obj, "locale") : js_mkundef(); + ant_value_t time_zone = is_object_type(this_obj) ? js_get(js, this_obj, "timeZone") : js_mkundef(); + + if (vtype(locale) != T_STR) locale = js_mkstr(js, "en-US", 5); + if (vtype(time_zone) != T_STR) time_zone = js_mkstr(js, "UTC", 3); + + js_set(js, obj, "locale", locale); + js_set(js, obj, "timeZone", time_zone); + + return obj; +} + +static ant_value_t intl_dtf_make_part(ant_t *js, const char *type, const char *value) { + ant_value_t obj = js_mkobj(js); + js_set(js, obj, "type", js_mkstr(js, type, strlen(type))); + js_set(js, obj, "value", js_mkstr(js, value, strlen(value))); + return obj; +} + +static ant_value_t intl_dtf_format_to_parts(ant_t *js, ant_value_t *args, int nargs) { + intl_dtf_fields_t fields; + intl_dtf_extract_fields(js, args, nargs, &fields); + + char hour[8]; + char minute[8]; + char second[8]; + + snprintf(hour, sizeof(hour), "%d", fields.hour12); + snprintf(minute, sizeof(minute), "%02d", fields.minute); + snprintf(second, sizeof(second), "%02d", fields.second); + + ant_value_t parts = js_mkarr(js); + js_arr_push(js, parts, intl_dtf_make_part(js, "hour", hour)); + js_arr_push(js, parts, intl_dtf_make_part(js, "literal", ":")); + js_arr_push(js, parts, intl_dtf_make_part(js, "minute", minute)); + js_arr_push(js, parts, intl_dtf_make_part(js, "literal", ":")); + js_arr_push(js, parts, intl_dtf_make_part(js, "second", second)); + js_arr_push(js, parts, intl_dtf_make_part(js, "literal", " ")); + js_arr_push(js, parts, intl_dtf_make_part(js, "dayPeriod", fields.day_period)); + + return parts; +} + +static size_t intl_utf8_segment_len(const char *input, size_t remaining) { + if (remaining == 0) return 0; + + const unsigned char *s = (const unsigned char *)input; + unsigned char c = s[0]; + size_t len = 1; + + if ((c & 0x80) == 0) return 1; + if ((c & 0xe0) == 0xc0) len = 2; + else if ((c & 0xf0) == 0xe0) len = 3; + else if ((c & 0xf8) == 0xf0) len = 4; + + if (len > remaining) return 1; + for (size_t i = 1; i < len; i++) if ((s[i] & 0xc0) != 0x80) return 1; + + return len; +} + +static bool intl_ascii_is_word_byte(const char *segment, size_t len) { + if (len != 1) return true; + + unsigned char c = (unsigned char)segment[0]; + return + (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + c == '_'; +} + +static const char *intl_segmenter_granularity(ant_t *js, ant_value_t segmenter, size_t *len) { + ant_value_t granularity = js_get(js, segmenter, "granularity"); + if (vtype(granularity) != T_STR) { + if (len) *len = 8; + return "grapheme"; + } + + return js_getstr(js, granularity, len); +} + +static ant_value_t intl_segmenter_segment(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t input = nargs > 0 ? js_tostring_val(js, args[0]) : js_mkstr(js, "", 0); + if (is_err(input)) return input; + + size_t input_len = 0; + char *input_str = js_getstr(js, input, &input_len); + ant_value_t segments = js_mkarr(js); + + ant_value_t this_obj = js_getthis(js); + size_t granularity_len = 0; + const char *granularity = intl_segmenter_granularity(js, this_obj, &granularity_len); + bool word_granularity = granularity_len == 4 && memcmp(granularity, "word", 4) == 0; + + for (size_t offset = 0; offset < input_len;) { + size_t segment_len = intl_utf8_segment_len(input_str + offset, input_len - offset); + ant_value_t record = js_mkobj(js); + + js_set(js, record, "segment", js_mkstr(js, input_str + offset, segment_len)); + js_set(js, record, "index", js_mknum((double)offset)); + js_set(js, record, "input", input); + + if (word_granularity) js_set( + js, record, "isWordLike", + js_bool(intl_ascii_is_word_byte(input_str + offset, segment_len)) + ); + + js_arr_push(js, segments, record); + offset += segment_len; + } + + return segments; +} + +static ant_value_t intl_segmenter_resolved_options(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t obj = js_mkobj(js); + ant_value_t this_obj = js_getthis(js); + + size_t granularity_len = 0; + const char *granularity = intl_segmenter_granularity(js, this_obj, &granularity_len); + + ant_value_t locale = is_object_type(this_obj) ? js_get(js, this_obj, "locale") : js_mkundef(); + if (vtype(locale) != T_STR) locale = js_mkstr(js, "en-US", 5); + + js_set(js, obj, "locale", locale); + js_set(js, obj, "granularity", js_mkstr(js, granularity, granularity_len)); + + return obj; +} + +static ant_value_t intl_collator_constructor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t locale = intl_resolve_locale(js, nargs > 0 ? args[0] : js_mkundef()); + if (is_err(locale)) return locale; + + ant_value_t obj = intl_create_instance(js, g_intl_collator_proto); + js_set(js, obj, "locale", locale); + + return obj; +} + +static ant_value_t intl_numberformat_constructor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t locale = intl_resolve_locale(js, nargs > 0 ? args[0] : js_mkundef()); + if (is_err(locale)) return locale; + + ant_value_t obj = intl_create_instance(js, g_intl_numberformat_proto); + js_set(js, obj, "locale", locale); + + return obj; +} + +static ant_value_t intl_dtf_constructor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t locale = intl_resolve_locale(js, nargs > 0 ? args[0] : js_mkundef()); + if (is_err(locale)) return locale; + + ant_value_t time_zone = intl_get_option_string( + js, nargs > 1 ? args[1] : js_mkundef(), + "timeZone", "UTC" + ); + if (is_err(time_zone)) return time_zone; + + ant_value_t obj = intl_create_instance(js, g_intl_datetimeformat_proto); + js_set(js, obj, "locale", locale); + js_set(js, obj, "timeZone", time_zone); + + return obj; +} + +static ant_value_t intl_segmenter_constructor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t locale = intl_resolve_locale(js, nargs > 0 ? args[0] : js_mkundef()); + if (is_err(locale)) return locale; + + ant_value_t granularity = intl_get_option_string( + js, nargs > 1 ? args[1] : js_mkundef(), + "granularity", "grapheme" + ); + if (is_err(granularity)) return granularity; + + ant_value_t obj = intl_create_instance(js, g_intl_segmenter_proto); + js_set(js, obj, "locale", locale); + js_set(js, obj, "granularity", granularity); + + return obj; +} + +void init_intl_module(void) { + ant_t *js = rt->js; + + ant_value_t global = js_glob(js); + ant_value_t intl = js_mkobj(js); + ant_value_t object_proto = js->sym.object_proto; + + if (is_object_type(object_proto)) js_set_proto_init(intl, object_proto); + js_set_sym(js, intl, get_toStringTag_sym(), js_mkstr(js, "Intl", 4)); + + g_intl_collator_proto = js_mkobj(js); + js_set(js, g_intl_collator_proto, "compare", js_mkfun(intl_collator_compare)); + js_set(js, g_intl_collator_proto, "resolvedOptions", js_mkfun(intl_collator_resolved_options)); + js_set_sym(js, g_intl_collator_proto, get_toStringTag_sym(), js_mkstr(js, "Intl.Collator", 13)); + ant_value_t collator_ctor = js_make_ctor(js, intl_collator_constructor, g_intl_collator_proto, "Collator", 8); + js_set(js, intl, "Collator", collator_ctor); + + g_intl_numberformat_proto = js_mkobj(js); + js_set(js, g_intl_numberformat_proto, "format", js_mkfun(intl_numberformat_format)); + js_set(js, g_intl_numberformat_proto, "resolvedOptions", js_mkfun(intl_numberformat_resolved_options)); + js_set_sym(js, g_intl_numberformat_proto, get_toStringTag_sym(), js_mkstr(js, "Intl.NumberFormat", 17)); + ant_value_t numberformat_ctor = js_make_ctor(js, intl_numberformat_constructor, g_intl_numberformat_proto, "NumberFormat", 12); + js_set(js, intl, "NumberFormat", numberformat_ctor); + + g_intl_datetimeformat_proto = js_mkobj(js); + js_set(js, g_intl_datetimeformat_proto, "format", js_mkfun(intl_dtf_format)); + js_set(js, g_intl_datetimeformat_proto, "resolvedOptions", js_mkfun(intl_dtf_resolved_options)); + js_set(js, g_intl_datetimeformat_proto, "formatToParts", js_mkfun(intl_dtf_format_to_parts)); + js_set_sym(js, g_intl_datetimeformat_proto, get_toStringTag_sym(), js_mkstr(js, "Intl.DateTimeFormat", 19)); + ant_value_t dtf_ctor = js_make_ctor(js, intl_dtf_constructor, g_intl_datetimeformat_proto, "DateTimeFormat", 14); + js_set(js, intl, "DateTimeFormat", dtf_ctor); + + g_intl_segmenter_proto = js_mkobj(js); + js_set(js, g_intl_segmenter_proto, "segment", js_mkfun(intl_segmenter_segment)); + js_set(js, g_intl_segmenter_proto, "resolvedOptions", js_mkfun(intl_segmenter_resolved_options)); + js_set_sym(js, g_intl_segmenter_proto, get_toStringTag_sym(), js_mkstr(js, "Intl.Segmenter", 14)); + ant_value_t segmenter_ctor = js_make_ctor(js, intl_segmenter_constructor, g_intl_segmenter_proto, "Segmenter", 9); + js_set(js, intl, "Segmenter", segmenter_ctor); + + js_set(js, global, "Intl", intl); + js_set_descriptor(js, global, "Intl", 4, JS_DESC_W | JS_DESC_C); +} diff --git a/tests/test_intl.cjs b/tests/test_intl.cjs new file mode 100644 index 0000000..5becf89 --- /dev/null +++ b/tests/test_intl.cjs @@ -0,0 +1,42 @@ +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +assert(typeof Intl === 'object', 'expected Intl global'); +assert(Intl.constructor === Object, 'expected Intl to inherit from Object'); + +const collator = Intl.Collator('de-DE'); +assert(collator instanceof Intl.Collator, 'expected Intl.Collator() to create an instance'); +assert(typeof collator.compare === 'function', 'expected collator.compare'); +assert(typeof collator.resolvedOptions === 'function', 'expected collator.resolvedOptions'); + +const numberFormat = new Intl.NumberFormat('en-US'); +assert(numberFormat instanceof Intl.NumberFormat, 'expected NumberFormat instance'); +assert(numberFormat.format(1234567.5) === '1,234,567.5', `unexpected formatted number: ${numberFormat.format(1234567.5)}`); + +const dateTimeFormat = Intl.DateTimeFormat('en-US', { timeZone: 'Australia/Sydney' }); +assert(dateTimeFormat instanceof Intl.DateTimeFormat, 'expected DateTimeFormat() to create an instance'); +const dtfOptions = dateTimeFormat.resolvedOptions(); +assert(dtfOptions.timeZone === 'Australia/Sydney', `unexpected timeZone: ${dtfOptions.timeZone}`); +const dtfFormatted = dateTimeFormat.format(0); +const dtfParts = dateTimeFormat.formatToParts(0); +assert(Array.isArray(dtfParts), 'expected formatToParts() to return an array'); +assert(dtfParts.length === 7, `unexpected formatToParts() length: ${dtfParts.length}`); +assert(dtfParts.map(part => part.value).join('') === dtfFormatted, 'expected formatToParts() values to match format() output'); +assert(dtfParts[0].type === 'hour', `unexpected first part type: ${dtfParts[0].type}`); +assert(dtfParts[6].type === 'dayPeriod', `unexpected last part type: ${dtfParts[6].type}`); + +let rejected = false; +try { + Intl.Collator('x-en-US-12345'); +} catch (error) { + rejected = true; +} +assert(rejected, 'expected invalid language tags to throw'); + +const segmenter = Intl.Segmenter('en-US', { granularity: 'word' }); +const segments = segmenter.segment('ok'); +assert(Array.isArray(segments), 'expected Intl.Segmenter().segment() to return an array'); +assert(segments.length === 2, `unexpected segment count: ${segments.length}`); + +console.log('intl test passed');