Background
While addressing PR #754 review item 2b (throw on class decorator that returns a replacement class), the natural check would have been:
if ret !== undefined { throw TypeError(...) }
The PR author had to fall back to:
if typeof ret === "function" { throw TypeError(...) }
…with this inline comment in crates/perry-hir/src/lower.rs:
Check typeof ret === "function" rather than ret !== undefined: Perry's lowering for a function expression with no explicit return currently leaves a numeric sentinel in the return slot rather than the NaN-boxed undefined value, so !== undefined would false-positive on side-effect-only decorators.
So bare @Injectable (function expression with no return) returns some non-undefined value — a numeric sentinel — instead of the NaN-boxed undefined that strict JS semantics require.
Why this matters
- It's masking a wider correctness bug. The decorator workaround only catches the
typeof === "function" case. A decorator that returns a primitive or plain object would currently be silently discarded; once this is fixed, the decorator check can be tightened to the cleaner !== undefined shape and catch all replacement-attempt patterns.
- Any user code that does
if (someFn() !== undefined) against a function with no explicit return is silently wrong. That's a fundamental JS semantic and the workaround comment suggests Perry currently violates it for a specific lowering path.
Repro shape (suggested — needs verification)
function bare() { /* no return */ }
const r = bare();
console.log(r === undefined); // node: true
console.log(typeof r); // node: "undefined"
console.log(r); // node: undefined
Worth confirming whether this reproduces standalone or only on the specific decorator-invocation lowering path that the PR #754 author hit.
Suggested fix
Audit Perry's function-expression epilogue lowering in crates/perry-codegen/ (probably the Stmt::Return insertion when the function body falls off the end without an explicit return) and ensure the implicit return slot is the NaN-boxed TAG_UNDEFINED value (0x7FFC_0000_0000_0001) rather than whatever numeric sentinel is currently leaking through.
Once fixed:
- Tighten the
append_decorator_invocations_inner check from typeof ret === "function" to ret !== undefined so non-function class-decorator returns also throw.
- Remove the workaround comment.
Related
Background
While addressing PR #754 review item 2b (throw on class decorator that returns a replacement class), the natural check would have been:
The PR author had to fall back to:
…with this inline comment in
crates/perry-hir/src/lower.rs:So bare
@Injectable(function expression with noreturn) returns some non-undefined value — a numeric sentinel — instead of the NaN-boxedundefinedthat strict JS semantics require.Why this matters
typeof === "function"case. A decorator that returns a primitive or plain object would currently be silently discarded; once this is fixed, the decorator check can be tightened to the cleaner!== undefinedshape and catch all replacement-attempt patterns.if (someFn() !== undefined)against a function with no explicit return is silently wrong. That's a fundamental JS semantic and the workaround comment suggests Perry currently violates it for a specific lowering path.Repro shape (suggested — needs verification)
Worth confirming whether this reproduces standalone or only on the specific decorator-invocation lowering path that the PR #754 author hit.
Suggested fix
Audit Perry's function-expression epilogue lowering in
crates/perry-codegen/(probably theStmt::Returninsertion when the function body falls off the end without an explicit return) and ensure the implicit return slot is the NaN-boxedTAG_UNDEFINEDvalue (0x7FFC_0000_0000_0001) rather than whatever numeric sentinel is currently leaking through.Once fixed:
append_decorator_invocations_innercheck fromtypeof ret === "function"toret !== undefinedso non-function class-decorator returns also throw.Related
crates/perry-hir/src/lower.rs—append_decorator_invocations_inner, with the explanatory comment