mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-19 05:29:46 +08:00
* fix(compiler): recognize tuples in Array.isArray - Treat fixed tuple record shapes as JavaScript arrays. - Keep union tag tests and narrowing aligned. - Add Native SDK facade regression coverage. Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com> * fix(compiler): narrow readonly tuple arrays * fix(compiler): preserve narrowed readonly tuples --------- Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com>
110 lines
4.3 KiB
TypeScript
110 lines
4.3 KiB
TypeScript
// Array.isArray on UNION-typed values answers by the runtime tag: true iff
|
|
// the active arm is an array kind — the narrowing test tsc's control flow
|
|
// then builds on (the certs `string | readonly string[]` and tailscale
|
|
// `string[] | undefined` idioms).
|
|
function names(tlds: string | string[]): string {
|
|
const configured: string[] = Array.isArray(tlds) ? tlds : [tlds];
|
|
return configured.join(",");
|
|
}
|
|
console.log(names("localhost"));
|
|
console.log(names(["a.test", "b.test"]));
|
|
console.log(names([]));
|
|
|
|
// readonly arrays ride the same tag test (tsc's own narrowing answers
|
|
// `any[]` around an `arg is any[]` guard on readonly unions, so the
|
|
// branches read nothing here — the certs SNICallback idiom's shape).
|
|
function isList(tlds: string | readonly string[]): boolean {
|
|
return Array.isArray(tlds);
|
|
}
|
|
console.log(isList(["x.test", "y.test"]));
|
|
console.log(isList("bare"));
|
|
|
|
function hasHttps(capabilities: string[] | undefined): boolean {
|
|
if (Array.isArray(capabilities) && capabilities.some((c) => c === "https")) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
console.log(hasHttps(undefined));
|
|
console.log(hasHttps(["ssh", "https"]));
|
|
console.log(hasHttps(["ssh"]));
|
|
|
|
// The negated spelling narrows the else way around.
|
|
function total(value: number | number[]): number {
|
|
if (!Array.isArray(value)) return value;
|
|
let sum = 0;
|
|
for (const n of value) sum += n;
|
|
return sum;
|
|
}
|
|
console.log(total(7));
|
|
console.log(total([1, 2, 3]));
|
|
|
|
// Zero array arms fold to constant false; a union of two array kinds
|
|
// answers true for both.
|
|
const scalar: string | number = "text" as string | number;
|
|
console.log(Array.isArray(scalar));
|
|
const mixed: string[] | number[] | undefined = [3, 4] as string[] | number[] | undefined;
|
|
console.log(Array.isArray(mixed));
|
|
const missing: string[] | number[] | undefined = undefined;
|
|
console.log(Array.isArray(missing));
|
|
|
|
// Fixed tuples use a positional record-shaped IR representation so each
|
|
// slot can keep its own type, but they are still JavaScript arrays. This is
|
|
// the Native SDK facade shape: an update returns either its model record or
|
|
// [model, command], then Array.isArray selects the tuple before indexing it.
|
|
interface TupleModel {
|
|
n: number;
|
|
}
|
|
|
|
interface TupleCommand {
|
|
op: string;
|
|
}
|
|
|
|
function tupleUpdate(model: TupleModel, effect: boolean): TupleModel | [TupleModel, TupleCommand] {
|
|
if (!effect) return model;
|
|
return [model, { op: "spawn" }];
|
|
}
|
|
|
|
function normalizeTuple(model: TupleModel, effect: boolean): [TupleModel, string] {
|
|
const out = tupleUpdate(model, effect);
|
|
if (Array.isArray(out)) return [out[0], out[1].op];
|
|
return [out, "none"];
|
|
}
|
|
|
|
function isFixedTuple(value: [TupleModel, TupleCommand]): boolean {
|
|
return Array.isArray(value);
|
|
}
|
|
|
|
// For a readonly tuple union, tsc narrows the guarded value to an
|
|
// intersection with any[] rather than directly to an array type. The
|
|
// runtime tag still proves the tuple arm, so positional reads must bridge
|
|
// back to its fixed shape.
|
|
type ReadonlyTupleResult = TupleModel | readonly [TupleModel, TupleCommand];
|
|
function describeReadonlyTuple(value: ReadonlyTupleResult): string {
|
|
if (Array.isArray(value)) return `${value[0].n}:${value[1].op}`;
|
|
return "plain";
|
|
}
|
|
|
|
// The checker keeps the same synthetic intersection for neighboring tuple
|
|
// operations. A const alias must retain the lowered tuple representation;
|
|
// then length, the supported read-only methods, and iteration all dispatch
|
|
// from that representation too.
|
|
function summarizeReadonlyTuple(value: ReadonlyTupleResult): string {
|
|
if (!Array.isArray(value)) return "plain";
|
|
const tuple = value;
|
|
let visits = 0;
|
|
for (const _part of tuple) visits++;
|
|
return `${value.length}:${value.slice(0).length}:${value.map(() => "v").join("")}:${tuple.length}:${tuple.slice(0).length}:${tuple.map(() => "t").join("")}:${visits}`;
|
|
}
|
|
|
|
const plainResult = normalizeTuple({ n: 7 }, false);
|
|
console.log(plainResult[0].n, plainResult[1]);
|
|
const tupleResult = normalizeTuple({ n: 9 }, true);
|
|
console.log(tupleResult[0].n, tupleResult[1]);
|
|
console.log(isFixedTuple([{ n: 11 }, { op: "direct" }]));
|
|
const readonlyTuple: readonly [TupleModel, TupleCommand] = [{ n: 12 }, { op: "readonly" }];
|
|
console.log(describeReadonlyTuple({ n: 13 }));
|
|
console.log(describeReadonlyTuple(readonlyTuple));
|
|
console.log(summarizeReadonlyTuple({ n: 14 }));
|
|
console.log(summarizeReadonlyTuple(readonlyTuple));
|