Skip to content
Closed
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
13 changes: 13 additions & 0 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@ pub(super) fn init_static_fields_early(
let mut cur: Option<String> = c.extends_name.clone();
let mut extends_error = false;
let mut extends_data_view = false;
let mut extends_typed_array = false;
let mut depth = 0usize;
while let Some(name) = cur {
if matches!(
Expand All @@ -1201,6 +1202,10 @@ pub(super) fn init_static_fields_early(
extends_data_view = true;
break;
}
if crate::type_analysis::is_typed_array_class(&name) {
extends_typed_array = true;
break;
}
// Walk user-defined ancestor chain.
if let Some(parent) = ctx.classes.get(&name) {
cur = parent.extends_name.clone();
Expand Down Expand Up @@ -1230,6 +1235,14 @@ pub(super) fn init_static_fields_early(
);
}
}
if extends_typed_array {
if let Some(&cid) = ctx.class_ids.get(&c.name) {
ctx.block().call_void(
"js_register_class_extends_typed_array",
&[(crate::types::I32, &cid.to_string())],
);
}
}
}
// Well-known symbol class hooks: HIR lifts `static [Symbol.hasInstance]`
// and `get [Symbol.toStringTag]` to top-level functions with the
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-codegen/src/expr/computed_store_rooting_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ fn collecting_masked_window_index_declines_the_hoisted_pointer_tier() {

let collecting = masked_window_index_coercion_loop(Type::Any);
assert!(
calls(&collecting, "js_number_coerce"),
calls(&collecting, "js_dynamic_pos"),
"unary + over an any key must exercise the collecting coercion witness:\n{collecting}"
);
assert!(
Expand All @@ -819,7 +819,7 @@ fn collecting_rhs_between_masked_reads_declines_the_hoisted_pointer_tier() {

let collecting = masked_window_rhs_coercion_loop(Type::Any);
assert!(
calls(&collecting, "js_number_coerce"),
calls(&collecting, "js_dynamic_pos"),
"the any-typed RHS must exercise the user-coercion witness:\n{collecting}"
);
assert!(
Expand All @@ -842,7 +842,7 @@ fn collecting_rhs_declines_the_straight_line_masked_region() {

let collecting = masked_window_rhs_coercion_region(Type::Any);
assert!(
calls(&collecting, "js_number_coerce"),
calls(&collecting, "js_dynamic_pos"),
"the any-typed region RHS must exercise the user-coercion witness:\n{collecting}"
);
assert!(
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/unary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
if numeric {
Ok(v)
} else {
Ok(blk.call(DOUBLE, "js_number_coerce", &[(DOUBLE, &v)]))
Ok(blk.call(DOUBLE, "js_dynamic_pos", &[(DOUBLE, &v)]))
}
}
UnaryOp::Not => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ pub(crate) fn declare_core(module: &mut LlModule) {

// ========== NaN-boxing / typeof / is_* ==========
module.declare_function("js_dynamic_neg", DOUBLE, &[DOUBLE]);
module.declare_function("js_dynamic_pos", DOUBLE, &[DOUBLE]);
module.declare_function("js_dynamic_string_equals", I32, &[DOUBLE, DOUBLE]);
module.declare_function("js_is_nan", DOUBLE, &[DOUBLE]);
module.declare_function("js_jsvalue_compare", I32, &[DOUBLE, DOUBLE]);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_instanceof_noncallable_rhs", DOUBLE, &[]);
module.declare_function("js_register_class_extends_error", VOID, &[I32]);
module.declare_function("js_register_class_extends_data_view", VOID, &[I32]);
module.declare_function("js_register_class_extends_typed_array", VOID, &[I32]);
module.declare_function("js_register_class_id", VOID, &[I32]);
// #1021 NestJS: surface Perry class names to V8 so `metatype.name`
// is non-empty. Codegen emits one call per registered class id at
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-codegen/src/type_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ pub(crate) use numeric::{
pub(crate) use pod::{
add_operands_have_pod_materialization_hazard,
expr_may_return_boxed_value_from_raw_f64_fallback, expression_has_numeric_length,
is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, is_typed_array_expr,
numeric_proof_is_declared_only, pod_record_field_is_numeric,
is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, is_typed_array_class,
is_typed_array_expr, numeric_proof_is_declared_only, pod_record_field_is_numeric,
scalar_replaced_array_element_is_raw_f64, scalar_replaced_field_is_raw_f64,
scalar_replaced_field_raw_f64_store_state,
};
Expand Down
77 changes: 44 additions & 33 deletions crates/perry-hir/src/lower/const_fold_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ use super::global_eval_hoist::{
use super::lower_expr::lower_expr;
use super::LoweringContext;

mod param_early_error;
use param_early_error::fn_ctor_kind_param_early_error;

/// Lower an expression that throws a `SyntaxError` when the enclosing call
/// site is evaluated — a throwing IIFE in value position. Used when a folded
/// `new Function(...)` / `Function(...)` body is not syntactically valid JS,
Expand Down Expand Up @@ -311,37 +314,6 @@ pub(crate) fn try_const_fold_function_construct_kind(
None => (String::new(), String::new()),
};

// CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op
// `new Function("")` / `Function("")` is the canonical runtime-codegen
// feature-test (`try { new Function(""), true } catch { false }`). perry is
// ahead-of-time compiled and cannot generate code from a runtime string, so
// under CSP mode this probe must report "unavailable" — throw at
// *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked
// environment does — so probing callers (e.g. zod 4's validator JIT) take
// their non-codegen interpreter fallback. Only the trivial empty-body no-op
// is refused; real literal bodies (`return 42`, the `return this` globalThis
// polyfill) still fold, preserving spec behavior by default.
//
// The probe passes AT LEAST ONE string argument (`new Function("")`). A
// ZERO-argument `new Function()` is not a codegen request at all — it
// constructs the empty function `anonymous() {}` — so it must NEVER throw,
// even under the default CSP mode (#5835 Intl-ctor test regressed here:
// `new Function()` used purely as a constructable-with-settable-prototype
// scaffold for `Reflect.construct` began throwing). Gate the refusal on
// there actually being an argument.
if !consts.is_empty()
&& body_src.trim().is_empty()
&& crate::eval_classifier::eval_csp_probe_unavailable()
{
return synth_throwing_iife(
ctx,
"throw new TypeError(\"Function: runtime dynamic code generation is \
unavailable in this ahead-of-time compiled binary\");",
span,
)
.map(Some);
}

// Assemble the exact source text the spec's CreateDynamicFunction
// prescribes: newlines around the body and *before the closing paren*
// so a `//` comment in the params or body can't swallow a delimiter.
Expand Down Expand Up @@ -374,10 +346,49 @@ pub(crate) fn try_const_fold_function_construct_kind(
// prologue makes duplicate or `eval`/`arguments` parameter names a
// SyntaxError, and a private name (`o.#f`) outside any class body is a
// SyntaxError regardless of mode (AllPrivateIdentifiersValid).
if fn_ctor_strict_param_early_error(fn_expr) || fn_body_has_stray_private_name(fn_expr) {
if fn_ctor_kind_param_early_error(fn_expr, kind)
|| fn_ctor_strict_param_early_error(fn_expr)
|| fn_body_has_stray_private_name(fn_expr)
{
return synth_function_syntax_error(ctx, surface, span).map(Some);
}

// CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op
// `new Function("")` / `Function("")` is the canonical runtime-codegen
// feature-test (`try { new Function(""), true } catch { false }`). perry is
// ahead-of-time compiled and cannot generate code from a runtime string, so
// under CSP mode this probe must report "unavailable" — throw at
// *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked
// environment does — so probing callers (e.g. zod 4's validator JIT) take
// their non-codegen interpreter fallback. Only the trivial empty-body no-op
// is refused; real literal bodies (`return 42`, the `return this` globalThis
// polyfill) still fold, preserving spec behavior by default.
//
// Parameter/body syntax and CreateDynamicFunction early errors take
// precedence over this Perry-specific capability signal. In particular,
// `GeneratorFunction("x = yield", "")` must throw SyntaxError rather than
// being mistaken for a valid empty-body capability probe.
//
// The probe passes AT LEAST ONE string argument (`new Function("")`). A
// ZERO-argument `new Function()` is not a codegen request at all — it
// constructs the empty function `anonymous() {}` — so it must NEVER throw,
// even under the default CSP mode (#5835 Intl-ctor test regressed here:
// `new Function()` used purely as a constructable-with-settable-prototype
// scaffold for `Reflect.construct` began throwing). Gate the refusal on
// there actually being an argument.
if !consts.is_empty()
&& body_src.trim().is_empty()
&& crate::eval_classifier::eval_csp_probe_unavailable()
{
return synth_throwing_iife(
ctx,
"throw new TypeError(\"Function: runtime dynamic code generation is \
unavailable in this ahead-of-time compiled binary\");",
span,
)
.map(Some);
}

let outer_strict = ctx.current_strict;
ctx.current_strict = false;
let lowered_result = lower_fn_expr(ctx, fn_expr);
Expand Down Expand Up @@ -1352,7 +1363,7 @@ pub(crate) fn try_eval_function_call_fold(
}
// `var AsyncFunction = (async function(){}).constructor; AsyncFunction(...)`
// — a single-assignment module var recorded as a dynamic-function ctor.
if ctx.scope_depth == 0 {
if ctx.local_decl_scope_depth(id.sym.as_str()) == Some(0) {
if let Some(super::fn_ctor_env::FnCtorShape::DynCtor(kind)) =
ctx.fn_ctor_env.entries.get(id.sym.as_str()).cloned()
{
Expand Down
116 changes: 116 additions & 0 deletions crates/perry-hir/src/lower/const_fold_fn/param_early_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use swc_ecma_ast as ast;

use super::super::fn_ctor_env::DynFnCtorKind;

/// CreateDynamicFunction's parameter early errors are kind-sensitive. Inspect
/// the parsed parameter AST so keyword-looking text in comments or string
/// literals is ignored while real `yield` / `await` syntax is rejected.
pub(super) fn fn_ctor_kind_param_early_error(fn_expr: &ast::FnExpr, kind: DynFnCtorKind) -> bool {
let forbidden = match kind {
DynFnCtorKind::Generator => &["yield"][..],
DynFnCtorKind::Async => &["await"][..],
DynFnCtorKind::AsyncGenerator => &["yield", "await"][..],
DynFnCtorKind::Plain => return false,
};

fn prop_name_has(name: &ast::PropName, forbidden: &[&str]) -> bool {
matches!(name, ast::PropName::Computed(c) if expr_has(&c.expr, forbidden))
}

fn pat_has(pat: &ast::Pat, forbidden: &[&str]) -> bool {
match pat {
ast::Pat::Ident(id) => forbidden.contains(&id.id.sym.as_ref()),
ast::Pat::Array(array) => array
.elems
.iter()
.flatten()
.any(|pat| pat_has(pat, forbidden)),
ast::Pat::Object(object) => object.props.iter().any(|prop| match prop {
ast::ObjectPatProp::KeyValue(kv) => {
prop_name_has(&kv.key, forbidden) || pat_has(&kv.value, forbidden)
}
ast::ObjectPatProp::Assign(assign) => {
forbidden.contains(&assign.key.sym.as_ref())
|| assign
.value
.as_deref()
.is_some_and(|value| expr_has(value, forbidden))
}
ast::ObjectPatProp::Rest(rest) => pat_has(&rest.arg, forbidden),
}),
ast::Pat::Assign(assign) => {
pat_has(&assign.left, forbidden) || expr_has(&assign.right, forbidden)
}
ast::Pat::Rest(rest) => pat_has(&rest.arg, forbidden),
ast::Pat::Expr(expr) => expr_has(expr, forbidden),
ast::Pat::Invalid(_) => false,
}
}

fn expr_has(expr: &ast::Expr, forbidden: &[&str]) -> bool {
match expr {
ast::Expr::Ident(id) => forbidden.contains(&id.sym.as_ref()),
ast::Expr::Yield(_) => forbidden.contains(&"yield"),
ast::Expr::Await(await_expr) => {
forbidden.contains(&"await") || expr_has(&await_expr.arg, forbidden)
}
ast::Expr::Paren(paren) => expr_has(&paren.expr, forbidden),
ast::Expr::Unary(unary) => expr_has(&unary.arg, forbidden),
ast::Expr::Update(update) => expr_has(&update.arg, forbidden),
ast::Expr::Bin(binary) => {
expr_has(&binary.left, forbidden) || expr_has(&binary.right, forbidden)
}
ast::Expr::Assign(assign) => expr_has(&assign.right, forbidden),
ast::Expr::Cond(cond) => {
expr_has(&cond.test, forbidden)
|| expr_has(&cond.cons, forbidden)
|| expr_has(&cond.alt, forbidden)
}
ast::Expr::Seq(seq) => seq.exprs.iter().any(|expr| expr_has(expr, forbidden)),
ast::Expr::Member(member) => {
expr_has(&member.obj, forbidden)
|| matches!(&member.prop, ast::MemberProp::Computed(c) if expr_has(&c.expr, forbidden))
}
ast::Expr::Call(call) => {
matches!(&call.callee, ast::Callee::Expr(expr) if expr_has(expr, forbidden))
|| call.args.iter().any(|arg| expr_has(&arg.expr, forbidden))
}
ast::Expr::New(new_expr) => {
expr_has(&new_expr.callee, forbidden)
|| new_expr
.args
.as_ref()
.is_some_and(|args| args.iter().any(|arg| expr_has(&arg.expr, forbidden)))
}
ast::Expr::Array(array) => array
.elems
.iter()
.flatten()
.any(|elem| expr_has(&elem.expr, forbidden)),
ast::Expr::Object(object) => object.props.iter().any(|prop| match prop {
ast::PropOrSpread::Spread(spread) => expr_has(&spread.expr, forbidden),
ast::PropOrSpread::Prop(prop) => match prop.as_ref() {
ast::Prop::KeyValue(kv) => {
prop_name_has(&kv.key, forbidden) || expr_has(&kv.value, forbidden)
}
ast::Prop::Assign(assign) => expr_has(&assign.value, forbidden),
ast::Prop::Getter(getter) => prop_name_has(&getter.key, forbidden),
ast::Prop::Setter(setter) => prop_name_has(&setter.key, forbidden),
ast::Prop::Method(method) => prop_name_has(&method.key, forbidden),
ast::Prop::Shorthand(id) => forbidden.contains(&id.sym.as_ref()),
},
}),
ast::Expr::TsAs(ts) => expr_has(&ts.expr, forbidden),
ast::Expr::TsTypeAssertion(ts) => expr_has(&ts.expr, forbidden),
ast::Expr::TsConstAssertion(ts) => expr_has(&ts.expr, forbidden),
ast::Expr::TsNonNull(ts) => expr_has(&ts.expr, forbidden),
_ => false,
}
}

fn_expr
.function
.params
.iter()
.any(|param| pat_has(&param.pat, forbidden))
}
25 changes: 25 additions & 0 deletions crates/perry-hir/src/lower/expr_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,12 @@ pub(crate) fn lower_ident_assignment(
Ok(*value)
} else {
if ctx.current_strict {
if matches!(name.as_str(), "undefined" | "NaN" | "Infinity") {
return Ok(Expr::Sequence(vec![
*value,
throw_type_error_const_assignment(&name),
]));
}
// #5989: strict-mode assignment to an existing global
// builtin is a property write, not a ReferenceError. See
// `strict_global_assign_existing_or_throw` for the full
Expand Down Expand Up @@ -554,6 +560,25 @@ fn lower_assignment_target(
// Check if this is a static field assignment (e.g., Counter.count = 5)
if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() {
let obj_name = obj_ident.sym.to_string();
// Dynamic GeneratorFunction/AsyncGeneratorFunction results
// use a compact non-closure runtime representation, but still
// inherit Function.prototype's poisoned caller/arguments
// accessors. The local's inferred brand is authoritative here.
if let ast::MemberProp::Ident(prop_ident) = &member.prop {
let is_dynamic_generator = matches!(
ctx.lookup_local_type(&obj_name),
Some(Type::Named(name))
if matches!(name.as_str(), "GeneratorFunction" | "AsyncGeneratorFunction")
);
if is_dynamic_generator
&& matches!(prop_ident.sym.as_ref(), "caller" | "arguments")
{
return Ok(Expr::Sequence(vec![
*value,
throw_restricted_function_property_assignment(),
]));
}
}
// `f.caller = v` / `f.arguments = v` on a declared function —
// the poisoned setter-less accessor on Function.prototype
// throws (strict semantics; Perry-compiled code is strict).
Expand Down
21 changes: 16 additions & 5 deletions crates/perry-hir/src/lower/expr_call/url_date_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ pub(super) fn try_url_date_weakref_instance(
// `let u = new URL(...); u.toString()` (typed local)
if let ast::MemberProp::Ident(method_ident) = &member.prop {
let method_name = method_ident.sym.as_ref();
// `%Date.prototype%` is an ordinary object without [[DateValue]].
// Do not feed direct calls on it into the statically-specialized
// DateCell path; the reflective prototype thunk performs the
// required brand check and throws TypeError.
let receiver_is_date_prototype = matches!(
member.obj.as_ref(),
ast::Expr::Member(proto_member)
if matches!(proto_member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "Date")
&& matches!(&proto_member.prop, ast::MemberProp::Ident(id) if id.sym.as_ref() == "prototype")
);
if receiver_is_date_prototype {
return Ok(Err(args));
}
if static_receiver_class(ctx, member.obj.as_ref()) == Some("URL") {
match method_name {
"toString" => {
Expand Down Expand Up @@ -288,11 +301,9 @@ pub(super) fn try_url_date_weakref_instance(
let date_expr = lower_expr(ctx, &member.obj)?;
return Ok(Ok(Expr::DateGetUtcMilliseconds(Box::new(date_expr))));
}
// Other getters/methods
"valueOf" => {
let date_expr = lower_expr(ctx, &member.obj)?;
return Ok(Ok(Expr::DateValueOf(Box::new(date_expr))));
}
// Other getters/methods. `valueOf` deliberately remains a
// generic property call: an own replacement on a Date must
// take precedence over Date.prototype.valueOf.
"toDateString" => {
let date_expr = lower_expr(ctx, &member.obj)?;
return Ok(Ok(Expr::DateToDateString(Box::new(date_expr))));
Expand Down
Loading
Loading