mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Preserve JavaScript values beyond null initializers
- Keep null-initialized fields and inferred returns in mutable native storage, including inherited resets and saved callbacks. - Convert checked native values with Number using JavaScript coercion and catchable errors.
This commit is contained in:
@@ -1006,9 +1006,9 @@ function completeFuncValueArgs(
|
||||
const sig = lowerer.checker.getSignatureFromDeclaration(decl);
|
||||
if (!sig) lowerer.unsupported("SC1090", decl, "this function form");
|
||||
const retTsType = lowerer.checker.getReturnTypeOfSignature(sig);
|
||||
// An inferred undefined return in JS can read a mutable checked-value
|
||||
// field. Keep the returned value instead of erasing it to void.
|
||||
if (isJsSourceFile(decl.getSourceFile()) && (retTsType.flags & ts.TypeFlags.Undefined) !== 0) return DYN;
|
||||
// An inferred null/undefined return in JS can read a mutable checked-value
|
||||
// field. Keep the actual value instead of restricting it to the initializer.
|
||||
if (isJsSourceFile(decl.getSourceFile()) && (retTsType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0) return DYN;
|
||||
// A body that always throws infers `never` — as a RETURN type that is
|
||||
// void with a stronger guarantee (`() => never` is assignable to
|
||||
// `() => void`), and throw-only callbacks are ordinary code
|
||||
@@ -5074,6 +5074,7 @@ function lowerNumberConstructorValue(lowerer: Lowerer, argNode: ts.Expression, l
|
||||
};
|
||||
}
|
||||
if (arg.type.kind === "string") return { kind: "libCall", fn: "num.fromString", args: [arg], type: F64, loc };
|
||||
if (arg.type.kind === "dyn") return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [arg], type: F64, loc };
|
||||
const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc);
|
||||
if (optionalNumber) return optionalNumber;
|
||||
const scalarUnionNumber = lowerScalarUnionNumber(lowerer, arg, argNode, loc);
|
||||
|
||||
@@ -2207,6 +2207,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
// TS-annotated `unknown` fields keep their fence (KEEP NARROW
|
||||
// applies where an annotation could say better).
|
||||
let type = t ? (lowerer.mapTypeOf(t) ?? dynFallbackType(lowerer, assign, t)) : null;
|
||||
if (t && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 && isJsSourceFile(decl.getSourceFile())) type = DYN;
|
||||
if (type) type = jsOpenObjectType(assign, type, lowerer.shapes, lowerer.unions);
|
||||
if (type?.kind === "dyn") {
|
||||
type = inferredEmptyCollectionFieldType(lowerer, decl, name, rhs) ?? type;
|
||||
|
||||
@@ -935,7 +935,10 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
|
||||
return mapped;
|
||||
});
|
||||
const retT = lowerer.checker.getReturnTypeOfSignature(sig);
|
||||
const ret: IrType = retT.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retT) ?? DYN;
|
||||
const sigDecl = lowerer.checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
const ret: IrType = jsUnitReturn ? DYN : retT.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retT) ?? DYN;
|
||||
return { kind: "func", params, ret };
|
||||
}
|
||||
|
||||
@@ -956,7 +959,9 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null {
|
||||
) return null;
|
||||
const shapes = paramShapes(lowerer, decl.parameters);
|
||||
const retType = lowerer.checker.getReturnTypeOfSignature(sigs[0]!);
|
||||
const ret = retType.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retType) ?? DYN;
|
||||
const ret = retType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)
|
||||
? DYN
|
||||
: retType.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retType) ?? DYN;
|
||||
return funcTypeFromParamShapes([...shapes, { type: DYN, mode: "arguments" }], ret);
|
||||
}
|
||||
|
||||
@@ -4978,8 +4983,9 @@ export class Lowerer {
|
||||
irTypeOf(node: ts.Node): IrType {
|
||||
const t = this.typeOf(node);
|
||||
// JS inference can leave mutable fields, accessor parameters and
|
||||
// locals at undefined. They still carry values; void has no storage.
|
||||
if (isJsSourceFile(node.getSourceFile()) && (t.flags & ts.TypeFlags.Undefined) !== 0) return DYN;
|
||||
// locals at null/undefined despite later writes. Keep mutable native
|
||||
// storage instead of a slot restricted to the initializer's unit value.
|
||||
if (isJsSourceFile(node.getSourceFile()) && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0) return DYN;
|
||||
// A never-tainted JS type (neverTaintedJsType) maps — never rides as
|
||||
// f64 — but must not: pre-empt the mapping so the JS fallback below
|
||||
// answers instead.
|
||||
|
||||
@@ -2726,8 +2726,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations.
|
||||
const ret = retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx);
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
const ret = jsUnitReturn ? DYN : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx);
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
|
||||
@@ -76,3 +76,43 @@ const dormant = new DormantReturns();
|
||||
console.log("dormant", dormant.read(), new ShortDormantReturn().read());
|
||||
const repaired = new RepairedReturns();
|
||||
console.log("repaired", repaired.dispatch("item"), repaired.selected());
|
||||
|
||||
// A null initializer does not restrict an inferred JS field to null forever.
|
||||
class MutableNull {
|
||||
value = null;
|
||||
set(value) { this.value = value; }
|
||||
read() { return this.value; }
|
||||
get current() { return this.value; }
|
||||
set current(value) { this.value = value; }
|
||||
}
|
||||
class ResetNull extends MutableNull {
|
||||
value;
|
||||
constructor(value) {
|
||||
super();
|
||||
console.log('reset', this.value === undefined);
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
class ConstructorNull {
|
||||
constructor() { this.value = null; }
|
||||
set(value) { this.value = value; }
|
||||
read() { return this.value; }
|
||||
}
|
||||
const mutableNull = new MutableNull();
|
||||
console.log('null initial', mutableNull.read(), mutableNull.current);
|
||||
mutableNull.set('updated');
|
||||
console.log('null written', mutableNull.read(), mutableNull.current);
|
||||
mutableNull.current = 42;
|
||||
console.log('accessor', mutableNull.read());
|
||||
console.log('numeric field', Number(mutableNull.value));
|
||||
const resetNull = new ResetNull('derived');
|
||||
console.log('inherited', resetNull.read());
|
||||
const readMutableNull = () => mutableNull.value;
|
||||
function readNullField() { return mutableNull.value; }
|
||||
const savedNullReader = readNullField;
|
||||
console.log('readers', readMutableNull(), savedNullReader());
|
||||
const constructorNull = new ConstructorNull();
|
||||
constructorNull.set('constructor');
|
||||
console.log('constructor field', constructorNull.read());
|
||||
mutableNull.set(null);
|
||||
console.log('null reset', readMutableNull(), savedNullReader());
|
||||
|
||||
@@ -29,3 +29,52 @@ function withDefault(value = 41) {
|
||||
withDefault();
|
||||
withDefault(undefined);
|
||||
withDefault(5);
|
||||
|
||||
function readMutableUnit(first) {
|
||||
return arguments.length === 1 ? first.value : undefined;
|
||||
}
|
||||
const readThroughValue = readMutableUnit;
|
||||
const mutableUnit = { value: null };
|
||||
console.log('unit through value', readThroughValue(mutableUnit));
|
||||
mutableUnit.value = 'changed';
|
||||
console.log('updated through value', readThroughValue(mutableUnit));
|
||||
|
||||
// Saved arguments readers keep the implementation's return ABI even when
|
||||
// JavaScript inference sees only a null/undefined field initializer.
|
||||
class ArgumentState {
|
||||
nullable = null;
|
||||
missing = undefined;
|
||||
set(value) { this.nullable = value; this.missing = value; }
|
||||
}
|
||||
const argumentState = new ArgumentState();
|
||||
function readNullable(first) {
|
||||
console.log('null args', arguments.length, first);
|
||||
return argumentState.nullable;
|
||||
}
|
||||
function readMissing(first) {
|
||||
console.log('undefined args', arguments.length, first);
|
||||
return argumentState.missing;
|
||||
}
|
||||
const savedNullable = readNullable;
|
||||
const savedMissing = readMissing;
|
||||
console.log('initial returns', savedNullable('a', 'b'), savedMissing());
|
||||
argumentState.set('updated');
|
||||
console.log('updated returns', savedNullable(), savedMissing('c', 'd'));
|
||||
/** @returns {null} */
|
||||
function literalNull(first) {
|
||||
console.log('literal args', arguments.length, first);
|
||||
return null;
|
||||
}
|
||||
class ArgumentReaders {
|
||||
nullReader = literalNull;
|
||||
undefinedReader = function (first) {
|
||||
console.log('literal undefined args', arguments.length, first);
|
||||
return undefined;
|
||||
};
|
||||
mutableReader = readNullable;
|
||||
}
|
||||
const argumentReaders = new ArgumentReaders();
|
||||
const fieldNull = argumentReaders.nullReader;
|
||||
const fieldUndefined = argumentReaders.undefinedReader;
|
||||
const fieldMutable = argumentReaders.mutableReader;
|
||||
console.log('field returns', fieldNull(4, 5), fieldUndefined(6), fieldMutable());
|
||||
|
||||
@@ -10,3 +10,11 @@ function nullValue() { effects += "n"; return null; }
|
||||
function undefinedValue() { effects += "u"; return undefined; }
|
||||
console.log(Number(void value()), effects);
|
||||
console.log(Number(nullValue()), Number(undefinedValue()), effects);
|
||||
|
||||
// The same native coercion applies once JS storage carries checked values.
|
||||
/** @param {unknown} input */
|
||||
function numeric(input) { return Number(input); }
|
||||
console.log(numeric('17'), numeric(true), numeric([]), numeric([3]), numeric({}));
|
||||
console.log(numeric({ valueOf() { effects += 'c'; return '23'; } }), effects);
|
||||
try { numeric({ valueOf() { throw new RangeError('numeric hook'); } }); }
|
||||
catch (error) { console.log(error.name, error.message); }
|
||||
|
||||
Reference in New Issue
Block a user