diff --git a/packages/compiler/src/frontend/lowering/lower-node-island.ts b/packages/compiler/src/frontend/lowering/lower-node-island.ts index 76456833e..c36dc345f 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-island.ts @@ -681,12 +681,19 @@ function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr if ((tsType.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) !== 0 && isDiscarded(node)) return raw; const mapped = lowerer.mapTypeOf(tsType); - if (mapped === null) + if (mapped === null) { + // An intersection of containers (`Choice[] & GenChoice[]`: the + // island's array extended by the caller's own type) has no native + // representation of its own. Where the program only uses it through + // island operations it stays a handle, like an array the island does + // not declare. + if (containerType(lowerer, tsType) && handleUseOk(lowerer, node)) return raw; boundaryFail( lowerer, `an island value of type '${lowerer.checker.typeToString(tsType)}'`, node, ); + } if (mapped.kind === "dyn") return raw; if (mapped.kind === "void") return raw; // An island promise is the same promise on both sides (the runtime keeps @@ -887,7 +894,8 @@ function builtinHandleClass(type: ts.Type): boolean { /** Arrays, tuples, Maps and Sets (every arm of a union, nullish arms * aside), and the builtin classes of builtinHandleClass: the object types - * an island hands back without declaring them. */ + * an island hands back without declaring them. An intersection of those + * (`Choice[] & GenChoice[]`) is a container too: it is a value of each. */ function containerType(lowerer: Lowerer, type: ts.Type): boolean { if (type.isUnionType()) return ts @@ -897,6 +905,14 @@ function containerType(lowerer: Lowerer, type: ts.Type): boolean { (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0 || containerType(lowerer, t), ); + if ((type.flags & ts.TypeFlags.Intersection) !== 0) + return ts + .constituentTypes(type) + .every( + (t) => + (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0 || + containerType(lowerer, t), + ); if ((type.flags & ts.TypeFlags.Object) === 0) return false; if (lowerer.checker.isArrayType(type) || lowerer.checker.isTupleType(type)) return true; const name = type.getSymbol()?.escapedName; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6901-node-island-array-intersection/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6901-node-island-array-intersection/main.ts.json new file mode 100644 index 000000000..3ef274510 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6901-node-island-array-intersection/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6901-node-island-array-intersection/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6901-node-island-array-intersection/main.ts b/tests/corpus/6901-node-island-array-intersection/main.ts new file mode 100644 index 000000000..55539393e --- /dev/null +++ b/tests/corpus/6901-node-island-array-intersection/main.ts @@ -0,0 +1,24 @@ +// @requires-libnode +// Reading an element of an island array whose declared type is an +// INTERSECTION of two array types — `Choice[] & GenChoice[]`, the island's +// choices extended by the caller's own choice type — has no native +// representation of its own, so the array stays a handle and the read runs +// in the island; the fields the declaration names cross back out. +import { extendedImageResponse, imageResponse, unsafe } from "choicekit"; +import type { Choice } from "choicekit"; + +const extended = extendedImageResponse(); +const first = extended.choices[0]; +console.log(first.index, first.text); +console.log(first.images === undefined ? "no-images" : first.images.join("+")); + +const second = extended.choices[1]; +console.log(second.index, second.text, second.images === undefined ? "no-images" : second.images.join("+")); + +/** The caller's own choice type over the island's array. */ +interface NativeChoice extends Choice { + readonly images?: string[]; +} + +const third = unsafe(imageResponse().choices)[1]; +console.log(third.text, third.images === undefined ? "no-images" : third.images.join("+")); diff --git a/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/index.d.ts b/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/index.d.ts new file mode 100644 index 000000000..d530f50cf --- /dev/null +++ b/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/index.d.ts @@ -0,0 +1,18 @@ +export interface Choice { + readonly index: number; + readonly text: string; +} +export interface GenChoice extends Choice { + readonly images?: string[]; +} +export interface ImageResponse { + readonly choices: Choice[]; +} +export interface ExtendedImageResponse { + /** The island's choices, extended by the island's own choice type. */ + readonly choices: Choice[] & GenChoice[]; +} +export declare function imageResponse(): ImageResponse; +export declare function extendedImageResponse(): ExtendedImageResponse; +/** Declares the type of the value it is given, like `Type.Unsafe`. */ +export declare function unsafe(value: T): T; diff --git a/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/index.js b/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/index.js new file mode 100644 index 000000000..f4168c42c --- /dev/null +++ b/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/index.js @@ -0,0 +1,10 @@ +"use strict"; + +const choices = () => [ + { index: 0, text: "first", images: ["a.png", "b.png"] }, + { index: 1, text: "second" }, +]; + +exports.imageResponse = () => ({ choices: choices() }); +exports.extendedImageResponse = () => ({ choices: choices() }); +exports.unsafe = (value) => value; diff --git a/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/package.json b/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/package.json new file mode 100644 index 000000000..87cb5448c --- /dev/null +++ b/tests/corpus/6901-node-island-array-intersection/node_modules/choicekit/package.json @@ -0,0 +1 @@ +{ "name": "choicekit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6901-node-island-array-intersection/package.json b/tests/corpus/6901-node-island-array-intersection/package.json new file mode 100644 index 000000000..4468bc168 --- /dev/null +++ b/tests/corpus/6901-node-island-array-intersection/package.json @@ -0,0 +1 @@ +{ "name": "island-array-intersection", "private": true, "type": "module", "scriptc": { "packages": { "choicekit": "node" } } } diff --git a/tests/harness/corpus-costs/6901-node-island-array-intersection/main.ts.json b/tests/harness/corpus-costs/6901-node-island-array-intersection/main.ts.json new file mode 100644 index 000000000..f2a54cf68 --- /dev/null +++ b/tests/harness/corpus-costs/6901-node-island-array-intersection/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 42000 }