diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index 2cd029bf2..b6dbe16d3 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -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; +} + +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(); for (const { arm, tag } of unionRefArms) { diff --git a/packages/compiler/src/backend/emission/emit-walkers.ts b/packages/compiler/src/backend/emission/emit-walkers.ts index 36620c28a..4668b87b8 100644 --- a/packages/compiler/src/backend/emission/emit-walkers.ts +++ b/packages/compiler/src/backend/emission/emit-walkers.ts @@ -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(` }`); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 36e786810..47f7d2034 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -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)`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2012e17fc..210ac318f 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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; +} 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( diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 4d447f8e8..c93b5e18c 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -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; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3851f385b..2115ae688 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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; diff --git a/tests/fixtures/fetch/static-stream/main.mts b/tests/fixtures/fetch/static-stream/main.mts index f7f9419a7..c418f4ba7 100644 --- a/tests/fixtures/fetch/static-stream/main.mts +++ b/tests/fixtures/fetch/static-stream/main.mts @@ -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).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 = ReadableStream.from([ "same", ["sa", "me"].join(""),