mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Answer stream, StringDecoder and handle-object members in untyped code as Node does
This commit is contained in:
@@ -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";
|
||||
}
|
||||
@@ -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) {...} }`)
|
||||
|
||||
@@ -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({
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8232-native-streams-untyped.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8233-string-decoder-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8234-handle-typeof.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */ \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user