mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Keep island arrays of intersection types as handles
`Choice[] & GenChoice[]`, an island array extended by the caller's own choice type (pi's openrouter images: `imageResponse.choices`), was refused with SC8010. The intersection has no native IR type of its own — mapTypeOf returns null, or its arms map to different array types — so the crossing failed before the container rules that keep an array the island does not declare a handle. An intersection of containers is a container: it is a value of each arm, and where the program only uses it through island operations (member reads, an island call, an island identity test, a handle binding) it now stays a handle, exactly as an undeclared array does. Elements and fields the declaration does name cross back out on their own terms, so an element's `images` array copies, and any other use still refuses rather than inventing a native layout. Test: tests/corpus/6901-node-island-array-intersection (both shapes, the island-declared arm and the caller's own arm, differential to Node, plain and sanitized). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ba4e6e52a
commit
d2e3690bc3
@@ -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;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6901-node-island-array-intersection/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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<Choice[] & NativeChoice[]>(imageResponse().choices)[1];
|
||||
console.log(third.text, third.images === undefined ? "no-images" : third.images.join("+"));
|
||||
Generated
Vendored
+18
@@ -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<T>`. */
|
||||
export declare function unsafe<T>(value: T): T;
|
||||
+10
@@ -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;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "choicekit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-array-intersection", "private": true, "type": "module", "scriptc": { "packages": { "choicekit": "node" } } }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 42000 }
|
||||
Reference in New Issue
Block a user