fix fetch stream parity edge cases

This commit is contained in:
Chris Tate
2026-07-31 15:58:29 -05:00
parent 07c131a55b
commit c4ee48ea03
7 changed files with 703 additions and 59 deletions
@@ -15,6 +15,31 @@ function streamTypedRefCommitAdapter(
snapshot: string,
defs: string[],
): string {
if (t.kind === "array") {
const commit = `${snapshot}_commit`;
E.walkerProtos.push(
`static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`,
);
defs.push(
`static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`,
` ScrArr *sc_target = (ScrArr *)sc_p;`,
` ScrArr *sc_next = ${E.dynCheckHelper(t)}(sc_d, NULL);`,
` if (!sc_next) return;`,
` size_t sc_target_len = sc_target->len;`,
` size_t sc_target_cap = sc_target->cap;`,
` uint64_t *sc_target_data = sc_target->data;`,
` sc_target->len = sc_next->len;`,
` sc_target->cap = sc_next->cap;`,
` sc_target->data = sc_next->data;`,
` sc_next->len = sc_target_len;`,
` sc_next->cap = sc_target_cap;`,
` sc_next->data = sc_target_data;`,
` scr_arr_release(sc_next);`,
`}`,
``,
);
return `&${commit}`;
}
if (t.kind !== "record") return "NULL";
const shape = E.recordsById.get(t.shapeId);
if (!shape) {
@@ -57,6 +82,132 @@ function streamTypedRefCommitAdapter(
return `&${commit}`;
}
interface StreamTypedRefAdapter {
snapshot: string;
commit: string;
}
interface StreamTypedRefContext {
prefix: string;
defs: string[];
adapters: Map<string, StreamTypedRefAdapter>;
}
function streamTypedRefEligible(t: IrType): boolean {
return t.kind === "record" || t.kind === "array";
}
/** Build the live dyn view of one typed stream value. Mutable reference
* children are capsules of their own, so a write through `v.child.x` or
* `v.items[0]` commits directly to that child's static source instead of
* disappearing into the parent's detached snapshot. */
function streamTypedRefAdapter(
E: CEmitter,
t: IrType,
ctx: StreamTypedRefContext,
preferredSnapshot?: string,
): StreamTypedRefAdapter {
const key = typeKey(t);
const existing = ctx.adapters.get(key);
if (existing) return existing;
const snapshot = preferredSnapshot ??
`${ctx.prefix}_nested_${ctx.adapters.size}`;
const adapter: StreamTypedRefAdapter = { snapshot, commit: "NULL" };
/* Register before walking children: recursive record/array types refer
* back to this prototype without recursively generating helpers. */
ctx.adapters.set(key, adapter);
E.walkerProtos.push(
`static ScrDyn *${snapshot}(void *sc_p); /* materialize live stream value ${key} */`,
);
adapter.commit = streamTypedRefCommitAdapter(E, t, snapshot, ctx.defs);
const box = (child: IrType, expr: string): string => {
if (!streamTypedRefEligible(child)) {
return `${E.toDynHelper(child)}(${expr})`;
}
const nested = streamTypedRefAdapter(E, child, ctx);
const rc = vAdapters(child);
const childKey = typeKey(child);
const keyLit = cStringLiteral(Buffer.from(childKey, "utf8"));
return `scr_dyn_new_typed_ref(${expr}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(childKey, "utf8")}, &${nested.snapshot}, ${nested.commit})`;
};
const lines = [
`static ScrDyn *${snapshot}(void *sc_p) { /* materialize live stream value ${key} */`,
` ${cDecl(t, "v")} = (${cType(t).trim()})sc_p;`,
];
if (t.kind === "record") {
const shape = E.recordsById.get(t.shapeId);
if (!shape) {
throw new Error(
`emitter bug: stream typed-ref materialize of unknown shape ${t.shapeId}`,
);
}
const ordinary = shape.indexValue || shape.fields.some(
(field) => field.name === "handleEvent" && field.type.kind === "func",
);
if (ordinary) {
lines.push(` return ${E.toDynHelper(t)}(v);`, `}`, ``);
ctx.defs.push(...lines);
return adapter;
}
if (shape.tuple) {
lines.push(` ScrDyn *d = scr_dyn_new_arr();`);
const fields = [...shape.fields].sort(
(a, b) => Number(a.name) - Number(b.name),
);
for (const field of fields) {
lines.push(
` scr_dyn_arr_push(d, ${box(field.type, `v->${mangleField(field.name)}`)});`,
);
}
} else {
lines.push(` ScrDyn *d = scr_dyn_new_obj();`);
const byName = new Map(shape.fields.map((field) => [field.name, field]));
const order = shape.declaredOrder ?? shape.fields.map((field) => field.name);
const inOrder = new Set(order);
const fields = [
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
...shape.fields.filter((field) => !inOrder.has(field.name)),
];
for (const field of fields) {
const keyLit = cStringLiteral(Buffer.from(field.name, "utf8"));
lines.push(
` scr_dyn_obj_set(d, ${keyLit}, ${Buffer.byteLength(field.name, "utf8")}, ${box(field.type, `v->${mangleField(field.name)}`)});`,
);
}
}
lines.push(` return d;`);
} else if (t.kind === "array") {
const elem = t.elem;
lines.push(
` ScrDyn *d = scr_dyn_new_arr();`,
` for (size_t sc_i = 0; sc_i < v->len; sc_i++) {`,
);
if (elem.kind === "f64") {
lines.push(
` scr_dyn_arr_push(d, ${box(elem, "scr_arr_get_f64(v, (double)sc_i)")});`,
);
} else if (elem.kind === "bool") {
lines.push(
` scr_dyn_arr_push(d, ${box(elem, "scr_arr_get_bool(v, (double)sc_i)")});`,
);
} else {
lines.push(
` ${cDecl(elem, "sc_value")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)sc_i);`,
` scr_dyn_arr_push(d, ${box(elem, "sc_value")});`,
` ${releaseCallC(elem, "sc_value")};`,
);
}
lines.push(` }`, ` return d;`);
} else {
lines.push(` return ${E.toDynHelper(t)}(v);`);
}
lines.push(`}`, ``);
ctx.defs.push(...lines);
return adapter;
}
function streamFromArrayAdapter(
E: CEmitter,
t: IrType & { kind: "array" },
@@ -88,15 +239,25 @@ function streamFromArrayAdapter(
const d: string[] = [];
let commit = "NULL";
if (typedRef) {
const snapshotSig = `static ScrDyn *${snapshot}(void *sc_p)`;
E.walkerProtos.push(`${snapshotSig}; /* materialize stream element ${key} */`);
d.push(
`${snapshotSig} {`,
` return ${E.toDynHelper(elem)}((${cType(elem).trim()})sc_p);`,
`}`,
``,
);
commit = streamTypedRefCommitAdapter(E, elem, snapshot, d);
if (streamTypedRefEligible(elem)) {
const adapter = streamTypedRefAdapter(
E,
elem,
{ prefix: snapshot, defs: d, adapters: new Map() },
snapshot,
);
commit = adapter.commit;
} else {
const snapshotSig = `static ScrDyn *${snapshot}(void *sc_p)`;
E.walkerProtos.push(`${snapshotSig}; /* materialize stream element ${key} */`);
d.push(
`${snapshotSig} {`,
` return ${E.toDynHelper(elem)}((${cType(elem).trim()})sc_p);`,
`}`,
``,
);
commit = streamTypedRefCommitAdapter(E, elem, snapshot, d);
}
}
const unionCommits = new Map<number, string>();
for (const { arm, tag } of unionRefArms) {
@@ -1130,10 +1130,48 @@ export function jsonWriteHelper(E: CEmitter, t: IrType): string {
d.push(
` if (d && d->kind == SCR_DYN_TYPED_REF) {`,
` ${cDecl(t, "sc_cached")} = (${cType(t).trim()})scr_dyn_typed_ref_cached_cast(d, ${keyLit}, ${keyLen});`,
` if (sc_cached) return sc_cached;`,
);
if (t.kind !== "record") {
d.push(` if (sc_cached) return sc_cached;`);
}
d.push(
` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`,
` ${cDecl(t, "sc_out")} = ${name}(sc_materialized, path);`,
` scr_dyn_release(sc_materialized);`,
);
if (t.kind === "record") {
const shape = E.recordsById.get(t.shapeId);
if (!shape) {
throw new Error(
`emitter bug: cached typed-ref cast of unknown shape ${t.shapeId}`,
);
}
d.push(` if (sc_out && sc_cached) {`);
for (const field of shape.fields) {
const member = mangleField(field.name);
d.push(
` {`,
` ${cDecl(field.type, "sc_old")} = sc_cached->${member};`,
` sc_cached->${member} = sc_out->${member};`,
` sc_out->${member} = sc_old;`,
` }`,
);
}
if (shape.indexValue) {
d.push(
` ScrMap *sc_old = sc_cached->${OVERFLOW_MEMBER};`,
` sc_cached->${OVERFLOW_MEMBER} = sc_out->${OVERFLOW_MEMBER};`,
` sc_out->${OVERFLOW_MEMBER} = sc_old;`,
);
}
d.push(
` ${releaseCallC(t, "sc_out")};`,
` return sc_cached;`,
` }`,
` if (sc_cached) ${releaseCallC(t, "sc_cached")};`,
);
}
d.push(
` if (sc_out) {`,
` scr_dyn_typed_ref_cache_cast((ScrDyn *)d, ${keyLit}, ${keyLen}, sc_out, &${rc.retain}, &${rc.release});`,
` }`,
@@ -1344,10 +1382,21 @@ export function jsonWriteHelper(E: CEmitter, t: IrType): string {
const keyLen = Buffer.byteLength(key, "utf8");
const rc = vAdapters(t);
d.push(` ${cDecl(t, "sc_cached")} = (${cType(t).trim()})scr_dyn_typed_ref_cached_cast(d, ${keyLit}, ${keyLen});`);
d.push(` if (sc_cached) return sc_cached;`);
d.push(` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`);
d.push(` ${cDecl(t, "sc_out")} = ${name}(sc_materialized, path);`);
d.push(` scr_dyn_release(sc_materialized);`);
d.push(` if (sc_out && sc_cached) {`);
d.push(` size_t sc_cached_rc = sc_cached->rc;`);
d.push(` size_t sc_out_rc = sc_out->rc;`);
d.push(` ScrUnion sc_old = *sc_cached;`);
d.push(` *sc_cached = *sc_out;`);
d.push(` sc_cached->rc = sc_cached_rc;`);
d.push(` *sc_out = sc_old;`);
d.push(` sc_out->rc = sc_out_rc;`);
d.push(` scr_union_release(sc_out);`);
d.push(` return sc_cached;`);
d.push(` }`);
d.push(` if (sc_cached) scr_union_release(sc_cached);`);
d.push(` if (sc_out) {`);
d.push(` scr_dyn_typed_ref_cache_cast((ScrDyn *)d, ${keyLit}, ${keyLen}, sc_out, &${rc.retain}, &${rc.release});`);
d.push(` }`);
+110 -24
View File
@@ -582,12 +582,14 @@ export class LlDyn {
B.line(`${cached} = call ptr @scr_dyn_typed_ref_cached_cast(ptr %d, ptr ${host.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")})`);
const hasCached = B.tmp();
B.line(`${hasCached} = icmp ne ptr ${cached}, null`);
const lCached = B.newLabel("dc.tr.hit");
const lMaterialize = B.newLabel("dc.tr.mat");
B.condBr(hasCached, lCached, lMaterialize);
B.startBlock(lCached);
B.terminate(`ret ptr ${cached}`);
B.startBlock(lMaterialize);
if (t.kind !== "record") {
const lCached = B.newLabel("dc.tr.hit");
const lMaterialize = B.newLabel("dc.tr.mat");
B.condBr(hasCached, lCached, lMaterialize);
B.startBlock(lCached);
B.terminate(`ret ptr ${cached}`);
B.startBlock(lMaterialize);
}
const materialized = B.tmp();
const checked = B.tmp();
B.line(`${materialized} = call ptr @scr_dyn_typed_ref_materialize(ptr %d)`);
@@ -595,14 +597,71 @@ export class LlDyn {
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
const ok = B.tmp();
B.line(`${ok} = icmp ne ptr ${checked}, null`);
const lCache = B.newLabel("dc.tr.put");
const lReturn = B.newLabel("dc.tr.ret");
B.condBr(ok, lCache, lReturn);
B.startBlock(lCache);
B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`);
B.br(lReturn);
B.startBlock(lReturn);
B.terminate(`ret ptr ${checked}`);
if (t.kind === "record") {
const shape = host.recordsById.get(t.shapeId);
if (!shape) {
throw new Error(
`llvm emitter bug: cached typed-ref cast of unknown shape ${t.shapeId}`,
);
}
const lOk = B.newLabel("dc.tr.ok");
const lBad = B.newLabel("dc.tr.bad");
B.condBr(ok, lOk, lBad);
B.startBlock(lBad);
const lBadDrop = B.newLabel("dc.tr.bad.drop");
const lBadRet = B.newLabel("dc.tr.bad.ret");
B.condBr(hasCached, lBadDrop, lBadRet);
B.startBlock(lBadDrop);
B.line(`call void ${releaseSym(host, t)}(ptr ${cached})`);
B.br(lBadRet);
B.startBlock(lBadRet);
B.terminate(`ret ptr null`);
B.startBlock(lOk);
const lRefresh = B.newLabel("dc.tr.refresh");
const lCache = B.newLabel("dc.tr.put");
B.condBr(hasCached, lRefresh, lCache);
B.startBlock(lRefresh);
const members = [
...shape.fields.map((field, index) => ({
index: index + 1,
type: llFieldType(field.type),
name: field.name,
})),
...(shape.indexValue
? [{
index: shape.fields.length + 1,
type: "ptr" as const,
name: "[key: string] overflow",
}]
: []),
];
members.forEach((member) => {
const cachedPtr = B.tmp();
const checkedPtr = B.tmp();
const oldValue = B.tmp();
const newValue = B.tmp();
B.line(`${cachedPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr ${cached}, i64 0, i32 ${member.index}`);
B.line(`${checkedPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr ${checked}, i64 0, i32 ${member.index}`);
B.line(`${oldValue} = load ${member.type}, ptr ${cachedPtr} ; ${member.name}`);
B.line(`${newValue} = load ${member.type}, ptr ${checkedPtr}`);
B.line(`store ${member.type} ${newValue}, ptr ${cachedPtr}`);
B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`);
});
B.line(`call void ${releaseSym(host, t)}(ptr ${checked})`);
B.terminate(`ret ptr ${cached}`);
B.startBlock(lCache);
B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`);
B.terminate(`ret ptr ${checked}`);
} else {
const lCache = B.newLabel("dc.tr.put");
const lReturn = B.newLabel("dc.tr.ret");
B.condBr(ok, lCache, lReturn);
B.startBlock(lCache);
B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`);
B.br(lReturn);
B.startBlock(lReturn);
B.terminate(`ret ptr ${checked}`);
}
B.startBlock(lPlain);
}
}
@@ -977,6 +1036,7 @@ export class LlDyn {
host.declare(`declare ptr @scr_dyn_typed_ref_cached_cast(ptr, ptr, i64)`);
host.declare(`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, i64, ptr, ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
host.declare(`declare void @scr_union_release(ptr)`);
const rc = vAdapters(host, t);
const kind = this.kindOf(B, "%d");
const capsule = B.tmp();
@@ -989,12 +1049,6 @@ export class LlDyn {
B.line(`${cached} = call ptr @scr_dyn_typed_ref_cached_cast(ptr %d, ptr ${host.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")})`);
const hasCached = B.tmp();
B.line(`${hasCached} = icmp ne ptr ${cached}, null`);
const lCached = B.newLabel("dcu.hit");
const lMaterialize = B.newLabel("dcu.mat");
B.condBr(hasCached, lCached, lMaterialize);
B.startBlock(lCached);
B.terminate(`ret ptr ${cached}`);
B.startBlock(lMaterialize);
const materialized = B.tmp();
const checked = B.tmp();
B.line(`${materialized} = call ptr @scr_dyn_typed_ref_materialize(ptr %d)`);
@@ -1002,13 +1056,45 @@ export class LlDyn {
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
const ok = B.tmp();
B.line(`${ok} = icmp ne ptr ${checked}, null`);
const lOk = B.newLabel("dcu.ok");
const lBad = B.newLabel("dcu.bad");
B.condBr(ok, lOk, lBad);
B.startBlock(lBad);
const lBadDrop = B.newLabel("dcu.bad.drop");
const lBadRet = B.newLabel("dcu.bad.ret");
B.condBr(hasCached, lBadDrop, lBadRet);
B.startBlock(lBadDrop);
B.line(`call void @scr_union_release(ptr ${cached})`);
B.br(lBadRet);
B.startBlock(lBadRet);
B.terminate(`ret ptr null`);
B.startBlock(lOk);
const lRefresh = B.newLabel("dcu.refresh");
const lCache = B.newLabel("dcu.put");
const lReturn = B.newLabel("dcu.ret");
B.condBr(ok, lCache, lReturn);
B.condBr(hasCached, lRefresh, lCache);
B.startBlock(lRefresh);
([
[1, "i32", "tag"],
[2, "ptr", "retain"],
[3, "ptr", "release"],
[4, "ptr", "trace"],
[5, "i64", "slot"],
] as const).forEach(([index, fieldType, fieldName]) => {
const cachedPtr = B.tmp();
const checkedPtr = B.tmp();
const oldValue = B.tmp();
const newValue = B.tmp();
B.line(`${cachedPtr} = getelementptr inbounds %ScrUnion, ptr ${cached}, i64 0, i32 ${index}`);
B.line(`${checkedPtr} = getelementptr inbounds %ScrUnion, ptr ${checked}, i64 0, i32 ${index}`);
B.line(`${oldValue} = load ${fieldType}, ptr ${cachedPtr} ; ${fieldName}`);
B.line(`${newValue} = load ${fieldType}, ptr ${checkedPtr}`);
B.line(`store ${fieldType} ${newValue}, ptr ${cachedPtr}`);
B.line(`store ${fieldType} ${oldValue}, ptr ${checkedPtr}`);
});
B.line(`call void @scr_union_release(ptr ${checked})`);
B.terminate(`ret ptr ${cached}`);
B.startBlock(lCache);
B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`);
B.br(lReturn);
B.startBlock(lReturn);
B.terminate(`ret ptr ${checked}`);
B.startBlock(lFail);
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
+248 -9
View File
@@ -88,6 +88,16 @@ import {
import { DK, LlDyn } from "./dyn.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import { LlWalkers } from "./walkers.js";
interface LlStreamTypedRefAdapter {
snapshot: string;
commit: string;
}
interface LlStreamTypedRefContext {
prefix: string;
adapters: Map<string, LlStreamTypedRefAdapter>;
}
import {
arrNewCall,
boxAccess,
@@ -9980,6 +9990,44 @@ class LlEmitter {
t: IrType,
snapshot: string,
): string {
if (t.kind === "array") {
const commit = `${snapshot}_commit`;
const check = this.dyn.dynCheckHelper(t);
this.declare(`declare void @scr_arr_release(ptr)`);
this.resolveThunkDefs.push(
`define internal void @${commit}(ptr %target, ptr %d) ${FN_ATTRS} { ; commit live stream element ${typeKey(t)}`,
`entry:`,
` %next = call ptr @${check}(ptr %d, ptr null)`,
` %missing = icmp eq ptr %next, null`,
` br i1 %missing, label %done, label %swap`,
`swap:`,
` %target_len_ptr = getelementptr inbounds %ScrArr, ptr %target, i64 0, i32 1`,
` %next_len_ptr = getelementptr inbounds %ScrArr, ptr %next, i64 0, i32 1`,
` %target_len = load i64, ptr %target_len_ptr`,
` %next_len = load i64, ptr %next_len_ptr`,
` store i64 %next_len, ptr %target_len_ptr`,
` store i64 %target_len, ptr %next_len_ptr`,
` %target_cap_ptr = getelementptr inbounds %ScrArr, ptr %target, i64 0, i32 2`,
` %next_cap_ptr = getelementptr inbounds %ScrArr, ptr %next, i64 0, i32 2`,
` %target_cap = load i64, ptr %target_cap_ptr`,
` %next_cap = load i64, ptr %next_cap_ptr`,
` store i64 %next_cap, ptr %target_cap_ptr`,
` store i64 %target_cap, ptr %next_cap_ptr`,
` %target_data_ptr = getelementptr inbounds %ScrArr, ptr %target, i64 0, i32 7`,
` %next_data_ptr = getelementptr inbounds %ScrArr, ptr %next, i64 0, i32 7`,
` %target_data = load ptr, ptr %target_data_ptr`,
` %next_data = load ptr, ptr %next_data_ptr`,
` store ptr %next_data, ptr %target_data_ptr`,
` store ptr %target_data, ptr %next_data_ptr`,
` call void @scr_arr_release(ptr %next)`,
` br label %done`,
`done:`,
` ret void`,
`}`,
``,
);
return `@${commit}`;
}
if (t.kind !== "record") return "null";
const shape = this.recordsById.get(t.shapeId);
if (!shape) {
@@ -10033,6 +10081,188 @@ class LlEmitter {
return `@${commit}`;
}
private streamTypedRefEligible(t: IrType): boolean {
return t.kind === "record" || t.kind === "array";
}
private streamTypedRefBoxValue(
B: BlockBuilder,
t: IrType,
value: string,
ctx: LlStreamTypedRefContext,
): string {
const boxed = B.tmp();
if (!this.streamTypedRefEligible(t)) {
const valueTy = t.kind === "f64"
? "double"
: t.kind === "bool"
? "i1"
: "ptr";
B.line(
`${boxed} = call ptr @${this.dyn.toDynHelper(t)}(${valueTy} ${value})`,
);
return boxed;
}
const nested = this.streamTypedRefMaterializeAdapter(t, ctx);
const rc = vAdapters(this, t);
const key = typeKey(t);
this.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, i64, ptr, ptr)`,
);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${this.cstr(key)}, i64 ${Buffer.byteLength(key, "utf8")}, ptr @${nested.snapshot}, ptr ${nested.commit})`,
);
return boxed;
}
/** LLVM twin of the C stream materializer: mutable reference children
* are their own typed capsules, so writes through a nested alias commit
* to the nested static source immediately. */
private streamTypedRefMaterializeAdapter(
t: IrType,
ctx: LlStreamTypedRefContext,
preferredSnapshot?: string,
): LlStreamTypedRefAdapter {
const key = typeKey(t);
const existing = ctx.adapters.get(key);
if (existing) return existing;
const snapshot = preferredSnapshot ??
`${ctx.prefix}_nested_${ctx.adapters.size}`;
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
ctx.adapters.set(key, adapter);
adapter.commit = this.streamTypedRefCommitAdapter(t, snapshot);
const B = new BlockBuilder();
if (t.kind === "record") {
const shape = this.recordsById.get(t.shapeId);
if (!shape) {
throw new Error(
`llvm emitter bug: stream typed-ref materialize of unknown shape ${t.shapeId}`,
);
}
/* Keep the ordinary converter for index-signature/listener records;
* their source-identity and overflow walks have extra contracts.
* Declared-field records and tuples cover the live stream values. */
const ordinary = shape.indexValue || shape.fields.some(
(field) => field.name === "handleEvent" && field.type.kind === "func",
);
if (ordinary) {
const out = B.tmp();
B.line(`${out} = call ptr @${this.dyn.toDynHelper(t)}(ptr %p)`);
B.terminate(`ret ptr ${out}`);
} else if (shape.tuple) {
this.declare(`declare ptr @scr_dyn_new_arr()`);
this.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
const fields = [...shape.fields].sort(
(a, b) => Number(a.name) - Number(b.name),
);
for (const field of fields) {
const index = shape.fields.indexOf(field) + 1;
const fieldPtr = B.tmp();
const fieldValue = B.tmp();
B.line(`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`);
B.line(`${fieldValue} = load ${llFieldType(field.type)}, ptr ${fieldPtr}`);
const boxed = this.streamTypedRefBoxValue(
B,
field.type,
fieldValue,
ctx,
);
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${boxed})`);
}
B.terminate(`ret ptr ${out}`);
} else {
this.declare(`declare ptr @scr_dyn_new_obj()`);
this.declare(`declare void @scr_dyn_obj_set(ptr, ptr, i64, ptr)`);
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_obj()`);
const byName = new Map(shape.fields.map((field) => [field.name, field]));
const order = shape.declaredOrder ?? shape.fields.map((field) => field.name);
const inOrder = new Set(order);
const fields = [
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
...shape.fields.filter((field) => !inOrder.has(field.name)),
];
for (const field of fields) {
const index = shape.fields.indexOf(field) + 1;
const fieldPtr = B.tmp();
const fieldValue = B.tmp();
B.line(`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`);
B.line(`${fieldValue} = load ${llFieldType(field.type)}, ptr ${fieldPtr}`);
const boxed = this.streamTypedRefBoxValue(
B,
field.type,
fieldValue,
ctx,
);
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${this.cstr(field.name)}, i64 ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
}
B.terminate(`ret ptr ${out}`);
}
} else if (t.kind === "array") {
const elem = t.elem;
this.declare(`declare ptr @scr_dyn_new_arr()`);
this.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
this.declare(`declare double @scr_arr_len(ptr)`);
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
const len = B.tmp();
B.line(`${len} = call double @scr_arr_len(ptr %p)`);
const iSlot = B.slot();
B.entryAllocas.push(`${iSlot} = alloca double`);
B.line(`store double ${f64Lit(0)}, ptr ${iSlot}`);
const condition = B.newLabel("strm.c");
const body = B.newLabel("strm.b");
const done = B.newLabel("strm.e");
B.br(condition);
B.startBlock(condition);
const index = B.tmp();
const more = B.tmp();
B.line(`${index} = load double, ptr ${iSlot}`);
B.line(`${more} = fcmp olt double ${index}, ${len}`);
B.condBr(more, body, done);
B.startBlock(body);
let value: string;
if (elem.kind === "f64" || elem.kind === "bool") {
const valueTy = elem.kind === "f64" ? "double" : "i1";
this.declare(
`declare ${elem.kind === "bool" ? "zeroext i1" : valueTy} @scr_arr_get_${elem.kind}(ptr, double)`,
);
value = B.tmp();
B.line(`${value} = call ${valueTy} @scr_arr_get_${elem.kind}(ptr %p, double ${index})`);
} else {
this.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
value = B.tmp();
B.line(`${value} = call ptr @scr_arr_get_ref(ptr %p, double ${index}) ; +1`);
}
const boxed = this.streamTypedRefBoxValue(B, elem, value, ctx);
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${boxed})`);
if (isRefCounted(elem)) {
B.line(`call void ${releaseSym(this, elem)}(ptr ${value})`);
}
const next = B.tmp();
B.line(`${next} = fadd double ${index}, ${f64Lit(1)}`);
B.line(`store double ${next}, ptr ${iSlot}`);
B.br(condition);
B.startBlock(done);
B.terminate(`ret ptr ${out}`);
} else {
const out = B.tmp();
B.line(`${out} = call ptr @${this.dyn.toDynHelper(t)}(ptr %p)`);
B.terminate(`ret ptr ${out}`);
}
this.resolveThunkDefs.push(
`define internal ptr @${snapshot}(ptr %p) ${FN_ATTRS} { ; materialize live stream value ${key}`,
B.render(),
`}`,
``,
);
return adapter;
}
private streamFromArrayAdapter(
t: IrType & { kind: "array" },
): string {
@@ -10151,21 +10381,30 @@ class LlEmitter {
boxed = B.tmp();
const rc = vAdapters(this, elem);
const keyPtr = this.cstr(key);
const commit = this.streamTypedRefCommitAdapter(elem, snapshot);
let commit: string;
if (this.streamTypedRefEligible(elem)) {
commit = this.streamTypedRefMaterializeAdapter(
elem,
{ prefix: snapshot, adapters: new Map() },
snapshot,
).commit;
} else {
commit = this.streamTypedRefCommitAdapter(elem, snapshot);
this.resolveThunkDefs.push(
`define internal ptr @${snapshot}(ptr %p) ${FN_ATTRS} { ; materialize stream element ${key}`,
`entry:`,
` %d = call ptr @${this.dyn.toDynHelper(elem)}(ptr %p)`,
` ret ptr %d`,
`}`,
``,
);
}
this.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, i64, ptr, ptr)`,
);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyPtr}, i64 ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr ${commit})`,
);
this.resolveThunkDefs.push(
`define internal ptr @${snapshot}(ptr %p) ${FN_ATTRS} { ; materialize stream element ${key}`,
`entry:`,
` %d = call ptr @${this.dyn.toDynHelper(elem)}(ptr %p)`,
` ret ptr %d`,
`}`,
``,
);
} else {
boxed = B.tmp();
B.line(
+22 -15
View File
@@ -999,10 +999,6 @@ static ScrDyn *sf_signal_invoke(void *ptr, ScrDyn *self, const char *method,
const char *what) {
SfSignal *s = ptr;
if (strcmp(method, "throwIfAborted") == 0) {
if (argc != 0) {
sf_type_error("AbortSignal.throwIfAborted takes no arguments");
return NULL;
}
if (s->aborted) {
sf_throw_dyn_reason(s->reason);
return NULL;
@@ -2402,10 +2398,6 @@ static ScrDyn *sf_reader_invoke(void *ptr, ScrDyn *self, const char *method,
const char *what) {
SfReader *r = ptr;
if (strcmp(method, "read") == 0) {
if (argc != 0) {
sf_type_error("ReadableStreamDefaultReader.read takes no arguments");
return NULL;
}
ScrPromise *p = sf_reader_read(r);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
@@ -4208,9 +4200,8 @@ static void sf_on_upgrade(
static bool sf_body_is_stream(const ScrDyn *body) {
return body &&
((body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM) ||
body->kind == SCR_DYN_ARR);
body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
}
static bool sf_redirect(SfTransfer *t, int status,
@@ -4501,9 +4492,8 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
bool body_stream =
body_present &&
((body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM) ||
body->kind == SCR_DYN_ARR);
body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
const ScrDyn *duplex =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "duplex", 6)
@@ -4521,7 +4511,8 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
promise, "RequestInit.duplex must be 'half'");
}
if (body_present && body->kind != SCR_DYN_STR &&
body->kind != SCR_DYN_BYTES && !body_stream) {
body->kind != SCR_DYN_BYTES && body->kind != SCR_DYN_ARR &&
!body_stream) {
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
@@ -4542,12 +4533,24 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
promise, "Request with GET/HEAD method cannot have body.");
}
/* BodyInit's USVString fallback applies to ordinary arrays too. An
* `any`-typed array is therefore "a,b", not a stream of "a" and "b";
* only an actual ReadableStream uses the streaming-body branch. */
ScrDyn *coerced_body = NULL;
if (body_present && body->kind == SCR_DYN_ARR) {
ScrStr *text = scr_dyn_string_coerce(body);
coerced_body = scr_dyn_new_str(text);
scr_str_release(text);
body = coerced_body;
}
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, 16);
const ScrDyn *init_headers =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "headers", 7)
: NULL;
if (!sf_add_headers(headers, init_headers)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
@@ -4564,6 +4567,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
bool has_content_length = false;
size_t content_length = 0;
if (!sf_content_length(headers, &has_content_length, &content_length)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
@@ -4577,6 +4581,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
? body->v.str->len
: body->v.bytes->len;
if (body_length != content_length) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
@@ -4589,6 +4594,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
* over starting I/O, but never masks a RequestInit validation error.
*/
if (signal && signal->aborted) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
@@ -4604,6 +4610,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
t->method = method;
t->headers = headers;
t->body = body_present ? scr_dyn_retain((ScrDyn *)body) : NULL;
scr_dyn_release(coerced_body);
t->redirect_mode = redirect_mode;
t->request_has_content_length = has_content_length;
t->request_content_length = content_length;
+42
View File
@@ -700,6 +700,46 @@ void *scr_dyn_typed_ref_unbox(const ScrDyn *d) {
return d->v.typed_ref.retain(d->v.typed_ref.ptr);
}
static void scr_dyn_typed_ref_preserve_child(
ScrDyn **fresh_slot, ScrDyn *cached_child) {
ScrDyn *fresh_child = *fresh_slot;
if (!cached_child || !fresh_child ||
cached_child->kind != SCR_DYN_TYPED_REF ||
fresh_child->kind != SCR_DYN_TYPED_REF ||
!scr_dyn_strict_eq(cached_child, fresh_child)) {
return;
}
*fresh_slot = scr_dyn_retain(cached_child);
scr_dyn_release(fresh_child);
}
/* A live record/array materializer creates typed capsules for mutable
* children. Preserve an existing child capsule when the fresh view points
* at the same static source, so retaining `value.child` across parent
* refreshes keeps JavaScript reference identity as well as liveness. */
static void scr_dyn_typed_ref_preserve_children(
ScrDyn *cached, ScrDyn *fresh) {
if (cached->kind != fresh->kind) return;
if (cached->kind == SCR_DYN_ARR) {
size_t n = cached->v.arr.len < fresh->v.arr.len
? cached->v.arr.len
: fresh->v.arr.len;
for (size_t i = 0; i < n; i++) {
scr_dyn_typed_ref_preserve_child(
&fresh->v.arr.items[i], cached->v.arr.items[i]);
}
return;
}
if (cached->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < fresh->v.obj.len; i++) {
ScrDynEntry *entry = &fresh->v.obj.entries[i];
ScrDyn *cached_child = scr_dyn_obj_get(
cached, entry->key, entry->key_len);
scr_dyn_typed_ref_preserve_child(&entry->value, cached_child);
}
}
}
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) {
ScrDyn *capsule = (ScrDyn *)d;
if (!capsule->v.typed_ref.materialized) {
@@ -712,6 +752,8 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) {
* detached contents without changing the cached node's address. */
ScrDyn *fresh =
capsule->v.typed_ref.materialize(capsule->v.typed_ref.ptr);
scr_dyn_typed_ref_preserve_children(
capsule->v.typed_ref.materialized, fresh);
size_t cached_rc = capsule->v.typed_ref.materialized->rc;
size_t fresh_rc = fresh->rc;
ScrDyn old = *capsule->v.typed_ref.materialized;
+60
View File
@@ -57,6 +57,22 @@ const posted = await fetch(`${process.argv[2]}/post-echo`, {
});
console.log(await posted.json());
console.log("consumed request locked:", requestBody.locked);
const arrayRequestBody: any = ["array", "body"];
const arrayPosted = await fetch(`${process.argv[2]}/post-echo`, {
method: "POST",
body: arrayRequestBody,
duplex: "half",
});
const arrayPostResult = (await arrayPosted.json()) as {
body: string;
contentType: string | null;
};
console.log(
"array request body:",
arrayPostResult.body,
arrayPostResult.contentType,
);
try {
requestBody.getReader();
console.log("consumed request reader unexpectedly acquired");
@@ -300,6 +316,17 @@ const bracketReader = ReadableStream.from(["bracket"]).getReader();
const bracketPart = await bracketReader["read"]();
console.log("bracket reader read:", bracketPart.done, bracketPart.value);
const surplusReader: any = ReadableStream.from([11]).getReader();
console.log(
"reader surplus argument:",
JSON.stringify(await surplusReader.read("ignored")),
);
const surplusSignal: any = AbortSignal.any([]);
console.log(
"throwIfAborted surplus argument:",
surplusSignal.throwIfAborted("ignored"),
);
interface StreamUnionBox {
value: number;
}
@@ -365,12 +392,14 @@ const repeatedStructuralReader =
repeatedStructuralValue,
]) as ReadableStream<StreamBaseValue>).getReader();
const repeatedStructuralFirst = await repeatedStructuralReader.read();
repeatedStructuralValue.value = 19;
const repeatedStructuralSecond = await repeatedStructuralReader.read();
console.log(
"stream widened repeated identity:",
repeatedStructuralFirst.done || repeatedStructuralSecond.done
? false
: repeatedStructuralFirst.value === repeatedStructuralSecond.value,
repeatedStructuralSecond.done ? -1 : repeatedStructuralSecond.value.value,
);
const repeatedDynamicValue = { value: 14 };
@@ -401,6 +430,37 @@ console.log("dynamic stream live refresh:", JSON.stringify(liveDynamicSecond.val
liveDynamicSecond.value.value = 18;
console.log("dynamic stream live commit:", liveDynamicValue.value);
const dynamicArrayValue = [21];
const dynamicArrayReader: any = ReadableStream.from([
dynamicArrayValue,
dynamicArrayValue,
]).getReader();
const dynamicArrayFirst: any = await dynamicArrayReader.read();
dynamicArrayFirst.value[0] = 22;
const dynamicArraySecond: any = await dynamicArrayReader.read();
console.log(
"dynamic stream array commit:",
dynamicArrayValue[0],
dynamicArraySecond.value[0],
dynamicArrayFirst.value === dynamicArraySecond.value,
);
const dynamicNestedValue = { nested: { value: 23 } };
const dynamicNestedReader: any = ReadableStream.from([
dynamicNestedValue,
dynamicNestedValue,
]).getReader();
const dynamicNestedFirst: any = await dynamicNestedReader.read();
const retainedDynamicNested: any = dynamicNestedFirst.value.nested;
retainedDynamicNested.value = 24;
const dynamicNestedSecond: any = await dynamicNestedReader.read();
console.log(
"dynamic stream nested commit:",
dynamicNestedValue.nested.value,
dynamicNestedSecond.value.nested.value,
retainedDynamicNested === dynamicNestedSecond.value.nested,
);
const widenedStringStream: ReadableStream<unknown> = ReadableStream.from([
"same",
["sa", "me"].join(""),