diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 15cab8778..3782f1e90 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -23,6 +23,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ ...(features.nodeIsland ? { nodeIsland: true } : {}), ...(features.moduleNamespace ? { moduleNamespace: true } : {}), ...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}), + ...(features.primitiveWrapper ? { primitiveWrapper: true } : {}), zlib: features.zlib, ...(features.brotli ? { brotli: true } : {}), assert: features.assert, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 8378fabea..a5099b99a 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -1272,6 +1272,9 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return host.emitGenericLibCall(e); case "nodeIsland": return host.emitGenericLibCall(e); + // Primitive wrapper objects (scr_js_wrapper.c). + case "wrapper": + return host.emitGenericLibCall(e); case "webstream": // A stream's start, pull and cancel steps run as promise jobs, so // a program with the classes runs its event loop. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 73fd0b799..284d5e9f4 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -307,6 +307,15 @@ export const LIB_FN_SYMS: Record = { "process.builtinUnsupported": "scr_process_builtin_unsupported", "module.namespaceValue": "scr_module_namespace_value", "module.namespaceDyn": "scr_module_namespace_dyn_value", + "wrapper.new": "scr_js_wrapper_new", + "wrapper.toObject": "scr_js_wrapper_to_object", + "wrapper.instanceOf": "scr_js_wrapper_instance_of", + "wrapper.objectTag": "scr_js_wrapper_object_tag", + "wrapper.typeIs": "scr_js_wrapper_type_is", + "wrapper.guard": "scr_js_wrapper_guard", + "wrapper.guardList": "scr_js_wrapper_guard_list", + "wrapper.isStringMethod": "scr_js_wrapper_is_string_method", + "wrapper.thisString": "scr_js_wrapper_this_string", "fs.callbackValue": "scr_fs_callback_value", "fs.callbackCall": "scr_fs_callback_call", "process.hrtimeValue": "scr_process_hrtime_value", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index ded050bce..c2e2fc194 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -43,6 +43,9 @@ export interface NativeLinkFeatures { /** A module namespace boxed for typed code links * scr_module_namespace_dyn.c; absent (false) otherwise. */ moduleNamespaceDyn?: boolean; + /** Primitive wrapper objects link scr_js_wrapper.c; absent (false) + * otherwise. */ + primitiveWrapper?: boolean; zlib: boolean; /** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */ brotli?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 5026fd5f2..d1c6b6b9b 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -170,6 +170,8 @@ export interface CcOptions { /** A module namespace boxed for typed code: compiles * scr_module_namespace_dyn.c. */ moduleNamespaceDyn?: boolean; + /** Primitive wrapper objects: compiles scr_js_wrapper.c. */ + primitiveWrapper?: boolean; /** The program uses the process-events surface (signal/exit listeners, * stdin events, for-await over stdin — moduleUsesProcessEvents on the * IR): compiles scr_events.c into the binary. Event-free binaries keep diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a650955d6..5d4388a21 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -212,6 +212,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"), // A module namespace as the frozen null-prototype object typed code sees. unit((u) => u.opts.moduleNamespaceDyn === true, "scr_module_namespace_dyn.c"), + // Primitive wrapper objects (new String/Number/Boolean, Object(primitive)). + unit((u) => u.opts.primitiveWrapper === true, "scr_js_wrapper.c"), unit( (u) => u.opts.dynAsync || diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 2ce4f21fe..b5005991c 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -105,6 +105,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { brotli: boolean; moduleNamespace: boolean; moduleNamespaceDyn: boolean; + primitiveWrapper: boolean; nativeFetch: boolean; abortSignalOnly: boolean; netEffective: boolean; @@ -316,6 +317,7 @@ export function effectiveRuntimeFeatures( brotli: features.brotli === true, moduleNamespace: features.moduleNamespace === true, moduleNamespaceDyn: features.moduleNamespaceDyn === true, + primitiveWrapper: features.primitiveWrapper === true, nativeFetch, // The fetch unit has AbortController and AbortSignal; scr_abort.c is // the same implementation without the fetch transport. diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 91fa7d791..295793d8b 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -68,6 +68,7 @@ export interface EarlyExecutableNativeFeatures { nodeIsland?: boolean; moduleNamespace?: boolean; moduleNamespaceDyn?: boolean; + primitiveWrapper?: boolean; zlib: boolean; brotli?: boolean; assert: boolean; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index b837b74fd..52fb2c8ed 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -703,6 +703,7 @@ async function compileExecutableNative( ...(features.nodeIsland ? { nodeIsland: true } : {}), ...(features.moduleNamespace ? { moduleNamespace: true } : {}), ...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}), + ...(features.primitiveWrapper ? { primitiveWrapper: true } : {}), zlib: features.zlib, ...(features.brotli ? { brotli: true } : {}), assert: features.assert, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 60f46e7ee..5078f6f11 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -754,6 +754,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "record.throwFrozen", "dyn.toString", "dyn.valueOf", + // Primitive wrapper objects (scr_js_wrapper.c): the conversions run + // user hooks, member reads run getters, and the guard refuses. + "wrapper.new", + "wrapper.toObject", + "wrapper.objectTag", + "wrapper.isStringMethod", + "wrapper.thisString", + "wrapper.guard", + "wrapper.guardList", "string.fromCodePoint", "dyn.defineProps", "dyn.definePrototypeProps", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 3287ac54a..eeb986bf6 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3682,6 +3682,32 @@ function buildLibFnSignatures() { * names, the compiled getter, and the Symbol.toStringTag key. Draws * Node's frozen null-prototype object, one per module. Never throws. */ "module.namespaceDyn": { argTypes: [STRING, arrayOf(STRING), DYN, DYN], result: DYN }, + /** A primitive wrapper object (scr_js_wrapper_new): the kind + * (0 String, 1 Number, 2 Boolean), the value it converts, and the three + * constructor values for the prototypes' `constructor`. */ + "wrapper.new": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, + /** ToObject (scr_js_wrapper_to_object): a wrapper for a primitive, the + * value for an object, a fresh object for null/undefined; then the + * three constructor values. */ + "wrapper.toObject": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + /** `value instanceof String/Number/Boolean` (scr_js_wrapper_instance_of): + * the kind's prototype on the value's chain. */ + "wrapper.instanceOf": { argTypes: [DYN, F64], result: BOOL }, + /** Object.prototype.toString with the [[PrimitiveValue]] brands + * (scr_js_wrapper_object_tag); dyn.objectTag otherwise. */ + "wrapper.objectTag": { argTypes: [DYN], result: STRING }, + /** util.types brands that read the slot (scr_js_wrapper_type_is). */ + "wrapper.typeIs": { argTypes: [DYN, STRING], result: BOOL }, + /** The value unchanged, or the not-supported Error when its own data + * graph holds a wrapper (scr_js_wrapper_guard; the string names the + * consumer). guardList checks each element. */ + "wrapper.guard": { argTypes: [DYN, STRING], result: DYN }, + "wrapper.guardList": { argTypes: [arrayOf(DYN), STRING], result: arrayOf(DYN) }, + /** Whether the member `name` of the value is the builtin + * String.prototype method (scr_js_wrapper_is_string_method), and the + * method's receiver, ToString of the value (scr_js_wrapper_this_string). */ + "wrapper.isStringMethod": { argTypes: [DYN, STRING], result: BOOL }, + "wrapper.thisString": { argTypes: [DYN, STRING], result: DYN }, "fs.callbackValue": { argTypes: [STRING], result: DYN }, "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, "process.hrtimeValue": { argTypes: [], result: DYN }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 28677f3ec..f2fa176c3 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -975,6 +975,11 @@ const RUNTIME_FEATURE_RULES = { "webstream.class", "webstream.instanceOf", "webstream.install", + // scr_js_wrapper.c answers other values' tags through + // scr_dyn_object_tag_of, and its prototype methods call scr_dyn_invoke. + "wrapper.objectTag", + "wrapper.new", + "wrapper.toObject", ], // Constant-key reads of Object.prototype member names answer the // inherited member through scr_dyn_invoke.c. @@ -1217,6 +1222,8 @@ const RUNTIME_FEATURE_RULES = { moduleNamespace: rule({ fns: ["module.namespaceValue"] }), /** A module namespace boxed for typed code (scr_module_namespace_dyn.c). */ moduleNamespaceDyn: rule({ fns: ["module.namespaceDyn"] }), + /** Primitive wrapper objects (scr_js_wrapper.c). */ + primitiveWrapper: rule({ prefixes: ["wrapper."] }), /** The WebAssembly API (scr_wasm.c). */ wasm: rule({ prefixes: ["wasm."] }), /** crypto.subtle importKey/sign/verify/digest and CryptoKey: @@ -1318,6 +1325,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stdioExtended: false, moduleNamespace: false, moduleNamespaceDyn: false, + primitiveWrapper: false, stream: false, streamDyn: false, handleProps: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index b1d03b52d..b123d0bbf 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -36,6 +36,7 @@ const features = { brotli: false, moduleNamespace: false, moduleNamespaceDyn: false, + primitiveWrapper: false, regex: true, copying: false, textDecoderLegacy: false, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 87d42ac5f..5697a0c27 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -100,6 +100,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_module_namespace.c", "moduleNamespace"], // The same namespace as the frozen null-prototype object typed code sees. ["scr_module_namespace_dyn.c", "moduleNamespaceDyn"], + // Primitive wrapper objects. + ["scr_js_wrapper.c", "primitiveWrapper"], // Blob and File box their reads as dyn promises through scr_async_dyn.c. ["scr_blob.c", "dynAsync"], ["scr_zlib.c", "zlib"], diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index f4bc8db68..d2ee9ccb3 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1447,6 +1447,24 @@ ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d); bool scr_dyn_bytes_is(const ScrDyn *d, int elem); bool scr_dyn_typed_array_is(const ScrDyn *d, int elem); bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe); + +/* Primitive wrapper objects (scr_js_wrapper.c, linked only into programs + * that create one): `new String/Number/Boolean` (kind 0/1/2) and ToObject, + * with the compiled String, Number and Boolean constructor values; the + * instanceof, Object.prototype.toString and util.types answers that read + * the [[PrimitiveValue]] slot; and the guard of the consumers that cannot + * read it yet. */ +ScrDyn *scr_js_wrapper_new(double kind, ScrDyn *value, ScrDyn *string_ctor, ScrDyn *number_ctor, + ScrDyn *boolean_ctor); +ScrDyn *scr_js_wrapper_to_object(ScrDyn *value, ScrDyn *string_ctor, ScrDyn *number_ctor, + ScrDyn *boolean_ctor); +bool scr_js_wrapper_instance_of(ScrDyn *value, double kind); +ScrStr *scr_js_wrapper_object_tag(ScrDyn *value); +bool scr_js_wrapper_type_is(ScrDyn *value, ScrStr *probe); +ScrDyn *scr_js_wrapper_guard(ScrDyn *value, ScrStr *consumer); +ScrArr *scr_js_wrapper_guard_list(ScrArr *values, ScrStr *consumer); +bool scr_js_wrapper_is_string_method(ScrDyn *value, ScrStr *name); +ScrDyn *scr_js_wrapper_this_string(ScrDyn *value, ScrStr *name); ScrDyn *scr_dyn_util_type_value(const ScrDyn *value, const ScrStr *probe); void scr_dyn_arr_push(ScrDyn *arr, ScrDyn *item); /* Spread completion for a runtime-arity argument list (`f(...xs)` in the diff --git a/packages/runtime/src/scr_js_wrapper.c b/packages/runtime/src/scr_js_wrapper.c new file mode 100644 index 000000000..da218a173 --- /dev/null +++ b/packages/runtime/src/scr_js_wrapper.c @@ -0,0 +1,516 @@ +/* Primitive wrapper objects: `new String(s)`, `new Number(n)`, + * `new Boolean(b)` and ToObject of a primitive (`Object(p)`). + * + * A wrapper is an ordinary dictionary-mode object (SCR_DYN_OBJ). Its + * prototype is this unit's String, Number or Boolean prototype object, and + * its [[PrimitiveValue]] internal slot rides the object's source identity + * (scr_dyn_new_obj_with_identity): a ScrJsWrapperSlot that only this unit's + * accessor recognizes (scr_dyn_obj_source_cast), so no shared structure or + * branch changes and a program that never creates a wrapper links nothing + * of this unit. A String wrapper owns its index properties (non-writable, + * enumerable, non-configurable) and `length` (non-writable, non-enumerable, + * non-configurable), as Node's String exotic objects do. + * + * Every generic dynamic operation already handles that shape: ToPrimitive + * finds valueOf and toString on the prototype, property reads and writes + * honor the attributes, key listing orders the indices first, identity is + * the object's, typeof answers "object" and ToBoolean true. The prototype + * methods read the slot (thisStringValue, thisNumberValue, + * thisBooleanValue) or, for the other String methods, apply ToString to the + * receiver and call the method on the primitive. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +enum { SCR_JS_WRAPPER_STRING = 0, SCR_JS_WRAPPER_NUMBER = 1, SCR_JS_WRAPPER_BOOLEAN = 2 }; + +static const char *const scr_js_wrapper_names[3] = { "String", "Number", "Boolean" }; + +typedef struct ScrJsWrapperSlot { + size_t rc; + int kind; + ScrDyn *primitive; /* owned */ + ScrDyn *object; /* borrowed: the wrapper that owns this slot */ +} ScrJsWrapperSlot; + +static SCR_TL ScrDyn *scr_js_wrapper_prototypes[3]; +static SCR_TL bool scr_js_wrapper_cleanup_registered; + +static void *scr_js_wrapper_slot_retain(void *p) { + ((ScrJsWrapperSlot *)p)->rc++; + return p; +} + +/* The source-identity accessor: `false` releases one reference (the + * object's release protocol); `true` asks for the object view, which is the + * wrapper itself (+1). Its address is the wrapper brand. */ +static ScrDyn *scr_js_wrapper_slot_access(void *p, bool materialize) { + ScrJsWrapperSlot *slot = p; + if (materialize) return scr_dyn_retain(slot->object); + if (--slot->rc == 0) { + scr_dyn_release(slot->primitive); + free(slot); + } + return NULL; +} + +/* The wrapper slot of `d` (+1 reference), or NULL when `d` is not a + * wrapper. */ +static ScrJsWrapperSlot *scr_js_wrapper_slot_of(const ScrDyn *d) { + return scr_dyn_obj_source_cast(d, scr_js_wrapper_slot_access, scr_js_wrapper_slot_retain); +} + +static void scr_js_wrapper_slot_drop(ScrJsWrapperSlot *slot) { + if (slot) scr_js_wrapper_slot_access(slot, false); +} + +static void scr_js_wrapper_cleanup(void) { + for (int i = 0; i < 3; i++) { + ScrDyn *prototype = scr_js_wrapper_prototypes[i]; + scr_js_wrapper_prototypes[i] = NULL; + scr_dyn_release(prototype); + } +} + +static void scr_js_wrapper_type_error(const char *method, int kind) { + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, scr_js_wrapper_names[kind]); + scr_jb_puts(&text, ".prototype."); + scr_jb_puts(&text, method); + scr_jb_puts(&text, " requires that 'this' be a"); + scr_jb_puts(&text, kind == SCR_JS_WRAPPER_STRING ? " String" : kind == SCR_JS_WRAPPER_NUMBER ? " Number" : " Boolean"); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); +} + +/* thisStringValue / thisNumberValue / thisBooleanValue: the primitive of + * `self` (+1) when it is that primitive or a wrapper of that kind, else + * Node's TypeError naming `method` and NULL. */ +static ScrDyn *scr_js_wrapper_this_value(const ScrDyn *self, int kind, const char *method) { + ScrDynKind want = kind == SCR_JS_WRAPPER_STRING ? SCR_DYN_STR + : kind == SCR_JS_WRAPPER_NUMBER ? SCR_DYN_NUM : SCR_DYN_BOOL; + if (self->kind == want) return scr_dyn_retain((ScrDyn *)self); + ScrJsWrapperSlot *slot = scr_js_wrapper_slot_of(self); + if (slot && slot->kind == kind) { + ScrDyn *primitive = scr_dyn_retain(slot->primitive); + scr_js_wrapper_slot_drop(slot); + return primitive; + } + scr_js_wrapper_slot_drop(slot); + scr_js_wrapper_type_error(method, kind); + return NULL; +} + +/* The ToIntegerOrInfinity-style numeric argument `i` (undefined when + * absent); false with the conversion's exception pending. */ +static bool scr_js_wrapper_number_arg(ScrDyn *const *args, size_t argc, size_t i, double *out, + bool *present) { + *present = i < argc && args[i]->kind != SCR_DYN_UNDEF; + *out = 0; + if (!*present) return true; + return scr_dyn_number_coerce_js(args[i], out); +} + +static ScrDyn *scr_js_wrapper_string_result(ScrStr *text) { + if (!text) return NULL; + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +} + +/* Method `method` of kind `kind` applied to its primitive receiver. + * valueOf and String's toString answer the primitive; Number's methods + * format through the runtime's number formatters; Boolean's toString + * spells the value. The String methods the dynamic dispatch implements + * (at, slice, indexOf, lastIndexOf, includes) run there; any other String + * method called through its function value (`String.prototype.trim.call`, + * a method value stored and called later) refuses, because the compiler + * lowers those calls itself at their call sites. */ +static ScrDyn *scr_js_wrapper_apply(int kind, const char *method, ScrDyn *primitive, + ScrDyn *const *args, size_t argc) { + if (strcmp(method, "valueOf") == 0) return scr_dyn_retain(primitive); + if (kind == SCR_JS_WRAPPER_BOOLEAN) { + const char *text = primitive->v.b ? "true" : "false"; + return scr_js_wrapper_string_result(scr_str_new(text, strlen(text))); + } + if (kind == SCR_JS_WRAPPER_NUMBER) { + double x = primitive->v.num, digits; + bool present; + if (strcmp(method, "toString") == 0) + return scr_js_wrapper_string_result( + scr_num_to_string_radix(x, argc > 0 ? args[0] : scr_dyn_undefined())); + if (!scr_js_wrapper_number_arg(args, argc, 0, &digits, &present)) return NULL; + if (strcmp(method, "toFixed") == 0) { + ScrStr *text = scr_num_to_fixed(x, digits); + if (scr_exc_pending()) { scr_str_release(text); return NULL; } + return scr_js_wrapper_string_result(text); + } + if (strcmp(method, "toPrecision") == 0) { + ScrStr *text = scr_num_to_precision(x, digits, present); + if (scr_exc_pending()) { scr_str_release(text); return NULL; } + return scr_js_wrapper_string_result(text); + } + if (strcmp(method, "toExponential") == 0) { + ScrStr *text = present ? scr_num_to_exponential_digits(x, digits) : scr_num_to_exponential(x); + if (scr_exc_pending()) { scr_str_release(text); return NULL; } + return scr_js_wrapper_string_result(text); + } + } else if (strcmp(method, "toString") == 0) { + return scr_dyn_retain(primitive); + } else if (strcmp(method, "at") == 0 || strcmp(method, "slice") == 0 || + strcmp(method, "indexOf") == 0 || strcmp(method, "lastIndexOf") == 0 || + strcmp(method, "includes") == 0) { + return scr_dyn_invoke(primitive, method, args, argc, method); + } + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, scr_js_wrapper_names[kind]); + scr_jb_puts(&text, ".prototype."); + scr_jb_puts(&text, method); + scr_jb_puts(&text, " called through its function value is not supported yet"); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return NULL; +} + +/* A prototype method: caps[0] holds the method name (a dyn string) and + * caps[1] the kind (a dyn number). valueOf, and toString of Number and + * Boolean and String, read the slot; Number's other methods run on + * thisNumberValue; String's other methods run on ToString of a receiver + * that is neither null nor undefined. Each then calls the method on the + * primitive through the ordinary dynamic dispatch. */ +static ScrDyn *scr_js_wrapper_method(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + ScrDyn *name = scr_box_get_ref(clo->caps[0]); /* +1 */ + ScrDyn *kind_box = scr_box_get_ref(clo->caps[1]); + int kind = (int)kind_box->v.num; + scr_dyn_release(kind_box); + const char *method = name->v.str->data; + ScrDyn *self = scr_dyn_this_get(); /* +1 */ + ScrDyn *primitive = NULL; + bool slot_method = kind != SCR_JS_WRAPPER_STRING || strcmp(method, "valueOf") == 0 || + strcmp(method, "toString") == 0; + if (slot_method) { + primitive = scr_js_wrapper_this_value(self, kind, method); + } else if (self->kind == SCR_DYN_UNDEF || self->kind == SCR_DYN_NULL) { + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "String.prototype."); + scr_jb_puts(&text, method); + scr_jb_puts(&text, " called on null or undefined"); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + } else { + ScrStr *text = scr_dyn_string_coerce_js(self); + if (text) { + primitive = scr_dyn_new_str(text); + scr_str_release(text); + } + } + scr_dyn_release(self); + ScrDyn *result = primitive ? scr_js_wrapper_apply(kind, method, primitive, args, argc) : NULL; + scr_dyn_release(primitive); + scr_dyn_release(name); + return result; +} + +/* Defines `key` on `object` as a non-enumerable, writable, configurable + * data property, the attributes of a builtin prototype member. */ +static void scr_js_wrapper_define_hidden(ScrDyn *object, const char *key, ScrDyn *value) { + size_t len = strlen(key); + scr_dyn_obj_set(object, key, len, value); + scr_dyn_obj_set_attrs_at(object, scr_dyn_obj_index_of(object, key, len), true, false, true); +} + +static ScrDyn *scr_js_wrapper_method_value(const char *name, int kind, uint32_t length) { + ScrClosure *closure = scr_closure_new(NULL, 2); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + ScrStr *text = scr_str_new(name, strlen(name)); + scr_box_set_ref(closure->caps[0], scr_dyn_new_str(text)); + scr_str_release(text); + closure->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[1], scr_dyn_new_num((double)kind)); + return scr_dyn_new_func(closure, scr_js_wrapper_method, length, "native:wrapper", name); +} + +/* The prototype members, with Node's `length` values. */ +typedef struct { const char *name; uint32_t length; } ScrJsWrapperMember; + +static const ScrJsWrapperMember scr_js_wrapper_string_members[] = { + {"anchor", 1}, {"at", 1}, {"big", 0}, {"blink", 0}, {"bold", 0}, {"charAt", 1}, + {"charCodeAt", 1}, {"codePointAt", 1}, {"concat", 1}, {"endsWith", 1}, {"fixed", 0}, + {"fontcolor", 1}, {"fontsize", 1}, {"includes", 1}, {"indexOf", 1}, {"isWellFormed", 0}, + {"italics", 0}, {"lastIndexOf", 1}, {"link", 1}, {"localeCompare", 1}, {"match", 1}, + {"matchAll", 1}, {"normalize", 0}, {"padEnd", 1}, {"padStart", 1}, {"repeat", 1}, + {"replace", 2}, {"replaceAll", 2}, {"search", 1}, {"slice", 2}, {"small", 0}, + {"split", 2}, {"startsWith", 1}, {"strike", 0}, {"sub", 0}, {"substr", 2}, + {"substring", 2}, {"sup", 0}, {"toLocaleLowerCase", 0}, {"toLocaleUpperCase", 0}, + {"toLowerCase", 0}, {"toString", 0}, {"toUpperCase", 0}, {"toWellFormed", 0}, + {"trim", 0}, {"trimEnd", 0}, {"trimLeft", 0}, {"trimRight", 0}, {"trimStart", 0}, + {"valueOf", 0}, {NULL, 0}, +}; +static const ScrJsWrapperMember scr_js_wrapper_number_members[] = { + {"toExponential", 1}, {"toFixed", 1}, {"toPrecision", 1}, {"toString", 1}, + {"valueOf", 0}, {"toLocaleString", 0}, {NULL, 0}, +}; +static const ScrJsWrapperMember scr_js_wrapper_boolean_members[] = { + {"toString", 0}, {"valueOf", 0}, {NULL, 0}, +}; + +/* The kind's prototype object (borrowed), built on first use with + * `constructor` (the compiled constructor value) and the members above. + * Its own prototype is Object.prototype. */ +static ScrDyn *scr_js_wrapper_prototype(int kind, ScrDyn *constructor) { + if (scr_js_wrapper_prototypes[kind]) return scr_js_wrapper_prototypes[kind]; + if (!scr_js_wrapper_cleanup_registered) { + scr_atexit(scr_js_wrapper_cleanup); + scr_js_wrapper_cleanup_registered = true; + } + ScrDyn *prototype = scr_dyn_new_obj(); + scr_js_wrapper_define_hidden(prototype, "constructor", scr_dyn_retain(constructor)); + const ScrJsWrapperMember *members = kind == SCR_JS_WRAPPER_STRING ? scr_js_wrapper_string_members + : kind == SCR_JS_WRAPPER_NUMBER ? scr_js_wrapper_number_members + : scr_js_wrapper_boolean_members; + for (const ScrJsWrapperMember *m = members; m->name; m++) + scr_js_wrapper_define_hidden(prototype, m->name, scr_js_wrapper_method_value(m->name, kind, m->length)); + scr_js_wrapper_prototypes[kind] = prototype; + return prototype; +} + +/* The wrapper of kind `kind` over `primitive` (borrowed; already the + * converted primitive). +1. */ +static ScrDyn *scr_js_wrapper_make(int kind, ScrDyn *primitive, ScrDyn *const constructors[3]) { + ScrJsWrapperSlot *slot = malloc(sizeof *slot); + if (!slot) scr_trap("scriptc: out of memory\n"); + *slot = (ScrJsWrapperSlot){ 0, kind, scr_dyn_retain(primitive), NULL }; + /* scr_dyn_new_obj_with_identity retains once: the object's reference. */ + ScrDyn *object = scr_dyn_new_obj_with_identity(slot, scr_js_wrapper_slot_retain, scr_js_wrapper_slot_access); + slot->object = object; + object->prototype = scr_dyn_retain(scr_js_wrapper_prototype(kind, constructors[kind])); + if (kind == SCR_JS_WRAPPER_STRING) { + /* Each UTF-16 code unit is an own index property. */ + ScrStr *text = primitive->v.str; + double units = scr_str_utf16_len(text); + for (double i = 0; i < units; i++) { + ScrStr *code_unit = scr_str_char_at(text, i); /* +1 */ + ScrDyn *unit = scr_dyn_new_str(code_unit); + scr_str_release(code_unit); + char key[32]; + int key_len = snprintf(key, sizeof key, "%.0f", i); + scr_dyn_obj_set(object, key, (size_t)key_len, unit); + scr_dyn_obj_set_attrs_at(object, scr_dyn_obj_index_of(object, key, (size_t)key_len), false, true, false); + } + scr_dyn_obj_set(object, "length", 6, scr_dyn_new_num(units)); + scr_dyn_obj_set_attrs_at(object, scr_dyn_obj_index_of(object, "length", 6), false, false, false); + } + return object; +} + +/* `new String(v)` / `new Number(v)` / `new Boolean(v)`: kind 0, 1 or 2; + * `value` is the argument (the undefined value is converted like any + * other: callers pass "", 0 or false for an omitted argument). The + * constructors are the compiled String, Number and Boolean values. +1, or + * NULL with the conversion's exception pending. All arguments borrowed. */ +ScrDyn *scr_js_wrapper_new(double kind_number, ScrDyn *value, ScrDyn *string_ctor, ScrDyn *number_ctor, + ScrDyn *boolean_ctor) { + int kind = (int)kind_number; + ScrDyn *const constructors[3] = { string_ctor, number_ctor, boolean_ctor }; + ScrDyn *primitive = NULL; + if (kind == SCR_JS_WRAPPER_STRING) { + if (value->kind == SCR_DYN_SYMBOL) { + static const char message[] = "Cannot convert a Symbol value to a string"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + primitive = scr_dyn_new_str(text); + scr_str_release(text); + } else if (kind == SCR_JS_WRAPPER_NUMBER) { + double number = scr_dyn_number_constructor(value); + if (scr_exc_pending()) return NULL; + primitive = scr_dyn_new_num(number); + } else { + primitive = scr_dyn_new_bool(scr_dyn_truthy(value)); + } + ScrDyn *object = scr_js_wrapper_make(kind, primitive, constructors); + scr_dyn_release(primitive); + return object; +} + +/* ToObject: a wrapper for a string, number or boolean; the value itself + * for an object; a fresh ordinary object for null and undefined (the + * Object constructor's answer, not ToObject's TypeError). BigInt and Symbol + * wrappers are refused. +1. */ +ScrDyn *scr_js_wrapper_to_object(ScrDyn *value, ScrDyn *string_ctor, ScrDyn *number_ctor, + ScrDyn *boolean_ctor) { + ScrDyn *const constructors[3] = { string_ctor, number_ctor, boolean_ctor }; + switch (value->kind) { + case SCR_DYN_STR: return scr_js_wrapper_make(SCR_JS_WRAPPER_STRING, value, constructors); + case SCR_DYN_NUM: return scr_js_wrapper_make(SCR_JS_WRAPPER_NUMBER, value, constructors); + case SCR_DYN_BOOL: return scr_js_wrapper_make(SCR_JS_WRAPPER_BOOLEAN, value, constructors); + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: return scr_dyn_new_obj(); + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: { + static const char message[] = "Object() of a bigint or symbol is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + default: return scr_dyn_retain(value); + } +} + +/* `value instanceof String/Number/Boolean`: the kind's prototype on the + * value's prototype chain. A primitive is never an instance; a prototype + * that was never built is on no chain. */ +bool scr_js_wrapper_instance_of(ScrDyn *value, double kind_number) { + ScrDyn *prototype = scr_js_wrapper_prototypes[(int)kind_number]; + if (!prototype || value->kind != SCR_DYN_OBJ) return false; + for (const ScrDyn *current = value->prototype; current; current = current->prototype) + if (current == prototype) return true; + return false; +} + +/* Object.prototype.toString: "[object String]", "[object Number]" or + * "[object Boolean]" for a wrapper without an own or inherited + * Symbol.toStringTag; the ordinary answer otherwise. +1. */ +ScrStr *scr_js_wrapper_object_tag(ScrDyn *value) { + ScrJsWrapperSlot *slot = scr_js_wrapper_slot_of(value); + if (!slot) return scr_dyn_object_tag_of(value); + int kind = slot->kind; + scr_js_wrapper_slot_drop(slot); + ScrStr *ordinary = scr_dyn_object_tag_of(value); + if (!ordinary) return NULL; + /* An explicit @@toStringTag wins over the builtin tag. */ + if (ordinary->len != 15 || memcmp(ordinary->data, "[object Object]", 15) != 0) return ordinary; + scr_str_release(ordinary); + const char *tag = kind == SCR_JS_WRAPPER_STRING ? "[object String]" + : kind == SCR_JS_WRAPPER_NUMBER ? "[object Number]" : "[object Boolean]"; + return scr_str_new(tag, strlen(tag)); +} + +/* util.types.isStringObject/isNumberObject/isBooleanObject/isBoxedPrimitive. */ +bool scr_js_wrapper_type_is(ScrDyn *value, ScrStr *probe) { + ScrJsWrapperSlot *slot = scr_js_wrapper_slot_of(value); + if (!slot) return false; + int kind = slot->kind; + scr_js_wrapper_slot_drop(slot); +#define PROBE(name) (probe->len == sizeof(name) - 1 && memcmp(probe->data, name, sizeof(name) - 1) == 0) + if (PROBE("isBoxedPrimitive")) return true; + if (PROBE("isStringObject")) return kind == SCR_JS_WRAPPER_STRING; + if (PROBE("isNumberObject")) return kind == SCR_JS_WRAPPER_NUMBER; + if (PROBE("isBooleanObject")) return kind == SCR_JS_WRAPPER_BOOLEAN; +#undef PROBE + return false; +} + +/* Whether `value`'s own data graph (array elements, own data properties of + * ordinary objects and arrays; accessors are not called) holds a wrapper. + * `depth` bounds cycles: a graph deeper than the limit is not searched + * further. */ +static bool scr_js_wrapper_found(const ScrDyn *value, int depth) { + if (depth > 64) return false; + if (value->kind == SCR_DYN_OBJ) { + ScrJsWrapperSlot *slot = scr_js_wrapper_slot_of(value); + if (slot) { + scr_js_wrapper_slot_drop(slot); + return true; + } + for (size_t i = 0; i < scr_dyn_obj_count(value); i++) { + ScrDynProp prop = scr_dyn_obj_prop_at(value, i); + if (!prop.accessor && prop.value && scr_js_wrapper_found(prop.value, depth + 1)) return true; + } + return false; + } + if (value->kind == SCR_DYN_ARR) { + for (size_t i = 0; i < value->v.arr.len; i++) + if (value->v.arr.items[i] && scr_js_wrapper_found(value->v.arr.items[i], depth + 1)) return true; + } + return false; +} + +static void scr_js_wrapper_refuse(const ScrStr *consumer) { + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_put_str(&text, consumer); + scr_jb_puts(&text, " of a primitive wrapper object (new String/Number/Boolean or Object(primitive)) is not supported yet"); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); +} + +/* The value (+1), or the not-supported Error and NULL when its own data + * graph holds a wrapper: the consumer named by `consumer` cannot read the + * [[PrimitiveValue]] slot yet. */ +ScrDyn *scr_js_wrapper_guard(ScrDyn *value, ScrStr *consumer) { + if (scr_js_wrapper_found(value, 0)) { + scr_js_wrapper_refuse(consumer); + return NULL; + } + return scr_dyn_retain(value); +} + +/* scr_js_wrapper_guard over each element of a list of dynamic values. */ +ScrArr *scr_js_wrapper_guard_list(ScrArr *values, ScrStr *consumer) { + for (size_t i = 0; i < values->len; i++) { + ScrDyn *value = scr_arr_get_ref(values, (double)i); /* +1 */ + bool found = scr_js_wrapper_found(value, 0); + scr_dyn_release(value); + if (found) { + scr_js_wrapper_refuse(consumer); + return NULL; + } + } + return scr_arr_retain(values); +} + +/* Whether `name` on `value` resolves to this unit's String.prototype + * method of that name: the compiler then runs its own lowering of the + * method on ToString of the receiver (scr_js_wrapper_this_string), which + * is what the builtin does. The read runs any getter on the chain, as the + * member read of a call does. */ +bool scr_js_wrapper_is_string_method(ScrDyn *value, ScrStr *name) { + if (value->kind != SCR_DYN_OBJ) return false; + ScrDyn *member = scr_dyn_obj_read(value, name->data, name->len); + if (!member) return false; + bool builtin = false; + if (member->kind == SCR_DYN_FUNC && member->v.fn.thunk == scr_js_wrapper_method) { + ScrDyn *member_name = scr_box_get_ref(member->v.fn.clo->caps[0]); + ScrDyn *kind = scr_box_get_ref(member->v.fn.clo->caps[1]); + builtin = (int)kind->v.num == SCR_JS_WRAPPER_STRING && member_name->v.str->len == name->len && + memcmp(member_name->v.str->data, name->data, name->len) == 0; + scr_dyn_release(kind); + scr_dyn_release(member_name); + } + scr_dyn_release(member); + return builtin; +} + +/* The receiver of a String.prototype method: ToString of a value that is + * neither undefined nor null (the method then sees the primitive). +1, or + * NULL with the TypeError or the conversion's exception pending. */ +ScrDyn *scr_js_wrapper_this_string(ScrDyn *value, ScrStr *name) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "String.prototype."); + scr_jb_put_str(&text, name); + scr_jb_puts(&text, " called on null or undefined"); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + return NULL; + } + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +}