mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Support three.js meshes and materials in static builds
- Compile mesh construction, transforms, cloning, and material updates for native and WASI targets. - Preserve subclass dispatch, fluent returns, and shared array identity in JavaScript packages. - Support native class arrays and recursive conversions without materializing classes for type checks.
This commit is contained in:
@@ -4679,14 +4679,12 @@ export class LlEmitter {
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of ${key}`);
|
||||
}
|
||||
const prefix = `sc_ldr_${this.liveDynRefAdapters.size}`;
|
||||
const adapter = streamTypedRefMaterializeAdapter(
|
||||
return streamTypedRefMaterializeAdapter(
|
||||
this,
|
||||
t,
|
||||
{ prefix, adapters: new Map() },
|
||||
{ prefix, adapters: this.liveDynRefAdapters },
|
||||
`${prefix}_materialize`,
|
||||
);
|
||||
this.liveDynRefAdapters.set(key, adapter);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
liveDynUnionRefAdapter(
|
||||
|
||||
@@ -48,16 +48,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of ${typeKey(v.type)}`);
|
||||
}
|
||||
const key = typeKey(v.type);
|
||||
let adapter = host.liveDynRefAdapters.get(key);
|
||||
if (!adapter) {
|
||||
const prefix = `sc_ldr_${host.liveDynRefAdapters.size}`;
|
||||
adapter = host.streamTypedRefMaterializeAdapter(
|
||||
v.type,
|
||||
{ prefix, adapters: new Map() },
|
||||
`${prefix}_materialize`,
|
||||
);
|
||||
host.liveDynRefAdapters.set(key, adapter);
|
||||
}
|
||||
const adapter = host.liveDynRefAdapter(v.type);
|
||||
const rc = vAdapters(host.shapeHost, v.type);
|
||||
|
||||
const boxed = B.tmp();
|
||||
|
||||
@@ -559,6 +559,9 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
// Publish both symbols before their bodies: class-array and callback
|
||||
// converters can request this same adapter while emitting the commit.
|
||||
adapter.commit = `@${snapshot}_commit`;
|
||||
adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot);
|
||||
const B = new BlockBuilder();
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ interface Dispatch {
|
||||
classes: Set<string>;
|
||||
}
|
||||
|
||||
interface PropertyReceiver {
|
||||
info: ClassInfo;
|
||||
capsule: ClassInfo;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface PropertyDispatch {
|
||||
name: string;
|
||||
write: boolean;
|
||||
@@ -316,16 +322,18 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
}
|
||||
for (const dispatch of this.properties.values()) {
|
||||
for (const info of this.propertyCandidates(lowerer, dispatch.name)) {
|
||||
if (dispatch.classes.has(info.def.name)) continue;
|
||||
dispatch.classes.add(info.def.name);
|
||||
for (const plan of this.propertyReceivers(lowerer)) {
|
||||
const { info } = plan;
|
||||
if (!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
!info.fields.has(dispatch.name) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`)) continue;
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const receiver = varRef("p.0", DYN, loc);
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
const before = lowerer.diags.length;
|
||||
let branch: IrStmt[];
|
||||
try {
|
||||
branch = this.propertyBody(lowerer, dispatch, info, { kind: "dynCheck", value: receiver, type, loc });
|
||||
branch = this.propertyBody(lowerer, dispatch, info, receiver);
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError) || !info.decl) throw error;
|
||||
const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
|
||||
@@ -333,23 +341,18 @@ export class ClassDynamicDispatch {
|
||||
branch = [fence];
|
||||
}
|
||||
dispatch.fn.body.unshift({
|
||||
kind: "if", cond: {
|
||||
kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
|
||||
}, then: branch, else_: null, loc,
|
||||
kind: "if", cond: condition, then: branch, else_: null, loc,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (const dispatch of this.computed.values()) {
|
||||
for (const className of this.boxed) {
|
||||
if (dispatch.classes.has(className)) continue;
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
dispatch.classes.add(className);
|
||||
for (const plan of this.propertyReceivers(lowerer)) {
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
const { info } = plan;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const type: IrType = { kind: "object", className };
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
const names = new Set([...info.fields.keys()].filter(isClassOwnEnumerableFieldName));
|
||||
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
|
||||
for (const method of owner.methods.keys()) {
|
||||
@@ -372,9 +375,7 @@ export class ClassDynamicDispatch {
|
||||
kind: "strEq", left: varRef(dispatch.keyLocal, STRING, loc), right: { kind: "strLit", value: name, type: STRING, loc }, negated: false, type: BOOL, loc,
|
||||
}, then: body, else_: null, loc });
|
||||
}
|
||||
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: {
|
||||
kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
|
||||
}, then: branch, else_: null, loc });
|
||||
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: condition, then: branch, else_: null, loc });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -400,13 +401,34 @@ export class ClassDynamicDispatch {
|
||||
add(root);
|
||||
}
|
||||
|
||||
private propertyCandidates(lowerer: Lowerer, name: string): ClassInfo[] {
|
||||
if (!isClassOwnEnumerableFieldName(name)) return [];
|
||||
return [...this.boxed].flatMap((className) => {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) return [];
|
||||
return info.fields.has(name) || findMethodOn(lowerer, info, `get:${name}`) || findMethodOn(lowerer, info, `set:${name}`) ? [info] : [];
|
||||
});
|
||||
/** A capsule retains its static type, while its object may be a subclass.
|
||||
* Preorder insertion lets later branches test the most derived layout first. */
|
||||
private propertyReceivers(lowerer: Lowerer): PropertyReceiver[] {
|
||||
const plans: PropertyReceiver[] = [];
|
||||
for (const name of this.boxed) {
|
||||
const capsule = lowerer.classes.get(name);
|
||||
if (!capsule || capsule.builtinEmitter || capsule.builtinStream || capsule.builtinError) continue;
|
||||
const visit = (info: ClassInfo): void => {
|
||||
plans.push({ info, capsule, key: JSON.stringify([name, info.def.name]) });
|
||||
for (const child of info.subclasses) visit(child);
|
||||
};
|
||||
visit(capsule);
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
private propertyReceiver(plan: PropertyReceiver, loc: SrcLoc): { receiver: IrExpr; condition: IrExpr } {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const type: IrType = { kind: "object", className: plan.capsule.def.name };
|
||||
const checked: IrExpr = { kind: "dynCheck", value, type, loc };
|
||||
const matches: IrExpr = { kind: "libCall", fn: "dyn.typedRefIs", args: [value,
|
||||
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc };
|
||||
if (plan.info === plan.capsule) return { receiver: checked, condition: matches };
|
||||
return {
|
||||
receiver: { kind: "downcast", value: checked, type: { kind: "object", className: plan.info.def.name }, loc },
|
||||
condition: { kind: "ternary", cond: matches, then: { kind: "instanceOf", value: checked,
|
||||
className: plan.info.def.name, type: BOOL, loc }, else_: { kind: "boolLit", value: false, type: BOOL, loc }, type: BOOL, loc },
|
||||
};
|
||||
}
|
||||
|
||||
private propertyBody(lowerer: Lowerer, dispatch: PropertyDispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] {
|
||||
|
||||
@@ -325,6 +325,11 @@ export function lowerDynObjectLiteral(
|
||||
let v = boxValue
|
||||
? boxValue(valueExpr as ts.Expression, raw)
|
||||
: lowerer.coerceToExpected(raw, DYN);
|
||||
// A JS object's array-valued properties retain the same source array.
|
||||
// Repeated references must compare equal and mutations remain shared.
|
||||
if (!boxValue && isJsSourceFile(prop.getSourceFile()) && v.kind === "dynFrom" && v.value.type.kind === "array") {
|
||||
v = { ...v, liveRef: true };
|
||||
}
|
||||
if (v.type.kind !== "dyn") {
|
||||
const convDiagsBefore = lowerer.diags.length;
|
||||
try {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, st
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
|
||||
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, type ClassInfo } from "./lower-classes.js";
|
||||
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, returnsOnlyThis, type ClassInfo } from "./lower-classes.js";
|
||||
import { classCallbackCall, isClassCallback } from "./class-callbacks.js";
|
||||
import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js";
|
||||
import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js";
|
||||
@@ -6768,6 +6768,12 @@ const inliningPredicates = new Set<ts.Symbol>();
|
||||
function reconcileOverloadReturn(lowerer: Lowerer, expr: ts.CallExpression | ts.TaggedTemplateExpression, call: IrExpr): IrExpr {
|
||||
const rsig = lowerer.checker.getResolvedSignature(expr);
|
||||
const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined;
|
||||
// Fluent JS overrides share the base's return ABI, but their result
|
||||
// still has the receiver's subclass layout at the call site.
|
||||
if (call.type.kind === "object" && rdecl && ts.isMethodDeclaration(rdecl) &&
|
||||
isJsSourceFile(rdecl.getSourceFile()) && returnsOnlyThis(rdecl)) {
|
||||
call = lowerer.maybeNarrow(call, expr);
|
||||
}
|
||||
const resolvedOverload =
|
||||
rsig && rdecl &&
|
||||
(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) &&
|
||||
|
||||
@@ -30,6 +30,24 @@ import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
|
||||
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
|
||||
const body = member.body;
|
||||
const last = body?.statements.at(-1);
|
||||
if (!body || !last || !ts.isReturnStatement(last)) return false;
|
||||
let valid = true;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node)) return;
|
||||
if (ts.isReturnStatement(node)) {
|
||||
let value = node.expression;
|
||||
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
body.forEachChild(visit);
|
||||
return valid;
|
||||
}
|
||||
|
||||
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
|
||||
if (!ts.isClassExpression(decl)) return false;
|
||||
for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
|
||||
@@ -545,13 +563,16 @@ export interface GenericClassInfo {
|
||||
/** The undefined-armed union of a JS class property's inferred type — the
|
||||
* honest slot for a field first assigned outside the constructor's top
|
||||
* level (undefined until the write runs, Node-exact). Null when the
|
||||
* inference is unmappable, checked-dynamic (dyn stays out of class
|
||||
* fields — KEEP NARROW), or an arm-less kind that cannot join a union
|
||||
* inference is unmappable or an arm-less kind that cannot join a union
|
||||
* (genResultRecord's list, including scalar-backed Date values). */
|
||||
function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
|
||||
const t = lowerer.checker.getTypeOfSymbol(p);
|
||||
const mapped = lowerer.mapTypeOf(t);
|
||||
if (!mapped || mapped.kind === "void" || mapped.kind === "dyn") return null;
|
||||
const site = lowerer.checker.declarationsOf(p)[0];
|
||||
let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null);
|
||||
if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions);
|
||||
if (!mapped || mapped.kind === "void") return null;
|
||||
// An implicit JS value already carries undefined in its native box.
|
||||
if (mapped.kind === "dyn") return mapped;
|
||||
const byKey = new Map<string, IrType>();
|
||||
const arms = mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
|
||||
for (const a of arms) {
|
||||
@@ -1911,6 +1932,11 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
);
|
||||
}
|
||||
if (overridden && isJsSourceFile(member.getSourceFile())) {
|
||||
// Fluent overrides return the same native receiver. Keep the
|
||||
// inherited return ABI and upcast `this` when lowering returns.
|
||||
if (overridden.sig.ret.kind === "object" && base &&
|
||||
(base.def.name === overridden.sig.ret.className || lowerer.isSubclassOf(base.def.name, overridden.sig.ret.className)) &&
|
||||
returnsOnlyThis(member)) ft.ret = overridden.sig.ret;
|
||||
// An unannotated JS parameter can use the base's typed ABI.
|
||||
// Defaults stay local to each declaration, so only inherited
|
||||
// required/optional slots participate in this refinement.
|
||||
@@ -2386,6 +2412,10 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
if (isJsSourceFile(decl.getSourceFile())) {
|
||||
const armed = undefArmedFieldType(lowerer, p);
|
||||
if (armed !== null) {
|
||||
// Unknown JS properties use the instance bag: they do not
|
||||
// exist until assigned, and a subclass may declare the same
|
||||
// name as an ordinary layout field.
|
||||
if (armed.kind === "dyn") continue;
|
||||
fields.set(p.name, armed);
|
||||
fieldOrder.push({ name: p.name, type: armed, initializer: undefined });
|
||||
continue;
|
||||
|
||||
@@ -2163,8 +2163,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
`reading the abstract property '${expr.name.text}' through a '${lowerer.checker.typeToString(lowerer.typeOf(expr.expression))}'-typed receiver (abstract property declarations are erased at runtime, so no shared slot exists — type the receiver as the concrete class, or declare an abstract getter instead)`,
|
||||
);
|
||||
}
|
||||
if (isDynTypedRefType(recvLowered.type)) {
|
||||
const info = lowerer.classes.get(recvLowered.type.className);
|
||||
// Array reads may add an undefined arm even when the checker still
|
||||
// reports a class. Its property bag must remain reachable, while
|
||||
// an absent element takes the normal checked-value TypeError.
|
||||
const nativeReceiver = lowerer.stripUndefinedArm(recvLowered.type);
|
||||
if (isDynTypedRefType(nativeReceiver)) {
|
||||
const info = lowerer.classes.get(nativeReceiver.className);
|
||||
if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream) {
|
||||
return lowerer.maybeNarrow({
|
||||
kind: "dynKeyGet", value: lowerer.coerceToExpected(recvLowered, DYN),
|
||||
@@ -4679,7 +4683,7 @@ export function lowerOptionalNumber(
|
||||
// (`pkg.workspaces.packages["0"]` — the lowering world types the
|
||||
// unknown-rooted chain `any`); a non-dyn lowering falls through to
|
||||
// the fences below (re-lowering is pure IR construction).
|
||||
if (receiverIr?.kind === "dyn" || receiverIr === null) {
|
||||
if (receiverIr?.kind === "dyn" || receiverIr?.kind === "object" || receiverIr?.kind === "union" || receiverIr === null) {
|
||||
const obj = lowerer.lowerExpr(expr.expression);
|
||||
// A generic mapped type can stay unresolved at this body use even
|
||||
// though its instantiated parameter has a concrete record ABI.
|
||||
@@ -4690,7 +4694,7 @@ export function lowerOptionalNumber(
|
||||
return lowerer.lowerRecordKeyRead(expr, obj.type.shapeId, shape);
|
||||
}
|
||||
}
|
||||
if (obj.type.kind === "dyn") {
|
||||
if (obj.type.kind === "dyn" || isDynTypedRefType(obj.type)) {
|
||||
const rawKey = lowerer.lowerExpr(expr.argumentExpression);
|
||||
// Number, bool, and DYN keys stringify (ToPropertyKey) — the
|
||||
// dyn-keyed read `catchWarning[warning.name]` where the property
|
||||
@@ -4699,7 +4703,7 @@ export function lowerOptionalNumber(
|
||||
if (key.type.kind === "string") {
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
return lowerer.maybeNarrow(
|
||||
{ kind: "dynKeyGet", key, ...(opt ? { optional: true as const } : {}), value: obj, type: DYN, loc: locOf(expr) },
|
||||
{ kind: "dynKeyGet", key, ...(opt ? { optional: true as const } : {}), value: lowerer.coerceToExpected(obj, DYN), type: DYN, loc: locOf(expr) },
|
||||
expr,
|
||||
);
|
||||
}
|
||||
@@ -9913,6 +9917,9 @@ function representedClassFieldTarget(
|
||||
if (receiverIr?.kind === "object") {
|
||||
return classFieldTarget(lowerer, access.expression, receiverIr, access.name.text);
|
||||
}
|
||||
if (receiverIr?.kind === "union" && hasClassPayload(lowerer, receiverIr)) {
|
||||
return representedClassFieldTarget(lowerer, access.expression, access.name.text, lowerer.lowerExpr(access.expression));
|
||||
}
|
||||
if (receiverIr?.kind === "record") {
|
||||
const shape = lowerer.shapes.get(receiverIr.shapeId);
|
||||
const fieldType = shape?.fields.find((f) => f.name === access.name.text)?.type;
|
||||
|
||||
@@ -39,7 +39,7 @@ import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerUnionFieldWrite } from "./expressions/union-field-write.js";
|
||||
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { isParseArgsDynTypeName, isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
|
||||
import { isParseArgsDynTypeName, isUnitOnlyTsType, jsOpenObjectType, unitOnlyUnion } from "../type-mapper.js";
|
||||
import { canonicalBuiltinModule } from "../builtin-modules.js";
|
||||
import { isRelativeSpecifier } from "../workspace-registry.js";
|
||||
import { probeNodeRequireRefusal } from "../npm.js";
|
||||
@@ -3830,7 +3830,8 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
}
|
||||
const preservesObjectIdentity = init.type.kind === "dyn" &&
|
||||
(inferredObjectType?.kind === "record" ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType?.kind === "func") ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
(inferredObjectType.kind === "func" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
(isUnitType(inferredObjectType) || inferredObjectType.kind === "union" && lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType)))) &&
|
||||
!decl.type && !hasJsTypeAnnotation(decl);
|
||||
|
||||
@@ -6333,6 +6333,8 @@ function canBoxDynComposite(
|
||||
case "regex":
|
||||
case "url":
|
||||
return true;
|
||||
case "object":
|
||||
return isDynTypedRefType(t);
|
||||
case "func":
|
||||
return canBoxFuncIntoDyn(t, getRecord, getUnion);
|
||||
case "set":
|
||||
@@ -6386,6 +6388,10 @@ export function canDynCheckTo(
|
||||
if (t.kind === "bytes") return true;
|
||||
if (t.kind === "classval") return true;
|
||||
if (t.kind === "object" && t.className === "%Error") return true;
|
||||
// Native class arrays validate each branded reference, never fabricate
|
||||
// class instances from plain object data.
|
||||
if (isDynTypedRefType(t)) return true;
|
||||
if (t.kind === "array" && isDynTypedRefType(t.elem)) return true;
|
||||
if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion);
|
||||
if (DYN_HANDLE_KINDS.has(t.kind)) return true;
|
||||
if (t.kind === "union") {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
test("checked class arrays reject unbranded and differently branded elements", async () => {
|
||||
const dir = mkdtempSync("/tmp/scriptc-class-arrays-");
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
writeFileSync(entry, `
|
||||
class Item { value = 1; }
|
||||
class Other { value = 1; }
|
||||
function check(input: unknown): void {
|
||||
try { const items = input as Item[]; console.log('accepted', items.length); }
|
||||
catch (error) { console.log('rejected', error instanceof TypeError); }
|
||||
}
|
||||
check([new Item()]);
|
||||
check([new Other()]);
|
||||
check([{value: 1}]);
|
||||
const mixed: unknown[] = [];
|
||||
mixed.push(new Item()); mixed.push({value: 1});
|
||||
check(mixed);
|
||||
check(null);
|
||||
check([1]);
|
||||
check([]);
|
||||
`);
|
||||
const result = await compile(entry, {
|
||||
dynamic: false, outDir: dir, outPath: join(dir, "program"),
|
||||
sanitize: process.env["SCRIPTC_SAN"] === "1",
|
||||
});
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.diagnostics));
|
||||
const child = spawnSync(result.binaryPath, [], {encoding: "utf8", timeout: 30_000});
|
||||
expect(child.error).toBeUndefined();
|
||||
expect({status: child.status, signal: child.signal, stdout: child.stdout, stderr: child.stderr}).toEqual({
|
||||
status: 0, signal: null, stderr: "",
|
||||
stdout: "accepted 1\nrejected true\nrejected true\nrejected true\nrejected true\nrejected true\naccepted 0\n",
|
||||
});
|
||||
} finally {
|
||||
rmSync(dir, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
@@ -11823,6 +11823,42 @@
|
||||
"<repo>/tests/corpus/js-nested-native-descriptors.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-class-fluent-overrides.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-class-fluent-overrides.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-class-subclass-properties.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-class-subclass-properties.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-object-shared-arrays.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-object-shared-arrays.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-optional-class-fields.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-optional-class-fields.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-class-array-boundaries.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-class-array-boundaries.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-class-type-predicates.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-class-type-predicates.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1951,6 +1951,9 @@ const ScrDynJsvalOps *scr_dyn_jsval_ops(void) {
|
||||
|
||||
bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name) {
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (d->v.typed_ref.type_key_len >= 7 &&
|
||||
memcmp(d->v.typed_ref.type_key, "object:", 7) == 0)
|
||||
return strcmp(name, "object") == 0;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
|
||||
bool out = scr_dyn_isl_typeof_is(materialized, name);
|
||||
if (materialized->kind != SCR_DYN_JSVAL) {
|
||||
@@ -1978,6 +1981,8 @@ bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name) {
|
||||
|
||||
bool scr_dyn_isl_is_array(const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (d->v.typed_ref.type_key_len >= 7 &&
|
||||
memcmp(d->v.typed_ref.type_key, "object:", 7) == 0) return false;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
|
||||
bool out = materialized->kind == SCR_DYN_ARR ||
|
||||
scr_dyn_isl_is_array(materialized);
|
||||
@@ -2425,6 +2430,9 @@ bool scr_dyn_truthy(const ScrDyn *d) {
|
||||
ScrStr *scr_dyn_typeof(const ScrDyn *d) {
|
||||
const char *s;
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (d->v.typed_ref.type_key_len >= 7 &&
|
||||
memcmp(d->v.typed_ref.type_key, "object:", 7) == 0)
|
||||
return scr_str_new("object", 6);
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
|
||||
ScrStr *out = scr_dyn_typeof(materialized);
|
||||
scr_dyn_release(materialized);
|
||||
@@ -5963,11 +5971,18 @@ static bool scr_ffi_signature_equal(const ScrDyn *entry, const ScrDyn *definitio
|
||||
if (!definition || definition->kind != SCR_DYN_OBJ) return false;
|
||||
ScrDyn *expected = scr_dyn_obj_get(entry, "arguments", 9);
|
||||
ScrDyn *actual = scr_dyn_obj_get(definition, "arguments", 9);
|
||||
if (!actual || actual->kind != SCR_DYN_ARR || actual->v.arr.len != expected->v.arr.len ||
|
||||
!scr_ffi_abi_equal(scr_dyn_obj_get(entry, "return", 6), scr_dyn_obj_get(definition, "return", 6))) return false;
|
||||
for (size_t i = 0; i < actual->v.arr.len; i++)
|
||||
if (!scr_ffi_abi_equal(actual->v.arr.items[i], expected->v.arr.items[i])) return false;
|
||||
return true;
|
||||
if (!actual) return false;
|
||||
// JS object properties can retain a native array by reference. Read its
|
||||
// current contents when checking the requested ABI, just like a dyn array.
|
||||
ScrDyn *view = actual->kind == SCR_DYN_TYPED_REF
|
||||
? scr_dyn_typed_ref_materialize(actual) : scr_dyn_retain(actual);
|
||||
bool matches = view && view->kind == SCR_DYN_ARR && view->v.arr.len == expected->v.arr.len &&
|
||||
scr_ffi_abi_equal(scr_dyn_obj_get(entry, "return", 6), scr_dyn_obj_get(definition, "return", 6));
|
||||
if (matches) for (size_t i = 0; i < view->v.arr.len; i++) {
|
||||
if (!scr_ffi_abi_equal(view->v.arr.items[i], expected->v.arr.items[i])) { matches = false; break; }
|
||||
}
|
||||
scr_dyn_release(view);
|
||||
return matches;
|
||||
}
|
||||
|
||||
static void scr_ffi_callback_remove(ScrDyn *entry, ScrDyn *catalog) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class Base {
|
||||
constructor() { this.value = 1; }
|
||||
copy(source) { this.value = source.value; return this; }
|
||||
dispatch(source) { return this.copy(source); }
|
||||
}
|
||||
class Child extends Base {
|
||||
constructor() { super(); this.extra = 2; }
|
||||
copy(source) { super.copy(source); this.extra = source.extra; return this; }
|
||||
childOnly() { return this.extra; }
|
||||
}
|
||||
class Leaf extends Child {
|
||||
constructor() { super(); this.leaf = 3; }
|
||||
copy(source) {
|
||||
super.copy(source);
|
||||
if (source.leaf === 0) return (this);
|
||||
this.leaf = source.leaf;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
const source = new Leaf();
|
||||
source.value = 4; source.extra = 5; source.leaf = 6;
|
||||
const target = new Leaf();
|
||||
console.log('direct', target.copy(source) === target, target.value, target.extra, target.leaf);
|
||||
console.log('chain', target.copy(source).childOnly(), target.copy(source).leaf);
|
||||
source.extra = 8;
|
||||
console.log('virtual', target.dispatch(source) === target, target.extra);
|
||||
source.leaf = 0;
|
||||
console.log('early', target.copy(source) === target, target.leaf);
|
||||
@@ -0,0 +1,43 @@
|
||||
class Base {
|
||||
constructor() { this.base = 1; }
|
||||
read(key) { return this[key]; }
|
||||
write(key, value) { this[key] = value; }
|
||||
named() { return this.extra; }
|
||||
attach(value) { this.extra = value; }
|
||||
remember(key, value) {
|
||||
if (this.cache === undefined) this.cache = {};
|
||||
this.cache[key] = value;
|
||||
}
|
||||
recall(key) { return this.cache[key]; }
|
||||
saved() {
|
||||
const cache = this.cache;
|
||||
if (cache === undefined) return 'empty';
|
||||
return Object.keys(cache).join(',');
|
||||
}
|
||||
}
|
||||
class Child extends Base {
|
||||
constructor() { super(); this.extra = 2; this.storage = 3; }
|
||||
get doubled() { return this.storage * 2; }
|
||||
set doubled(value) { this.storage = value / 2; }
|
||||
}
|
||||
class Leaf extends Child {
|
||||
constructor() { super(); this.leaf = 4; }
|
||||
get doubled() { return this.storage * 3; }
|
||||
set doubled(value) { this.storage = value / 3; }
|
||||
}
|
||||
const base = new Base(), child = new Child(), leaf = new Leaf();
|
||||
console.log('absent', base.read('extra'), Object.keys(base).join(','));
|
||||
base.attach({answer: 42});
|
||||
console.log('added', base.read('extra').answer, Object.keys(base).join(','));
|
||||
console.log('inherited', child.read('extra'), child.named(), leaf.read('leaf'));
|
||||
child.write('extra', 9); leaf.write('leaf', 10);
|
||||
child.write('doubled', 12); leaf.write('doubled', 12);
|
||||
console.log('writes', child.extra, leaf.leaf, child.storage, leaf.storage);
|
||||
console.log('accessors', child.read('doubled'), leaf.read('doubled'));
|
||||
let order = '';
|
||||
function receiver() { order += 'r'; return leaf; }
|
||||
function key() { order += 'k'; return 'leaf'; }
|
||||
console.log('read', receiver()[key()], order);
|
||||
base.remember('first', 1); base.remember('second', {value: 2});
|
||||
console.log('cache', base.recall('first'), base.recall('second').value, Object.keys(base.cache).join(','));
|
||||
console.log('saved', base.saved(), child.saved());
|
||||
@@ -0,0 +1,11 @@
|
||||
const shared = [1, 2, 3];
|
||||
const a = {values: shared}, b = {nested: {values: shared}};
|
||||
console.log('identity', a.values === b.nested.values, a.values === shared);
|
||||
a.values[1] = 8;
|
||||
console.log('write', shared[1], b.nested.values[1]);
|
||||
shared.push(4);
|
||||
console.log('push', a.values.length, b.nested.values.join(','));
|
||||
b.nested.values.pop();
|
||||
console.log('pop', shared.length, a.values.join(','));
|
||||
const separate = {values: [1, 8, 3]};
|
||||
console.log('distinct', separate.values === a.values);
|
||||
@@ -0,0 +1,26 @@
|
||||
class Vector {
|
||||
constructor() { this.x = 1; this.y = 2; }
|
||||
get doubled() { return this.x * 2; }
|
||||
set doubled(value) { this.x = value / 2; }
|
||||
}
|
||||
class Holder {
|
||||
initialize() { this.vector = new Vector(); }
|
||||
}
|
||||
const holder = new Holder();
|
||||
holder.initialize();
|
||||
let calls = 0;
|
||||
function receiver() { calls++; return holder; }
|
||||
receiver().vector.x = 7;
|
||||
holder.vector.y += 3;
|
||||
holder.vector.doubled = 18;
|
||||
console.log('nested', holder.vector.x, holder.vector.y, calls);
|
||||
|
||||
class Positioned {
|
||||
constructor() { Object.defineProperty(this, 'position', {value: new Vector(), enumerable: true}); }
|
||||
}
|
||||
const objects = [new Positioned()];
|
||||
console.log('array', objects[0].position.x);
|
||||
objects[0].position.x = 4;
|
||||
console.log('write', objects[0].position.x);
|
||||
try { console.log(objects[1].position.x); }
|
||||
catch (error) { console.log('missing', error instanceof TypeError, error.message); }
|
||||
@@ -0,0 +1,29 @@
|
||||
class Plane {
|
||||
constant: number;
|
||||
constructor(value: number) { this.constant = value; }
|
||||
}
|
||||
function roundtrip(value: unknown): Plane[] { return value as Plane[]; }
|
||||
function optional(value: unknown): Plane[] | null { return value as Plane[] | null; }
|
||||
const plane = new Plane(2);
|
||||
const planes = [plane];
|
||||
const copy = roundtrip(planes);
|
||||
console.log('array', copy.length, copy[0] === plane, copy[0].constant);
|
||||
copy[0].constant = 4;
|
||||
console.log('mutation', plane.constant, optional(planes)![0] === plane, optional(null) === null);
|
||||
function boxedRecord(value: unknown) { return value; }
|
||||
const boxed = boxedRecord({plane, planes});
|
||||
const view = boxed as {plane: unknown; planes: unknown[]};
|
||||
console.log('record', view.plane === plane, view.planes[0] === plane);
|
||||
const callback: unknown = (value: Plane): number => value.constant;
|
||||
console.log('callback', (callback as (value: Plane) => number)(plane));
|
||||
|
||||
class Tree {
|
||||
children: Tree[] = [];
|
||||
select: (node: Tree) => Tree[] = (node) => node.children;
|
||||
}
|
||||
const root = new Tree(), child = new Tree();
|
||||
root.children.push(child);
|
||||
const tree: unknown = root;
|
||||
const restored = tree as Tree;
|
||||
console.log('recursive', restored === root, restored.select(root)[0] === child);
|
||||
console.log('view', Object.keys(tree as object).sort().join(','));
|
||||
@@ -0,0 +1,15 @@
|
||||
class Opaque {
|
||||
entries: Map<string, number> = new Map();
|
||||
self: Opaque | null = null;
|
||||
}
|
||||
function inspect(value: unknown): void {
|
||||
console.log(typeof value, typeof value === 'object', typeof value === 'function', Array.isArray(value));
|
||||
}
|
||||
const value = new Opaque();
|
||||
value.self = value;
|
||||
inspect(value);
|
||||
inspect([value]);
|
||||
inspect(null);
|
||||
inspect(1);
|
||||
inspect('text');
|
||||
value.self = null;
|
||||
@@ -2,8 +2,9 @@ import { createRequire } from "node:module";
|
||||
const requireModule = createRequire(import.meta.url);
|
||||
const ffi = requireModule("node:ffi");
|
||||
console.log(ffi === requireModule("node:ffi"));
|
||||
const u64Arguments = ["u64"];
|
||||
const definitions = {
|
||||
nativeU64: { arguments: ["u64"], return: "u64" },
|
||||
nativeU64: { arguments: u64Arguments, return: "u64" },
|
||||
nativePointer: { arguments: ["pointer"], return: "pointer" },
|
||||
nativeI64: { arguments: ["int64"], return: "int64" },
|
||||
nativeByte: { arguments: ["bool"], return: "uint8" },
|
||||
@@ -44,4 +45,9 @@ second.lib.close();
|
||||
if (process.env.FFI_STATIC === "1") {
|
||||
try { ffi.dlopen(process.env.FFI_LIBRARY, { nativeU64: { arguments: ["i64"], return: "u64" } }); }
|
||||
catch { console.log("signature mismatch"); }
|
||||
u64Arguments[0] = "i64";
|
||||
try { ffi.dlopen(process.env.FFI_LIBRARY, definitions); }
|
||||
catch { console.log("mutated signature"); }
|
||||
try { ffi.dlopen(process.env.FFI_LIBRARY, { nativeU64: { return: "u64" } }); }
|
||||
catch (error) { console.log("missing arguments", error.name, error.code); }
|
||||
}
|
||||
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { MeshNormalMaterial } from 'three/src/materials/MeshNormalMaterial.js';
|
||||
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial.js';
|
||||
import { MeshStandardMaterial } from 'three/src/materials/MeshStandardMaterial.js';
|
||||
import { Plane } from 'three/src/math/Plane.js';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
const normal = new MeshNormalMaterial({wireframe: true, opacity: 0.5, transparent: true});
|
||||
console.log('normal', normal.type, normal.wireframe, normal.opacity, normal.transparent);
|
||||
normal.setValues({wireframe: false, opacity: 0.8});
|
||||
normal.needsUpdate = true;
|
||||
console.log('updated', normal.wireframe, normal.opacity, normal.version);
|
||||
const copy = normal.clone();
|
||||
console.log('clone', copy !== normal, copy.type, copy.opacity, copy.normalScale.x);
|
||||
copy.normalScale.x = 2;
|
||||
console.log('independent', normal.normalScale.x, copy.normalScale.x);
|
||||
|
||||
const plane = new Plane(new Vector3(1, 0, 0), 2);
|
||||
normal.setValues({clippingPlanes: [plane], clipIntersection: true});
|
||||
if (normal.clippingPlanes !== null) {
|
||||
console.log('clip', normal.clippingPlanes.length, normal.clippingPlanes[0] === plane, normal.clipIntersection);
|
||||
normal.clippingPlanes[0].constant = 3;
|
||||
}
|
||||
console.log('plane', plane.constant);
|
||||
normal.setValues({clippingPlanes: null});
|
||||
console.log('unclipped', normal.clippingPlanes === null);
|
||||
|
||||
const basic = new MeshBasicMaterial({color: 0xff0000});
|
||||
console.log('red', basic.color.r, basic.color.g, basic.color.b);
|
||||
basic.setValues({color: 0x0000ff});
|
||||
console.log('blue', basic.color.r, basic.color.g, basic.color.b);
|
||||
console.log('basic-copy', basic.clone().color.b);
|
||||
const standard = new MeshStandardMaterial({roughness: 0.25, metalness: 0.8, color: 0x00ff00});
|
||||
console.log('standard', standard.type, standard.roughness, standard.metalness, standard.color.g);
|
||||
console.log('standard-copy', standard.clone().roughness, standard.clone().metalness);
|
||||
|
||||
let count = 0;
|
||||
function disposed(event) {
|
||||
count++;
|
||||
console.log('event', event.type, event.target === normal, this === normal);
|
||||
}
|
||||
normal.addEventListener('dispose', disposed);
|
||||
normal.dispose();
|
||||
normal.removeEventListener('dispose', disposed);
|
||||
normal.dispose();
|
||||
console.log('events', count);
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import { Mesh } from 'three/src/objects/Mesh.js';
|
||||
import { BoxGeometry } from 'three/src/geometries/BoxGeometry.js';
|
||||
import { MeshNormalMaterial } from 'three/src/materials/MeshNormalMaterial.js';
|
||||
import { Scene } from 'three/src/scenes/Scene.js';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
const geometry = new BoxGeometry(2, 4, 6);
|
||||
const material = new MeshNormalMaterial();
|
||||
const mesh = new Mesh(geometry, material);
|
||||
console.log('mesh', mesh.type, mesh.isMesh, mesh.geometry === geometry, mesh.material === material);
|
||||
const scene = new Scene();
|
||||
scene.add(mesh);
|
||||
mesh.position.set(1, 2, 3);
|
||||
scene.updateMatrixWorld();
|
||||
const point = mesh.getVertexPosition(0, new Vector3());
|
||||
console.log('vertex', point.x, point.y, point.z);
|
||||
console.log('world', mesh.matrixWorld.elements[12], mesh.matrixWorld.elements[13], mesh.matrixWorld.elements[14]);
|
||||
const copy = mesh.clone();
|
||||
console.log('copy', copy !== mesh, copy.geometry === geometry, copy.material === material, copy.position.x);
|
||||
copy.position.x = 7;
|
||||
console.log('independent', mesh.position.x, copy.position.x);
|
||||
const other = new Mesh(geometry, material);
|
||||
console.log('fluent', other.copy(mesh) === other, other.copy(mesh).geometry === geometry);
|
||||
const child = new Mesh(geometry, material);
|
||||
child.position.y = 3;
|
||||
mesh.add(child);
|
||||
const recursive = mesh.clone();
|
||||
console.log('children', recursive.children.length, recursive.children[0] !== child, recursive.children[0].position.y);
|
||||
console.log('shallow', mesh.clone(false).children.length);
|
||||
const materials = [material, new MeshNormalMaterial({wireframe: true})];
|
||||
const multi = new Mesh(geometry, materials);
|
||||
const multiCopy = multi.clone();
|
||||
console.log('materials', multiCopy.material !== multi.material, multiCopy.material.length, multiCopy.material[0] === material, multiCopy.material[1].wireframe);
|
||||
@@ -31,7 +31,7 @@ describe.each(["llvm"] as const)("static node:ffi catalog, %s", backend => {
|
||||
expect(native.error).toBeUndefined();
|
||||
expect(native.stderr).toBe("");
|
||||
expect(native.status).toBe(0);
|
||||
expect(native.stdout).toBe("true\n9007199254740993n\ntrue\n0n 0n\n65 255\n-9223372036854775808n 18446744073709551615n\n" + "TypeError ERR_INVALID_ARG_VALUE\n".repeat(12) + "5\narity\narity\nsignature\nclosed\n42n\nsignature mismatch\n");
|
||||
expect(native.stdout).toBe("true\n9007199254740993n\ntrue\n0n 0n\n65 255\n-9223372036854775808n 18446744073709551615n\n" + "TypeError ERR_INVALID_ARG_VALUE\n".repeat(12) + "5\narity\narity\nsignature\nclosed\n42n\nsignature mismatch\nmutated signature\nmissing arguments Error SC2020\n");
|
||||
// A separately linked Node library is only the oracle. scriptc links
|
||||
// the object file and must work before this dynamic library exists.
|
||||
if (oracle) {
|
||||
@@ -42,7 +42,7 @@ describe.each(["llvm"] as const)("static node:ffi catalog, %s", backend => {
|
||||
expect(node.error).toBeUndefined();
|
||||
expect(node.stderr).toBe(native.stderr);
|
||||
expect(node.status).toBe(native.status);
|
||||
expect(node.stdout + "signature mismatch\n").toBe(native.stdout);
|
||||
expect(node.stdout + "signature mismatch\nmutated signature\nmissing arguments Error SC2020\n").toBe(native.stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ function run(command: string, args: string[]) {
|
||||
return { stdout: result.stdout, stderr: result.stderr, status: result.status, signal: result.signal };
|
||||
}
|
||||
|
||||
const cases = ["math", "spatial", "attributes", "geometry", "scene"].flatMap((fixture) =>
|
||||
const cases = ["math", "spatial", "attributes", "geometry", "scene", "materials", "mesh"].flatMap((fixture) =>
|
||||
["native", "wasm32-wasi"].map((target) => ({ fixture, target })));
|
||||
test.for(cases)("published three.js $fixture runs statically through LLVM on $target", async ({ fixture, target }, context) => {
|
||||
const entry = join(import.meta.dirname, `../fixtures/three/${fixture}.mjs`);
|
||||
|
||||
Reference in New Issue
Block a user