diff --git a/crates/perry-codegen/src/codegen.rs b/crates/perry-codegen/src/codegen.rs index 0328f945..8b437ddb 100644 --- a/crates/perry-codegen/src/codegen.rs +++ b/crates/perry-codegen/src/codegen.rs @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/crates/perry-codegen/src/stmt.rs b/crates/perry-codegen/src/stmt.rs index 7a79bdde..e047147a 100644 --- a/crates/perry-codegen/src/stmt.rs +++ b/crates/perry-codegen/src/stmt.rs @@ -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 { @@ -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(()) } diff --git a/crates/perry-hir/src/lower.rs b/crates/perry-hir/src/lower.rs index 4b023f47..b2b56a8d 100644 --- a/crates/perry-hir/src/lower.rs +++ b/crates/perry-hir/src/lower.rs @@ -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 diff --git a/test-parity/expected/test_decorators_replacement_unsupported.txt b/test-parity/expected/test_decorators_replacement_unsupported.txt index a5e4fd92..5abdcd4f 100644 --- a/test-parity/expected/test_decorators_replacement_unsupported.txt +++ b/test-parity/expected/test_decorators_replacement_unsupported.txt @@ -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