Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions crates/perry-codegen/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3256,21 +3256,22 @@ fn compile_function(
stmt::lower_stmts(&mut ctx, &f.body)
.with_context(|| format!("lowering body of '{}'", f.name))?;

// Defensive: a well-typed numeric function always returns via an
// explicit `return`, but we emit `ret double 0.0` as a fallback so
// the LLVM verifier doesn't reject a missing terminator. For
// async functions, the fallback also wraps in a resolved promise
// so callers can await the result.
// A function that falls off the end without an explicit `return`
// returns `undefined` in JS — emit the NaN-boxed TAG_UNDEFINED
// value so the LLVM verifier has a terminator AND user code that
// does `f() === undefined` / `f() !== undefined` observes the
// correct value. For async functions, wrap undefined in a
// resolved promise so callers can await the result.
if !ctx.block().is_terminated() {
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
if f.is_async {
let zero = "0.0".to_string();
let handle = ctx
.block()
.call(I64, "js_promise_resolved", &[(DOUBLE, &zero)]);
.call(I64, "js_promise_resolved", &[(DOUBLE, &undef)]);
let boxed = crate::expr::nanbox_pointer_inline_pub(ctx.block(), &handle);
ctx.block().ret(DOUBLE, &boxed);
} else {
ctx.block().ret(DOUBLE, "0.0");
ctx.block().ret(DOUBLE, &undef);
}
}
let ic_globals = std::mem::take(&mut ctx.ic_globals);
Expand Down Expand Up @@ -3598,15 +3599,15 @@ fn compile_closure(
.with_context(|| format!("lowering closure body func_id={}", func_id))?;

if !ctx.block().is_terminated() {
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
if is_async {
let zero = "0.0".to_string();
let handle = ctx
.block()
.call(I64, "js_promise_resolved", &[(DOUBLE, &zero)]);
.call(I64, "js_promise_resolved", &[(DOUBLE, &undef)]);
let boxed = crate::expr::nanbox_pointer_inline_pub(ctx.block(), &handle);
ctx.block().ret(DOUBLE, &boxed);
} else {
ctx.block().ret(DOUBLE, "0.0");
ctx.block().ret(DOUBLE, &undef);
}
}
let ic_globals = std::mem::take(&mut ctx.ic_globals);
Expand Down Expand Up @@ -3898,9 +3899,12 @@ fn compile_method(
stmt::lower_stmts(&mut ctx, &method.body).with_context(|| {
format!("lowering body of method '{}::{}'", class.name, method.name)
})?;
// Fall through to the default ret-void at end.
// Fall through to the default ret at end.
if !ctx.block().is_terminated() {
ctx.block().ret(DOUBLE, "0.0");
let undef = crate::nanbox::double_literal(f64::from_bits(
crate::nanbox::TAG_UNDEFINED,
));
ctx.block().ret(DOUBLE, &undef);
}
let _ = std::mem::take(&mut ctx.ic_globals);
let _ = std::mem::take(&mut ctx.typed_parse_rodata);
Expand Down Expand Up @@ -3978,7 +3982,8 @@ fn compile_method(
.with_context(|| format!("lowering body of method '{}::{}'", class.name, method.name))?;

if !ctx.block().is_terminated() {
ctx.block().ret(DOUBLE, "0.0");
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
ctx.block().ret(DOUBLE, &undef);
}
let ic_globals = std::mem::take(&mut ctx.ic_globals);
let typed_parse_rodata = std::mem::take(&mut ctx.typed_parse_rodata);
Expand Down Expand Up @@ -5389,15 +5394,15 @@ fn compile_static_method(
.with_context(|| format!("lowering body of static '{}::{}'", class_name, f.name))?;

if !ctx.block().is_terminated() {
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
if f.is_async {
let zero = "0.0".to_string();
let handle = ctx
.block()
.call(I64, "js_promise_resolved", &[(DOUBLE, &zero)]);
.call(I64, "js_promise_resolved", &[(DOUBLE, &undef)]);
let boxed = crate::expr::nanbox_pointer_inline_pub(ctx.block(), &handle);
ctx.block().ret(DOUBLE, &boxed);
} else {
ctx.block().ret(DOUBLE, "0.0");
ctx.block().ret(DOUBLE, &undef);
}
}
let ic_globals = std::mem::take(&mut ctx.ic_globals);
Expand Down
15 changes: 10 additions & 5 deletions crates/perry-codegen/src/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,17 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> {
Ok(())
}
Stmt::Return(None) => {
// Bare `return;` returns undefined (encoded as 0.0). For
// async functions, wrap undefined in a resolved promise.
// Bare `return;` returns the NaN-boxed `undefined` value
// (TAG_UNDEFINED). For async functions, wrap it in a
// resolved promise.
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
if ctx.is_async_fn {
let zero = "0.0".to_string();
let blk = ctx.block();
let handle = blk.call(crate::types::I64, "js_promise_resolved", &[(DOUBLE, &zero)]);
let handle = blk.call(
crate::types::I64,
"js_promise_resolved",
&[(DOUBLE, &undef)],
);
let boxed = crate::expr::nanbox_pointer_inline_pub(blk, &handle);
// Pop open try frames first (see above).
for _ in 0..ctx.try_depth {
Expand All @@ -123,7 +128,7 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> {
for _ in 0..ctx.try_depth {
ctx.block().call_void("js_try_end", &[]);
}
ctx.block().ret(DOUBLE, "0.0");
ctx.block().ret(DOUBLE, &undef);
}
Ok(())
}
Expand Down
21 changes: 5 additions & 16 deletions crates/perry-hir/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3894,27 +3894,16 @@ fn append_decorator_invocations_inner(
init: Some(call),
});
let msg = format!(
"Class decorator `@{dec_name}` on `{class_name}` returned a replacement \
class. Perry does not install class replacements from decorators (see \
"Class decorator `@{dec_name}` on `{class_name}` returned a value. \
Perry does not install decorator return values as class replacements (see \
docs/src/language/decorators.md). Return `undefined` (or nothing) to keep \
the decorator running for side effects only."
);
// 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. The
// semantic check the maintainer asked for is "did the
// decorator return a class?" — a class is `typeof
// "function"` in JS, so this catches the @Memoize /
// @Throttle / GraphQL-wrapper case while leaving the bare
// `@Injectable` (no return) shape alone.
out.push(Stmt::If {
condition: Expr::Compare {
op: CompareOp::Eq,
left: Box::new(Expr::TypeOf(Box::new(Expr::LocalGet(ret_id)))),
right: Box::new(Expr::String("function".to_string())),
op: CompareOp::Ne,
left: Box::new(Expr::LocalGet(ret_id)),
right: Box::new(Expr::Undefined),
},
// Perry has dedicated HIR variants for built-in errors
// (`Expr::TypeErrorNew`, etc.); the generic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
TypeError: Class decorator `@ReplaceWithOther` on `Original` returned a replacement class. Perry does not install class replacements from decorators (see docs/src/language/decorators.md). Return `undefined` (or nothing) to keep the decorator running for side effects only.
TypeError: Class decorator `@ReplaceWithOther` on `Original` returned a value. Perry does not install decorator return values as class replacements (see docs/src/language/decorators.md). Return `undefined` (or nothing) to keep the decorator running for side effects only.
at <anonymous>
Loading