diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..01d871cb3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-id-instances-as-streams.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index b7295f59a..209c8c39a 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -140,9 +140,11 @@ static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); } /* Also the capsule's tag. */ static void bcs_capsule_release(void *p) { scr_stream_release(p); } -static bool bcs_stream_vt(const ScrVt *vt) { - return (vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post); +/* Whether `stream`, boxed as an "object:" typed ref, is a runtime stream. */ +static bool bcs_stream_vt(const ScrStream *stream) { + const ScrVt *vt = scr_stream_vt_of(stream); + return vt != NULL && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)); } static void bcs_rec_release(void *p); @@ -154,7 +156,7 @@ static ScrStream *bcs_of(const ScrDyn *value) { if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) { ScrStream *stream = value->v.typed_ref.ptr; - return bcs_stream_vt(stream->vt) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL; + return bcs_stream_vt(stream) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL; } if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL; ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN); @@ -184,7 +186,7 @@ static ScrStream *bcs_native_of(const ScrDyn *value) { memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) return NULL; ScrStream *stream = scr_dyn_typed_ref_unbox(value); - if (bcs_stream_vt(stream->vt)) return stream; + if (bcs_stream_vt(stream)) return stream; value->v.typed_ref.release(stream); return NULL; } diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index da28cec7a..7d7ec04ed 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -4740,12 +4740,14 @@ ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || - scr_vt_within(vt, &scr_duplex_vt); - bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) - : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); - if (ok) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt) { + bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || + scr_vt_within(vt, &scr_duplex_vt); + bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) + : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); + if (ok) return stream; + } value->v.typed_ref.release(stream); } static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering"; @@ -6411,9 +6413,7 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - /* A class instance with a class id in its rc word (scr_rc.h) has no - * vtable word: its class is no stream. */ - const ScrVt *vt = scr_rc_class_id(stream) == 0 ? stream->vt : NULL; + const ScrVt *vt = scr_stream_vt_of(stream); if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; value->v.typed_ref.release(stream); @@ -6718,9 +6718,9 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; value->v.typed_ref.release(stream); } scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 78290d8c4..99f58331c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -48,6 +48,13 @@ typedef struct ScrStream { ScrStreamState *st; } ScrStream; +/* The vtable of an object boxed as an "object:" typed ref, which may be any + * program class instance: NULL for one with a class id in its rc word + * (scr_rc.h), which has no vtable word and is no stream. */ +static inline const ScrVt *scr_stream_vt_of(const ScrStream *s) { + return scr_rc_class_id(s) == 0 ? s->vt : NULL; +} + extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt, scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */ diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c index 0859c4e07..11dcf6ee2 100644 --- a/packages/runtime/src/scr_stream_dyn.c +++ b/packages/runtime/src/scr_stream_dyn.c @@ -23,7 +23,11 @@ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) { memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) return NULL; ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt == NULL) { + value->v.typed_ref.release(stream); + return NULL; + } bool duplex = scr_stream_dyn_within(vt, &scr_duplex_vt) || scr_stream_dyn_within(vt, &scr_transform_vt) || scr_stream_dyn_within(vt, &scr_passthrough_vt); diff --git a/tests/corpus/class-id-instances-as-streams.ts b/tests/corpus/class-id-instances-as-streams.ts new file mode 100644 index 000000000..813e54ed2 --- /dev/null +++ b/tests/corpus/class-id-instances-as-streams.ts @@ -0,0 +1,39 @@ +// Instances of a class hierarchy, held as untyped values, where the stream +// adapters probe for a native stream: pipe, pipeline, readline, and +// instanceof checks against the stream classes. Their first field is a +// number, which sits where a stream's vtable word would be. +import { Duplex, PassThrough, Readable, Transform, Writable, pipeline } from "node:stream"; +import { createInterface } from "node:readline"; + +class Shape { + size = 3.5; + area(): number { + return 0; + } +} +class Square extends Shape { + override area(): number { + return this.size * this.size; + } +} + +function attempt(label: string, run: () => unknown): void { + try { + run(); + console.log(label, "ok"); + } catch (error) { + console.log(label, "throws", error instanceof Error); + } +} + +const shape: any = new Square(); +try { + Readable.from(["a"]).pipe(shape); +} catch (error) { + console.log("pipe", (error as Error).name); +} +attempt("pipeline", () => pipeline(shape, new PassThrough(), () => {})); +attempt("readline-input", () => createInterface({ input: shape })); +console.log("instanceof", shape instanceof Readable, shape instanceof Writable, shape instanceof Duplex); +console.log("instanceof-transform", shape instanceof Transform, shape instanceof PassThrough); +console.log("area", (shape as Square).area()); diff --git a/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..07189e390 --- /dev/null +++ b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4500 }