Add the primitive wrapper unit and its gated runtime feature

scr_js_wrapper.c constructs `new String/Number/Boolean` wrappers and
ToObject of a primitive as ordinary dictionary-mode objects whose
prototype is the unit's String, Number or Boolean prototype, with the
[[PrimitiveValue]] slot on the object's source identity. The unit also
answers instanceof, Object.prototype.toString, the util.types brands,
the String method receiver, and the guard of the graph-walking consumers
that cannot read the slot yet. A new `primitiveWrapper` runtime feature
(any `wrapper.` libCall) links it, so a program that never creates a
wrapper links no byte of the unit and the zero-cost gate keeps fully
typed programs clean. The `wrapper.*` signatures, symbols and dispatch
case are registered here; no frontend emits them yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-29 05:38:47 -07:00
co-authored by Claude Opus 5.5
parent 686c34bd25
commit fd7be9993d
16 changed files with 604 additions and 0 deletions
@@ -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,
@@ -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.
@@ -307,6 +307,15 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
@@ -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;
@@ -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
@@ -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 ||
@@ -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.
@@ -68,6 +68,7 @@ export interface EarlyExecutableNativeFeatures {
nodeIsland?: boolean;
moduleNamespace?: boolean;
moduleNamespaceDyn?: boolean;
primitiveWrapper?: boolean;
zlib: boolean;
brotli?: boolean;
assert: boolean;
+1
View File
@@ -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,
@@ -754,6 +754,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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",
@@ -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 },
@@ -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,
@@ -36,6 +36,7 @@ const features = {
brotli: false,
moduleNamespace: false,
moduleNamespaceDyn: false,
primitiveWrapper: false,
regex: true,
copying: false,
textDecoderLegacy: false,
@@ -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"],
+18
View File
@@ -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
+516
View File
@@ -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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}