Answer stream, StringDecoder and handle-object members in untyped code as Node does

This commit is contained in:
Chris Tate
2026-10-24 09:05:16 -05:00
parent 0dab283512
commit f2e710d65f
20 changed files with 437 additions and 35 deletions
+28 -17
View File
@@ -43,7 +43,7 @@ import {
isDynTypedRefType,
isRefCounted,
isUnitType,
recordTextCodecClass,
recordHandleCodecClass,
textCodecRecordClass,
typeKey,
} from "../../ir/ir.js";
@@ -98,14 +98,16 @@ export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
/** The handle tags of TextEncoder and TextDecoder records boxed as dyn
* (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = {
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder" | "StringDecoder", number> = {
TextDecoder: 76,
TextEncoder: 77,
StringDecoder: 78,
};
/** The decoder record fields scr_text_decoder_to_dyn reads, in its
* parameter order. */
const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const;
const STRING_DECODER_FIELDS = ["%enc", "%pending"] as const;
/** Handle kinds whose runtime unit boxes them through its own function,
* which installs the tag's ops on first use (scr_crypto.h). Programs that
@@ -1825,11 +1827,18 @@ export class LlDyn {
const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id));
if (codec !== null) {
// The record the codec's handle box holds (+1), or the path TypeError.
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
const checked = B.tmp();
B.line(
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
);
if (codec === "StringDecoder") {
host.declare(`declare ptr @scr_string_decoder_from_dyn(ptr, ptr, ptr)`);
B.line(
`${checked} = call ptr @scr_string_decoder_from_dyn(ptr %d, ptr %path, ptr ${want})`,
);
} else {
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
B.line(
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
);
}
B.terminate(`ret ptr ${checked}`);
this.defs.push(
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`,
@@ -2917,14 +2926,14 @@ export class LlDyn {
return matched;
}
/** A TextEncoder or TextDecoder record boxes as its handle (+1), which
* holds the record by reference. A decoder passes the byte offsets of the
* fields its boxed decode reads. */
/** A TextEncoder, TextDecoder or StringDecoder record boxes as its
* handle (+1), which holds the record by reference. The decoders pass
* the byte offsets of the fields their boxed methods read. */
private textCodecToDyn(
B: BlockBuilder,
t: IrType & { kind: "record" },
shape: IrRecordShape,
codec: "TextEncoder" | "TextDecoder",
codec: "TextEncoder" | "TextDecoder" | "StringDecoder",
): void {
const host = this.host;
const rc = vAdapters(host, t);
@@ -2939,10 +2948,12 @@ export class LlDyn {
const base = host.nullableUnions.fieldBase(t.shapeId);
const start = B.tmp();
B.line(`${start} = ptrtoint ptr %v to ${this.S}`);
const offsets = TEXT_DECODER_FIELDS.map((field) => {
const fields: readonly string[] =
codec === "StringDecoder" ? STRING_DECODER_FIELDS : TEXT_DECODER_FIELDS;
const offsets = fields.map((field) => {
const index = shape.fields.findIndex((f) => f.name === field);
if (index < 0)
throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`);
throw new InternalCompilerError(`llvm emitter bug: ${codec} record lacks ${field}`);
const p = B.tmp();
const at = B.tmp();
const offset = B.tmp();
@@ -2953,11 +2964,11 @@ export class LlDyn {
B.line(`${offset} = sub ${this.S} ${at}, ${start}`);
return `${this.S} ${offset}`;
});
host.declare(
`declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`,
);
const fn =
codec === "StringDecoder" ? "scr_string_decoder_to_dyn" : "scr_text_decoder_to_dyn";
host.declare(`declare ptr @${fn}(ptr, ptr, ptr, ${fields.map(() => this.S).join(", ")})`);
B.line(
`${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
`${r} = call ptr @${fn}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
);
}
B.terminate(`ret ptr ${r}`);
@@ -3137,7 +3148,7 @@ export class LlDyn {
const shape = host.recordsById.get(t.shapeId);
if (!shape)
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
const codec = recordTextCodecClass(shape);
const codec = recordHandleCodecClass(shape);
if (codec !== null) {
this.textCodecToDyn(B, t, shape, codec);
break;
@@ -89,7 +89,7 @@ export function emitDynamicExpr(
// Bytes already box their shared mutable storage directly. A second
// capsule would split identity between checked and native views.
const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className);
// A TextEncoder or TextDecoder record boxes as its own handle, which
// A TextEncoder, TextDecoder or StringDecoder record boxes as its own handle, which
// keeps the record's identity (toDynHelper).
const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null;
if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) {
@@ -1,7 +1,7 @@
import { typedRefConstructor } from "./shapes.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
import { streamRooted, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
import {
type IrRecordShape,
type IrType,
@@ -843,8 +843,7 @@ export function streamTypedRefMaterializeAdapter(
host.declare(`declare ptr @scr_error_dyn_fields(ptr)`);
B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`);
} else if (
meta.def.runtime &&
RUNTIME_STREAM_CLASSES.has(t.className) &&
((meta.def.runtime && RUNTIME_STREAM_CLASSES.has(t.className)) || streamRooted(meta)) &&
!meta.def.instancePrototypeHelper &&
!meta.def.prototypeDataHelper
) {
@@ -35,6 +35,7 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors
import { InternalCompilerError } from "../../../errors.js";
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
import { isTemporalGlobalRoot } from "../lower-namespaces.js";
import { isF64HandleTyped } from "../f64-handles.js";
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
@@ -287,7 +288,11 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
undefinedT: "undefined",
nullT: "object",
};
const folded = FOLD[operand.type.kind];
// An f64 handle (a readline Interface, a Timeout) is an object.
const folded =
operand.type.kind === "f64" && isF64HandleTyped(lowerer, expr.expression)
? "object"
: FOLD[operand.type.kind];
if (folded) {
const result: IrExpr = { kind: "strLit", value: folded, type: STRING, loc };
return isSafeToDiscard(operand)
@@ -0,0 +1,15 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
/** Whether `node`'s value is an f64 handle: an object whose class
* type-mapper.ts represents as an f64 id into a runtime registry (a
* readline Interface, a Timeout or Immediate, an AsyncLocalStorage, a
* diagnostics_channel Channel). Its checker type is an object type that
* maps to f64; the Number wrapper interface maps to f64 for primitives. */
export function isF64HandleTyped(lowerer: Lowerer, node: ts.Node): boolean {
const type = lowerer.checker.getTypeAtLocation(node);
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
const symbol = type.getSymbol();
if (symbol !== undefined && ts.symbolName(symbol) === "Number") return false;
return lowerer.mapTypeOf(type)?.kind === "f64";
}
+21 -5
View File
@@ -1781,16 +1781,32 @@ export function recordTextCodecClass(shape: IrRecordShape): "TextEncoder" | "Tex
return null;
}
/** The codec class of a TextEncoder or TextDecoder record type, else null.
* Such a record crosses an `unknown` slot as a native handle that holds the
* record itself (scr_text_codec_dyn.c), never as a copy or a field view. */
/** The class of a record that crosses checked-dynamic slots as a native
* handle: the codecs, and a StringDecoder (type-mapper.ts: its canonical
* encoding and the packing of its pending bytes). */
export function recordHandleCodecClass(
shape: IrRecordShape,
): "TextEncoder" | "TextDecoder" | "StringDecoder" | null {
const codec = recordTextCodecClass(shape);
if (codec !== null) return codec;
return shape.fields.length === 2 &&
shape.fields[0]!.name === "%enc" &&
shape.fields[1]!.name === "%pending"
? "StringDecoder"
: null;
}
/** The codec class of a TextEncoder, TextDecoder or StringDecoder record
* type, else null. Such a record crosses an `unknown` slot as a native
* handle that holds the record itself (scr_text_codec_dyn.c), never as a
* copy or a field view. */
export function textCodecRecordClass(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
): "TextEncoder" | "TextDecoder" | null {
): "TextEncoder" | "TextDecoder" | "StringDecoder" | null {
if (t.kind !== "record") return null;
const shape = getRecord(t.shapeId);
return shape ? recordTextCodecClass(shape) : null;
return shape ? recordHandleCodecClass(shape) : null;
}
/** Object-literal ACCESSOR properties (`{ get x() {...}, set x(v) {...} }`)
+4 -3
View File
@@ -2,6 +2,7 @@ import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef }
import type { IrLibFn } from "./builtin-signatures.js";
import {
isDirentShape,
recordHandleCodecClass,
recordTextCodecClass,
RUNTIME_EMITTER_CLASS,
RUNTIME_EVENT_CLASS,
@@ -586,7 +587,7 @@ function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean {
const textCodecLeaf: CarrierLeaf = (t, tables) => {
if (t.kind !== "record") return false;
const shape = tables.records.get(t.shapeId);
return shape !== undefined && recordTextCodecClass(shape) !== null;
return shape !== undefined && recordHandleCodecClass(shape) !== null;
};
const textDecoderLeaf: CarrierLeaf = (t, tables) => {
@@ -596,7 +597,7 @@ const textDecoderLeaf: CarrierLeaf = (t, tables) => {
};
/** Whether converting a value of type `t` to or from dyn converts a
* TextEncoder or TextDecoder record, which boxes it as a handle of
* TextEncoder, TextDecoder or StringDecoder record, which boxes it as a handle of
* scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */
function mentionsTextCodec(t: IrType, tables: TypeTables): boolean {
tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf);
@@ -858,7 +859,7 @@ const RUNTIME_FEATURE_RULES = {
(node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) ||
(node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())),
}),
/** A TextEncoder or TextDecoder record converts to or from an untyped
/** A TextEncoder, TextDecoder or StringDecoder record converts to or from an untyped
* value (its handle box, or a boxed function, record or collection that
* carries one): scr_text_codec_dyn.c. */
textCodecDyn: rule({
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8232-native-streams-untyped.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8233-string-decoder-values.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8234-handle-typeof.ts"
],
"diags": []
}
+6
View File
@@ -369,6 +369,12 @@ ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*relea
void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const struct ScrDynPath *path,
const char *want);
bool scr_text_codec_is(const ScrDyn *d, bool decoder);
/* StringDecoder records as checked-dynamic handles (the same unit): the
* byte offsets of the encoding (string) and pending (f64) fields. */
ScrDyn *scr_string_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *),
size_t encoding, size_t pending);
void *scr_string_decoder_from_dyn(const ScrDyn *d, const struct ScrDynPath *path,
const char *want);
bool scr_buffer_is_ascii(const ScrDyn *input);
bool scr_buffer_is_utf8(const ScrDyn *input);
ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to);
+1
View File
@@ -200,6 +200,7 @@ typedef enum {
X(SHARED_ARRAY_BUFFER, 37) \
X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \
X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \
X(STRING_DECODER, 78) /* compiled StringDecoder record (scr_text_codec_dyn.c) */ \
X(TEXT_DECODER, 76) /* compiled TextDecoder record (scr_text_codec_dyn.c) */ \
X(TEXT_ENCODER, 77) /* compiled TextEncoder record (scr_text_codec_dyn.c) */ \
X(URL, 28) /* native WHATWG URL */ \
+94 -2
View File
@@ -6634,6 +6634,12 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once,
return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, adapter,
error ? scr_ee_inv_fixed1 : scr_ee_inv_fixed0, once, prepend);
}
/* The stream emits 'pipe' and 'unpipe' with its source stream; other
* names reach the listener only through emit calls, with every argument
* they pass. */
bool piping = (name->len == 4 && memcmp(name->data, "pipe", 4) == 0) ||
(name->len == 6 && memcmp(name->data, "unpipe", 6) == 0);
if (!piping) return (ScrStream *)scr_emitter_on_flex((ScrEmitter *)s, name, cb, once, prepend);
static const char message[] = "this stream event has no checked-dynamic listener adapter";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
@@ -8058,10 +8064,68 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) {
/* The members a native stream answers to checked-dynamic calls. */
static const char *const scr_stream_dyn_methods[] = {
"on", "once", "addListener", "prependListener", "prependOnceListener",
"off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused",
"setEncoding", "write", "end", "destroy", "cork", "uncork",
"off", "removeListener", "removeAllListeners", "listenerCount", "eventNames", "pipe",
"unpipe", "read", "push", "unshift", "pause", "resume", "isPaused", "setEncoding", "write",
"end", "destroy", "cork", "uncork", "setDefaultEncoding",
};
/* An event name argument: a string, +1; other values throw the "not
* supported yet" Error. */
static ScrStr *scr_stream_dyn_event_name(const ScrDyn *value) {
if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str);
static const char message[] = "event names other than strings on a native stream are not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
/* push(chunk, encoding): a string with an encoding pushes the string's
* bytes in that encoding, undefined pushes an empty chunk outside object
* mode, and other chunks push as push(chunk) does. */
static ScrDyn *scr_stream_dyn_push(ScrStream *stream, ScrDyn *const *args, size_t argc) {
ScrDyn *chunk = argc ? args[0] : scr_dyn_undefined();
ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined();
if (chunk->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR) {
ScrStr *canonical = scr_bytes_normalize_encoding(encoding->v.str);
if (!canonical) return NULL;
bool more = scr_stream_push_str_enc(stream, chunk->v.str, canonical);
scr_str_release(canonical);
return scr_dyn_new_bool(more);
}
if (chunk->kind == SCR_DYN_UNDEF && !(stream->st->has_r && stream->st->r.object_entries)) {
ScrStr *empty = scr_str_new("", 0);
bool more = scr_stream_push_str(stream, empty);
scr_str_release(empty);
return scr_dyn_new_bool(more);
}
bool more = scr_stream_push_dyn(stream, chunk);
return scr_exc_pending() ? NULL : scr_dyn_new_bool(more);
}
/* unshift(chunk): whether the stream takes more data afterwards, Node's
* canPushMore. */
static ScrDyn *scr_stream_dyn_unshift(ScrStream *stream, const ScrDyn *chunk) {
scr_stream_unshift_dyn(stream, chunk);
if (scr_exc_pending()) return NULL;
ScrStreamState *st = stream->st;
bool more = chunk->kind != SCR_DYN_NULL && st->has_r && !st->destroyed && !st->errored &&
!st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0);
return scr_dyn_new_bool(more);
}
/* eventNames(): the stream's event names as a string array, +1. */
static ScrDyn *scr_stream_dyn_event_names(ScrStream *stream) {
ScrArr *names = scr_emitter_event_names((ScrEmitter *)stream);
ScrDyn *result = scr_dyn_new_arr();
double length = scr_arr_len(names);
for (double i = 0; i < length; i++) {
ScrStr *name = scr_arr_get_ref(names, i);
scr_dyn_arr_push(result, scr_dyn_new_str(name));
scr_str_release(name);
}
scr_arr_release(names);
return result;
}
/* A checked-dynamic call of one of a native stream's members (borrowed
* stream): the result, +1, with `this` results answering `self`, or the
* current `this` when self is NULL. */
@@ -8086,6 +8150,29 @@ static ScrDyn *scr_stream_dyn_call(ScrStream *stream, ScrDyn *self, const char *
argc > 1 ? args[1] : scr_dyn_undefined());
scr_emitter_release(registered);
}
} else if (!strcmp(method, "removeAllListeners")) {
bool all = first->kind == SCR_DYN_UNDEF;
ScrStr *name = all ? scr_str_new("", 0) : scr_stream_dyn_event_name(first);
if (name) {
scr_emitter_release(scr_emitter_remove_all((ScrEmitter *)stream, name, all));
scr_str_release(name);
}
} else if (!strcmp(method, "listenerCount")) {
ScrStr *name = scr_stream_dyn_event_name(first);
if (name) {
ScrDyn *only = argc > 1 ? args[1] : scr_dyn_undefined();
double count = only->kind == SCR_DYN_UNDEF || only->kind == SCR_DYN_NULL
? scr_emitter_listener_count((ScrEmitter *)stream, name)
: only->kind == SCR_DYN_FUNC ? scr_emitter_listener_count_dyn((ScrEmitter *)stream, name, only) : 0;
scr_str_release(name);
if (!scr_exc_pending()) result = scr_dyn_new_num(count);
}
} else if (!strcmp(method, "eventNames")) result = scr_stream_dyn_event_names(stream);
else if (!strcmp(method, "push")) result = scr_stream_dyn_push(stream, args, argc);
else if (!strcmp(method, "unshift")) result = scr_stream_dyn_unshift(stream, first);
else if (!strcmp(method, "setDefaultEncoding")) {
ScrStream *returned = scr_stream_set_default_encoding(stream, first);
if (returned) scr_stream_release(returned);
} else if (!strcmp(method, "pipe") || !strcmp(method, "unpipe")) {
ScrStream *destination = first->kind == SCR_DYN_UNDEF && !strcmp(method, "unpipe")
? NULL : scr_stream_view_receiver(first);
@@ -8220,6 +8307,11 @@ ScrDyn *scr_stream_dyn_view(ScrStream *stream) {
const char *name = scr_stream_dyn_properties[i];
scr_dyn_obj_set(view, name, strlen(name), scr_stream_dyn_property(stream, name));
}
if (stream->st->has_r) {
int flowing = stream->st->r.flowing;
scr_dyn_obj_set(view, "readableFlowing", 15,
flowing < 0 ? scr_dyn_new_null() : scr_dyn_new_bool(flowing == 1));
}
if (stream->st->native && stream->st->native_ops && stream->st->native_ops->view) {
ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2);
cb->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace);
+125 -3
View File
@@ -1,9 +1,10 @@
/* WHATWG TextEncoder and TextDecoder instances as checked-dynamic values
* (SCR_DYNH_TEXT_ENCODER, SCR_DYNH_TEXT_DECODER). A codec is a compiled
/* WHATWG TextEncoder and TextDecoder instances, and node:string_decoder
* StringDecoder instances, as checked-dynamic values (SCR_DYNH_TEXT_ENCODER,
* SCR_DYNH_TEXT_DECODER, SCR_DYNH_STRING_DECODER). A codec is a compiled
* record (type-mapper.ts): the handle holds that record by reference, so
* a codec stored in an untyped slot and read back keeps its identity and
* a decoder keeps its streaming state. The compiler passes the record's
* reference-count entry points and, for decoders, the byte offsets of the
* reference-count entry points and, for a TextDecoder, the byte offsets of the
* encoding, fatal, ignoreBOM and state fields when it boxes one. A unit
* of its own so programs that never box a codec link none of it
* (native-toolchain gating: the textCodecDyn runtime feature). */
@@ -213,3 +214,124 @@ bool scr_text_codec_is(const ScrDyn *d, bool decoder) {
return d->kind == SCR_DYN_HANDLE &&
d->v.handle.tag == (decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER);
}
/* node:string_decoder StringDecoder instances (SCR_DYNH_STRING_DECODER).
* The decoder is a compiled record of its canonical encoding (a string)
* and its pending partial sequence (an f64 packing, scr_bytes.c's
* scr_strdec_* helpers); the compiler passes both fields' byte offsets
* when it boxes one. */
typedef struct {
void *(*retain)(void *);
void (*release)(void *);
size_t encoding, pending;
} ScrStringDecoderLayout;
static SCR_TL ScrStringDecoderLayout scr_sdd_layout;
static void *scr_sdd_retain(void *rec) { return scr_sdd_layout.retain(rec); }
static void scr_sdd_release(void *rec) { scr_sdd_layout.release(rec); }
static ScrStr *scr_sdd_encoding(const void *rec) {
ScrStr *encoding;
memcpy(&encoding, (const char *)rec + scr_sdd_layout.encoding, sizeof encoding);
return encoding;
}
static double scr_sdd_pending(const void *rec) {
double pending;
memcpy(&pending, (const char *)rec + scr_sdd_layout.pending, sizeof pending);
return pending;
}
static void scr_sdd_set_pending(void *rec, double pending) {
memcpy((char *)rec + scr_sdd_layout.pending, &pending, sizeof pending);
}
/* write(buf): a string answers itself; an ArrayBuffer view decodes with
* the pending bytes before it; other values throw Node's
* ERR_INVALID_ARG_TYPE. +1 or NULL. */
static ScrStr *scr_sdd_write(void *rec, const ScrDyn *buf) {
if (buf->kind == SCR_DYN_STR) return scr_str_retain(buf->v.str);
if (buf->kind != SCR_DYN_BYTES) {
scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", buf);
return NULL;
}
ScrBytes *bytes = scr_bytes_raw_view(buf->v.bytes);
ScrStr *encoding = scr_sdd_encoding(rec);
double pending = scr_sdd_pending(rec);
ScrStr *text = scr_strdec_write(encoding, pending, bytes);
scr_sdd_set_pending(rec, scr_strdec_next(encoding, pending, bytes));
scr_bytes_release(bytes);
return text;
}
static ScrDyn *scr_sdd_invoke(void *rec, ScrDyn *self, const char *method, ScrDyn *const *args,
size_t argc, const char *what) {
(void)self;
ScrDyn *buf = scr_tcd_codec_arg(args, argc, 0);
if (strcmp(method, "write") == 0) {
ScrStr *text = scr_sdd_write(rec, buf);
return text ? scr_tcd_str(text) : NULL;
}
if (strcmp(method, "end") == 0) {
/* end(buf): write(buf) when buf is not undefined, then the pending
* bytes' replacement text. */
ScrStr *head = buf->kind == SCR_DYN_UNDEF ? scr_str_new("", 0) : scr_sdd_write(rec, buf);
if (!head) return NULL;
ScrStr *tail = scr_strdec_end(scr_sdd_encoding(rec), scr_sdd_pending(rec));
scr_sdd_set_pending(rec, 0);
ScrStr *text = scr_str_concat(head, tail);
scr_str_release(head);
scr_str_release(tail);
return scr_tcd_str(text);
}
if (strcmp(method, "text") == 0) {
static const char message[] = "StringDecoder.prototype.text is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
scr_tcd_codec_not_a_function(what);
return NULL;
}
static ScrDyn *scr_sdd_get(void *rec, const char *key, size_t len) {
if (len == 8 && memcmp(key, "encoding", 8) == 0)
return scr_dyn_new_str(scr_sdd_encoding(rec));
if ((len == 8 && memcmp(key, "lastChar", 8) == 0) ||
(len == 8 && memcmp(key, "lastNeed", 8) == 0) ||
(len == 9 && memcmp(key, "lastTotal", 9) == 0)) {
static const char message[] = "StringDecoder lastChar, lastNeed and lastTotal are not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
}
return NULL;
}
static const ScrDynHandleMethod scr_sdd_methods[] = {
{.name = "write", .length = 1},
{.name = "end", .length = 1},
{.name = "text", .length = 2},
{.name = NULL},
};
static const ScrDynHandleOps scr_sdd_ops = {
.cls = "StringDecoder",
.retain = scr_sdd_retain,
.release = scr_sdd_release,
.invoke = scr_sdd_invoke,
.get = scr_sdd_get,
.set = scr_tcd_codec_set,
.methods = scr_sdd_methods,
};
ScrDyn *scr_string_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *),
size_t encoding, size_t pending) {
if (!scr_sdd_layout.retain) {
scr_sdd_layout = (ScrStringDecoderLayout){retain, release, encoding, pending};
scr_dyn_handle_install(SCR_DYNH_STRING_DECODER, &scr_sdd_ops);
}
return scr_dyn_new_handle(rec, SCR_DYNH_STRING_DECODER);
}
void *scr_string_decoder_from_dyn(const ScrDyn *d, const ScrDynPath *path, const char *want) {
return scr_dyn_handle_unbox(d, SCR_DYNH_STRING_DECODER, path, want);
}
@@ -0,0 +1,56 @@
// Native streams passed to untyped helpers: push, unshift, the listener
// members, setDefaultEncoding and readableFlowing answer through the
// untyped value as on the typed one, also for a program subclass and in
// object mode.
const { Readable, Writable } = require("stream");
function feed(s) {
console.log("push", s.push("a"), s.push("b", "utf8"), s.unshift("z"));
console.log("counts", s.listenerCount("data"), s.eventNames().join(","));
s.on("custom", (x, y) => {
console.log("custom", x, y);
});
s.emit("custom", 1, 2);
s.removeAllListeners("custom");
console.log("after removeAll", s.listenerCount("custom"), s.readableFlowing);
s.push(null);
}
const r = new Readable({ read() {} });
const chunks = [];
r.on("data", (c) => chunks.push(String(c)));
r.on("end", () => console.log("end", chunks.join("|")));
feed(r);
class Source extends Readable {
_read() {}
}
const sub = new Source();
sub.on("data", (c) => console.log("source data", String(c)));
console.log("source push", sub instanceof Readable);
(function (s) {
console.log(s.push("from untyped"), s.readableLength);
s.push(null);
})(sub);
const objects = new Readable({ objectMode: true, read() {} });
console.log("flowing before", objects.readableFlowing);
objects.on("data", (c) => console.log("object data", c));
(function (s) {
console.log(s.push({ n: 1 }), s.push(undefined), s.push(2), s.readableFlowing);
s.push(null);
})(objects);
function drain(w) {
w.setDefaultEncoding("latin1");
w.write("é");
w.end();
}
const w = new Writable({
write(c, e, cb) {
console.log("wrote", c.length, e);
cb();
},
});
w.on("finish", () => console.log("finish"));
drain(w);
@@ -0,0 +1,33 @@
// A StringDecoder passed to untyped code keeps its identity and its
// pending bytes, and answers write, end and encoding as in Node.
const { StringDecoder } = require("string_decoder");
function feed(decoder, chunks) {
let out = "";
for (const chunk of chunks) out += decoder.write(chunk);
return out + decoder.end();
}
const euro = Buffer.from("€ and 😀");
const utf8 = new StringDecoder("utf-8");
const parts = [];
for (let i = 0; i < euro.length; i++) parts.push(euro.subarray(i, i + 1));
console.log(feed(utf8, parts), utf8.encoding);
const hex = new StringDecoder("hex");
console.log(feed(hex, [Buffer.from([1, 2]), Buffer.from([255])]), hex.encoding);
const utf16 = new StringDecoder("ucs2");
const pending = [Buffer.from([0x61]), Buffer.from([0x00, 0x62]), Buffer.from([0x00])];
console.log(JSON.stringify(feed(utf16, pending)), utf16.encoding);
function passThrough(d) {
return d;
}
const kept = passThrough(utf8);
console.log(kept === utf8, typeof kept, kept.write("text"), kept.end(Buffer.from([0xf0, 0x9f])));
try {
kept.write(null);
} catch (e) {
console.log(e.code, e.message);
}
+24
View File
@@ -0,0 +1,24 @@
// typeof of the objects the runtime keeps as numeric handles answers
// "object", as Node's does: readline interfaces, timers, async-local
// stores, diagnostics channels and event-loop histograms.
import * as readline from "node:readline";
import { PassThrough } from "node:stream";
import { AsyncLocalStorage } from "node:async_hooks";
import * as dc from "node:diagnostics_channel";
import { monitorEventLoopDelay } from "node:perf_hooks";
const rl = readline.createInterface({ input: new PassThrough(), terminal: false });
const timeout = setTimeout(() => {}, 1);
const interval = setInterval(() => {}, 1000);
const immediate = setImmediate(() => {});
const store = new AsyncLocalStorage<number>();
const channel = dc.channel("handles");
const tracing = dc.tracingChannel("handles");
const histogram = monitorEventLoopDelay();
const wrapped: Number = 5;
console.log(typeof rl, typeof timeout, typeof interval, typeof immediate);
console.log(typeof store, typeof channel, typeof tracing, typeof histogram, typeof wrapped);
clearTimeout(timeout);
clearInterval(interval);
clearImmediate(immediate);
rl.close();
@@ -0,0 +1 @@
{ "plain": 4000, "san": 5000 }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3000 }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 5500 }