mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Keep projected files valid for bare async arrows and unbraced bodies, and adopt tuple elements
- A bare arrow parameter after `async` (`async x => ...`) gets parentheses with its projected type, as `x => ...` does. Without them the rewritten file failed its check and lost its projection (SC6102). - A cast that starts an unbraced loop or `if` body gets no `;` guard. Only a statement in a statement list can continue the previous line, and in a body the `;` became the body, which moved the statement after the loop. - Adoption walks a tuple's elements through the live element capsules of the tuple's view, so references package code holds to them see the native values. - scr_runtime.h declares scr_dyn_decl_adopt (in scr_dyn.h), which the backend emits, so llvm-runtime-abi.test.ts finds its prototype. - npm.test.ts gives zod3-adoption and zod3-boundary the long per-case timeout of the other zod 3 cases; their sanitized builds compile zod's types module too.
This commit is contained in:
@@ -605,11 +605,15 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
const name = jsNode.name;
|
||||
const nameStart = ts.tokenStartOfNode(name);
|
||||
const text = jsSf.text;
|
||||
// `x => ...` or `async x => ...`: nothing but the modifiers comes
|
||||
// before the parameter, which then needs parentheses for its type.
|
||||
const head = ts.isArrowFunction(fn)
|
||||
? ((ts.getModifiers(fn) ?? []).at(-1)?.end ?? ts.tokenStartOfNode(fn))
|
||||
: nameStart;
|
||||
const bareArrow =
|
||||
ts.isArrowFunction(fn) &&
|
||||
fn.parameters.nodes.length === 1 &&
|
||||
text[ts.tokenStartOfNode(fn)] !== "(" &&
|
||||
text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === "";
|
||||
text.slice(head, nameStart).trim() === "";
|
||||
const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end;
|
||||
if (bareArrow) {
|
||||
fileEdits.push({ start: nameStart, end: nameStart, text: "(" });
|
||||
@@ -727,10 +731,20 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
// semicolon continues the previous expression: guard it.
|
||||
let up: ts.Node | undefined = e;
|
||||
while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent;
|
||||
// Only a statement in a statement list can follow such a line; an
|
||||
// unbraced body (`for (...) S.parse(x).run()`) follows its header,
|
||||
// and a `;` there would become the body.
|
||||
const list = up?.parent?.parent;
|
||||
if (
|
||||
up?.parent !== undefined &&
|
||||
ts.isExpressionStatement(up.parent) &&
|
||||
ts.tokenStartOfNode(up.parent) === start
|
||||
ts.tokenStartOfNode(up.parent) === start &&
|
||||
list !== undefined &&
|
||||
(ts.isBlock(list) ||
|
||||
ts.isSourceFile(list) ||
|
||||
ts.isModuleBlock(list) ||
|
||||
ts.isCaseClause(list) ||
|
||||
ts.isDefaultClause(list))
|
||||
)
|
||||
prefix = ";(";
|
||||
void stmt;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <string.h>
|
||||
|
||||
void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule);
|
||||
void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule);
|
||||
|
||||
typedef struct {
|
||||
ScrDyn *node;
|
||||
@@ -120,6 +119,32 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && !capsule->v.typed_ref.array &&
|
||||
n->kind == SCR_DYN_ARR && !n->v.arr.presence) {
|
||||
/* A capsule without element hooks over an array: a tuple, whose index
|
||||
* field names get no record hooks. Its view holds a live capsule of
|
||||
* each record or array element, which the source element adopts (the
|
||||
* walk skips the other elements). */
|
||||
ScrDyn *view = capsule->v.typed_ref.materialize(ptr);
|
||||
if (scr_exc_pending()) {
|
||||
scr_dyn_release(view);
|
||||
if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule);
|
||||
return false;
|
||||
}
|
||||
if (view && view->kind == SCR_DYN_ARR) {
|
||||
size_t len = n->v.arr.len < view->v.arr.len ? n->v.arr.len : view->v.arr.len;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
ScrDyn *child = n->v.arr.items[i], *element = view->v.arr.items[i];
|
||||
if (!child || !element || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue;
|
||||
ScrDynPath p = { path, NULL, i };
|
||||
if (!scr_decl_adopt_walk(list, child, scr_dyn_retain(element), &p, depth + 1)) {
|
||||
scr_dyn_release(view);
|
||||
if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
scr_dyn_release(view);
|
||||
} else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass &&
|
||||
capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) {
|
||||
const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array;
|
||||
|
||||
@@ -368,6 +368,12 @@ typedef struct ScrDynTypedRecordOps {
|
||||
/* A record capsule's ops while it answers from the native record (no view
|
||||
* created yet), or NULL. */
|
||||
const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d);
|
||||
/* The declaration-typed boundary's adoption (scr_decl_adopt.c, linked by
|
||||
* programs with boundary casts): the nodes of `source` that something else
|
||||
* still references become live capsules of the native value `capsule`
|
||||
* holds. Both borrowed; a node that is not plain data throws the path
|
||||
* TypeError before anything changes. */
|
||||
void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule);
|
||||
|
||||
/* Prototype members of a compiled class seen as `unknown`, emitted per
|
||||
* class by the compiler: the string names of methods, getters and setters
|
||||
|
||||
@@ -37,3 +37,12 @@ cyc.meta = cyc;
|
||||
const back = AsRow.parse(cyc);
|
||||
back.id = 10;
|
||||
console.log("cycle", cyc.id, back === cyc, back.meta === cyc);
|
||||
// A tuple's elements keep their identity too: the package-side array
|
||||
// still holds the element that typed code mutates.
|
||||
const Pair = z.custom<[{ id: number; tags: string[] }, string]>();
|
||||
const pairSource = JSON.parse('[{"id":1,"tags":["a"]},"s"]') as unknown[];
|
||||
const pairHead = pairSource[0];
|
||||
const pair = Pair.parse(pairSource);
|
||||
pair[0].tags.push("b");
|
||||
pair[0].id = 2;
|
||||
console.log("tuple", JSON.stringify(pairHead), JSON.stringify(pairSource), pair[1]);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Rewrites the declaration-typed boundary makes in user code: a typed
|
||||
// parameter of a bare `async` arrow zod calls, and a cast at the start of
|
||||
// an unbraced loop body.
|
||||
import { z } from "zod";
|
||||
|
||||
const Box = z.object({ n: z.number() });
|
||||
const raw: unknown = { n: 2 };
|
||||
|
||||
// The cast opens the loop body's statement; the loop runs it each time.
|
||||
let seen = 0;
|
||||
for (let i = 0; i < 3; i++) Box.parse(raw).n > 0 && seen++;
|
||||
let left = 2;
|
||||
while (left-- > 0) Box.parse(raw).n > 1 && seen++;
|
||||
console.log("loops", seen);
|
||||
|
||||
// zod calls the callback with the string it checked.
|
||||
const Length = z.string().transform(async s => s.length + 1);
|
||||
async function main(): Promise<void> {
|
||||
console.log("async arrow", await Length.parseAsync("abc"));
|
||||
}
|
||||
void main();
|
||||
@@ -3,7 +3,10 @@
|
||||
* naming the path, and a value the native record cannot represent (a
|
||||
* frozen object) fails with the reason. Node does not check, so this is a
|
||||
* native-only test; npm.test.ts runs the differential cases
|
||||
* (zod3-boundary, zod3-adoption, zod3-infer). */
|
||||
* (zod3-boundary, zod3-adoption, zod3-infer). The rewrite test checks that
|
||||
* a file keeps its projection (no SC6102 note) where the rewrite has to
|
||||
* parenthesize a bare async arrow's parameter and cast the start of an
|
||||
* unbraced loop body. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
@@ -35,3 +38,31 @@ test(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"boundary rewrites keep bare async arrows and unbraced loop bodies valid",
|
||||
{ timeout: 600_000 },
|
||||
async () => {
|
||||
const rewrites = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/rewrites.ts");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(rewrites, {
|
||||
outPath: join(outDir, "rewrites"),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
}
|
||||
// The file keeps its projection: no SC6102 note drops it (a build
|
||||
// that lowered nothing carries no warnings at all).
|
||||
expect((result.warnings ?? []).map((w) => w.code)).not.toContain("SC6102");
|
||||
const pending = execFileAsync(result.binaryPath, [], {
|
||||
cwd: dirname(rewrites),
|
||||
encoding: "utf8",
|
||||
});
|
||||
pending.child.stdin?.end();
|
||||
const { stdout } = await pending;
|
||||
// Node's output: every loop iteration runs the cast statement.
|
||||
expect(stdout).toBe("loops 5\nasync arrow 4\n");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -112,13 +112,16 @@ const KNOWN: Record<string, Known> = {
|
||||
const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name);
|
||||
|
||||
/** Per-case timeouts past the default 120 s: the undici cases compile the
|
||||
* built-in undici implementation's module graph, and zod3 and zod3-infer
|
||||
* compile zod 3's 3,700-line types module. A sanitized build of either
|
||||
* takes minutes on a CI runner. */
|
||||
* built-in undici implementation's module graph, and the zod 3 cases
|
||||
* (zod3, zod3-adoption, zod3-boundary, zod3-infer) compile zod 3's
|
||||
* 3,700-line types module. A sanitized build of either takes minutes on a
|
||||
* CI runner. */
|
||||
const TIMEOUTS: Record<string, number> = {
|
||||
undici5: 600_000,
|
||||
undici7: 600_000,
|
||||
zod3: 600_000,
|
||||
"zod3-adoption": 600_000,
|
||||
"zod3-boundary": 600_000,
|
||||
"zod3-infer": 600_000,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user