diff --git a/changelog.d/7256-split-large-files-lint.md b/changelog.d/7256-split-large-files-lint.md new file mode 100644 index 0000000000..3a01c20fcb --- /dev/null +++ b/changelog.d/7256-split-large-files-lint.md @@ -0,0 +1,50 @@ +**Split the 16 files that had drifted past the 2,000-line `lint` gate.** +`scripts/check_file_size.sh` is a required step of the `lint` job and 16 tracked +Rust files were over the cap. Each was split along an existing topical seam into +sibling modules, re-exported so every existing path resolves unchanged — a pure +refactor with no renames, no signature changes, and no new allowlist entries. + +Largest cuts: `native_module/callable_exports.rs` 2596→1851 (the +`CALLABLE_EXPORT_ARITY_TABLE` static and its tests peeled into +`callable_export_arity_table.rs`), `native_module/constants.rs` 2427→1410 +(zlib/crypto/errno/signal const groups → `constants_tables.rs`), +`native_module/callable_export_check.rs` 2218→1106 (`CALLABLE_EXPORT_TABLE` → +`callable_export_table.rs`), `lower/expr_call/native_module.rs` 2046→716 (five +receiver-dispatch blocks → `native_module/`), and the two GC test files +`gc/tests/layout_trace.rs` 2152→425 and `gc/tests/runtime_roots.rs` 2035→604. + +Two hazards specific to these files shaped the seams: + +- Three `native_module` files carry oracle tests that harvest *every string + literal in their own source* via `include_str!(".rs")`, one of them + asserting a `checked > 100_000` floor. A naive split silently shrinks that + literal universe — the test keeps passing while covering less. Those sites now + `concat!(include_str!(trunk), include_str!(sibling))`, preserving the set + exactly. +- `CALLABLE_EXPORT_ARITY_TABLE` and `CALLABLE_EXPORT_TABLE` are `binary_search` + targets (order is load-bearing) and the `BUFFER_*`/`SQLITE_*`/`ASSERT_*` + slices are `for`-iterated into observable `for…in` order. Every table moved as + one intact item; none was partitioned. + +The `addr_class_inventory.py` ratchet baseline and its whole-file allowlist +entries are **path-keyed**, so a ratcheted or allowlisted site moving to a new +sibling is a hard gate failure. Both traps fired during the work; the seams were +re-picked so every such site stays in its original file +(`proto_chain_contains_real_array`, `non_array_object_receiver` and +`plain_object_value` are deliberately kept in `array/generic.rs`). The +`addr_class_inventory.py` and `gc_store_site_inventory.py` finding sets are +byte-identical to before, and no gate script or allowlist was edited. + +Verified by: `cargo check --tests` clean on all six crates with an unchanged +warning set; crate-wide Rust-token multisets conserved across 5.39M tokens with +zero non-plumbing losses; the GC test files reassembling byte-identically from +parent + children with `cargo test` reporting the same 47 and 77 passing tests; +and unchanged `pub extern "C"` symbol lists for the FFI-heavy `perry-stdlib` and +`perry-ext-net` files. + +Note that this does not by itself make `lint` green: the job stops at the first +failing step, and three earlier/later steps on `main` fail independently — +`ci_public_baseline_check.py` (both artifact fingerprints stale for 40+ +commits), `gc_store_site_inventory.py` (19 unaudited store sites) and +`addr_class_inventory.py` (2 ratchet regressions plus a stale allowlist +substring). Those are tracked separately. diff --git a/crates/perry-codegen/src/codegen/typed_abi.rs b/crates/perry-codegen/src/codegen/typed_abi.rs index 8d09fc2c71..f31123471d 100644 --- a/crates/perry-codegen/src/codegen/typed_abi.rs +++ b/crates/perry-codegen/src/codegen/typed_abi.rs @@ -10,6 +10,17 @@ use std::collections::{HashMap, HashSet}; use perry_hir::types::Type; use perry_hir::{BinaryOp, CompareOp, Expr, Function, LogicalOp, Stmt, UnaryOp}; +mod lower; +#[cfg(test)] +mod tests; + +pub(crate) use lower::{ + lower_typed_f64_body, lower_typed_f64_body_with_seed_locals_and_reps, + lower_typed_f64_receiver_body, lower_typed_i1_body, lower_typed_i1_body_with_seed_locals, + lower_typed_i32_body, lower_typed_i32_body_with_seed_locals, lower_typed_string_body, + lower_typed_string_body_with_seed_locals, +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TypedFunctionTrampolineKind { F64, @@ -1345,748 +1356,3 @@ fn expr_is_typed_string_safe(expr: &Expr, locals: &HashSet) -> bool { _ => false, } } - -fn lower_typed_f64_expr_with_env( - blk: &mut crate::block::LlBlock, - expr: &Expr, - locals: &HashMap, - reps: &HashMap, -) -> anyhow::Result { - match expr { - Expr::Number(n) => Ok(crate::nanbox::double_literal(*n)), - Expr::Integer(n) => Ok(format!("{}.0", *n)), - Expr::LocalGet(id) => { - let value = locals - .get(id) - .cloned() - .unwrap_or_else(|| format!("%arg{id}")); - if matches!(reps.get(id), Some(TypedParamRep::I32)) { - Ok(blk.sitofp(crate::types::I32, &value, crate::types::DOUBLE)) - } else { - Ok(value) - } - } - Expr::Unary { - op: UnaryOp::Pos, - operand, - } => lower_typed_f64_expr_with_env(blk, operand, locals, reps), - Expr::Unary { - op: UnaryOp::Neg, - operand, - } => { - let v = lower_typed_f64_expr_with_env(blk, operand, locals, reps)?; - Ok(blk.fneg(&v)) - } - Expr::Binary { op, left, right } => { - let l = lower_typed_f64_expr_with_env(blk, left, locals, reps)?; - let r = lower_typed_f64_expr_with_env(blk, right, locals, reps)?; - Ok(match op { - BinaryOp::Add => blk.fadd(&l, &r), - BinaryOp::Sub => blk.fsub(&l, &r), - BinaryOp::Mul => blk.fmul(&l, &r), - BinaryOp::Div => blk.fdiv(&l, &r), - BinaryOp::Mod => blk.frem(&l, &r), - _ => { - anyhow::bail!("typed-f64 clone cannot lower non-arithmetic expression") - } - }) - } - _ => anyhow::bail!( - "typed-f64 clone cannot lower expression kind {}", - crate::expr::variant_name(expr) - ), - } -} - -fn lower_typed_i32_expr_with_env( - blk: &mut crate::block::LlBlock, - expr: &Expr, - locals: &HashMap, -) -> anyhow::Result { - match expr { - Expr::Integer(n) if integer_literal_fits_i32(*n) => Ok(n.to_string()), - Expr::LocalGet(id) => Ok(locals - .get(id) - .cloned() - .unwrap_or_else(|| format!("%arg{id}"))), - Expr::Unary { - op: UnaryOp::BitNot, - operand, - } => { - let v = lower_typed_i32_expr_with_env(blk, operand, locals)?; - Ok(blk.xor(crate::types::I32, &v, "-1")) - } - Expr::Binary { op, left, right } => { - let l = lower_typed_i32_expr_with_env(blk, left, locals)?; - let r_raw = lower_typed_i32_expr_with_env(blk, right, locals)?; - let r = if matches!(op, BinaryOp::Shl | BinaryOp::Shr) { - blk.and(crate::types::I32, &r_raw, "31") - } else { - r_raw - }; - Ok(match op { - BinaryOp::BitAnd => blk.and(crate::types::I32, &l, &r), - BinaryOp::BitOr => blk.or(crate::types::I32, &l, &r), - BinaryOp::BitXor => blk.xor(crate::types::I32, &l, &r), - BinaryOp::Shl => blk.shl(crate::types::I32, &l, &r), - BinaryOp::Shr => blk.ashr(crate::types::I32, &l, &r), - _ => anyhow::bail!("typed-i32 clone cannot lower non-bitwise expression"), - }) - } - _ => anyhow::bail!( - "typed-i32 clone cannot lower expression kind {}", - crate::expr::variant_name(expr) - ), - } -} - -fn lower_typed_i1_expr_with_env( - blk: &mut crate::block::LlBlock, - expr: &Expr, - locals: &HashMap, - reps: &HashMap, -) -> anyhow::Result { - match expr { - Expr::Bool(value) => Ok(value.to_string()), - Expr::LocalGet(id) => Ok(locals - .get(id) - .cloned() - .unwrap_or_else(|| format!("%arg{id}"))), - Expr::Unary { - op: UnaryOp::Not, - operand, - } => { - let v = lower_typed_i1_expr_with_env(blk, operand, locals, reps)?; - Ok(blk.xor(crate::types::I1, &v, "true")) - } - Expr::Logical { op, left, right } => { - let l = lower_typed_i1_expr_with_env(blk, left, locals, reps)?; - let r = lower_typed_i1_expr_with_env(blk, right, locals, reps)?; - Ok(match op { - LogicalOp::And => blk.and(crate::types::I1, &l, &r), - LogicalOp::Or => blk.or(crate::types::I1, &l, &r), - LogicalOp::Coalesce => { - anyhow::bail!("typed-i1 clone cannot lower nullish coalesce") - } - }) - } - Expr::Compare { op, left, right } => { - if expr_is_typed_i1_safe(left, reps) - && expr_is_typed_i1_safe(right, reps) - && matches!( - op, - CompareOp::Eq | CompareOp::Ne | CompareOp::LooseEq | CompareOp::LooseNe - ) - { - let l = lower_typed_i1_expr_with_env(blk, left, locals, reps)?; - let r = lower_typed_i1_expr_with_env(blk, right, locals, reps)?; - return Ok(match op { - CompareOp::Eq | CompareOp::LooseEq => blk.icmp_eq(crate::types::I1, &l, &r), - CompareOp::Ne | CompareOp::LooseNe => blk.icmp_ne(crate::types::I1, &l, &r), - _ => unreachable!("guarded boolean comparison op"), - }); - } - if expr_is_typed_f64_safe(left, reps) && expr_is_typed_f64_safe(right, reps) { - if expr_is_typed_i32_safe(left, reps) && expr_is_typed_i32_safe(right, reps) { - let l = lower_typed_i32_expr_with_env(blk, left, locals)?; - let r = lower_typed_i32_expr_with_env(blk, right, locals)?; - return Ok(match op { - CompareOp::Eq | CompareOp::LooseEq => { - blk.icmp_eq(crate::types::I32, &l, &r) - } - CompareOp::Ne | CompareOp::LooseNe => { - blk.icmp_ne(crate::types::I32, &l, &r) - } - CompareOp::Lt => blk.icmp_slt(crate::types::I32, &l, &r), - CompareOp::Le => blk.icmp_sle(crate::types::I32, &l, &r), - CompareOp::Gt => blk.icmp_sgt(crate::types::I32, &l, &r), - CompareOp::Ge => blk.icmp_sge(crate::types::I32, &l, &r), - }); - } - let l = lower_typed_f64_expr_with_env(blk, left, locals, reps)?; - let r = lower_typed_f64_expr_with_env(blk, right, locals, reps)?; - let cond = match op { - CompareOp::Eq | CompareOp::LooseEq => "oeq", - CompareOp::Ne | CompareOp::LooseNe => "une", - CompareOp::Lt => "olt", - CompareOp::Le => "ole", - CompareOp::Gt => "ogt", - CompareOp::Ge => "oge", - }; - return Ok(blk.fcmp(cond, &l, &r)); - } - anyhow::bail!("typed-i1 clone cannot lower mixed comparison") - } - _ => anyhow::bail!( - "typed-i1 clone cannot lower expression kind {}", - crate::expr::variant_name(expr) - ), - } -} - -fn lower_typed_string_expr_with_env( - expr: &Expr, - locals: &HashMap, -) -> anyhow::Result { - match expr { - Expr::LocalGet(id) => Ok(locals - .get(id) - .cloned() - .unwrap_or_else(|| format!("%arg{id}"))), - _ => anyhow::bail!( - "typed-string clone cannot lower expression kind {}", - crate::expr::variant_name(expr) - ), - } -} - -pub(crate) fn lower_typed_f64_body_with_seed_locals( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], - locals: HashMap, -) -> anyhow::Result { - lower_typed_f64_body_with_seed_locals_and_reps(blk, params, body, locals, HashMap::new()) -} - -pub(crate) fn lower_typed_f64_body_with_seed_locals_and_reps( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], - mut locals: HashMap, - mut reps: HashMap, -) -> anyhow::Result { - for param in params { - locals.insert(param.id, format!("%arg{}", param.id)); - if let Some(rep) = typed_param_rep_for_type(¶m.ty) { - reps.insert(param.id, rep); - } - } - let Some((last, prefix)) = body.split_last() else { - anyhow::bail!("typed-f64 clone cannot lower empty body"); - }; - for stmt in prefix { - match stmt { - Stmt::Let { - id, - ty, - mutable: false, - init: Some(expr), - .. - } if is_f64_type(ty) => { - let value = lower_typed_f64_expr_with_env(blk, expr, &locals, &reps)?; - locals.insert(*id, value); - reps.insert(*id, TypedParamRep::F64); - } - Stmt::Let { - id, - ty: Type::Int32, - mutable: false, - init: Some(expr), - .. - } => { - let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; - locals.insert(*id, value); - reps.insert(*id, TypedParamRep::I32); - } - _ => anyhow::bail!("typed-f64 clone cannot lower non-straight-line statement"), - } - } - match last { - Stmt::Return(Some(expr)) => lower_typed_f64_expr_with_env(blk, expr, &locals, &reps), - _ => anyhow::bail!("typed-f64 clone requires a final return value"), - } -} - -pub(crate) fn lower_typed_f64_body( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], -) -> anyhow::Result { - lower_typed_f64_body_with_seed_locals(blk, params, body, HashMap::new()) -} - -pub(crate) fn lower_typed_i32_body( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], -) -> anyhow::Result { - lower_typed_i32_body_with_seed_locals(blk, params, body, HashMap::new()) -} - -pub(crate) fn lower_typed_i32_body_with_seed_locals( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], - mut locals: HashMap, -) -> anyhow::Result { - for param in params { - locals.insert(param.id, format!("%arg{}", param.id)); - } - let Some((last, prefix)) = body.split_last() else { - anyhow::bail!("typed-i32 clone cannot lower empty body"); - }; - for stmt in prefix { - match stmt { - Stmt::Let { - id, - ty: Type::Int32, - mutable: false, - init: Some(expr), - .. - } => { - let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; - locals.insert(*id, value); - } - _ => anyhow::bail!("typed-i32 clone cannot lower non-straight-line statement"), - } - } - match last { - Stmt::Return(Some(expr)) => lower_typed_i32_expr_with_env(blk, expr, &locals), - _ => anyhow::bail!("typed-i32 clone requires a final return value"), - } -} - -pub(crate) fn lower_typed_string_body_with_seed_locals( - _blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], - mut locals: HashMap, -) -> anyhow::Result { - for param in params { - locals.insert(param.id, format!("%arg{}", param.id)); - } - let Some((last, prefix)) = body.split_last() else { - anyhow::bail!("typed-string clone cannot lower empty body"); - }; - for stmt in prefix { - match stmt { - Stmt::Let { - id, - ty, - mutable: false, - init: Some(expr), - .. - } if is_string_type(ty) => { - let value = lower_typed_string_expr_with_env(expr, &locals)?; - locals.insert(*id, value); - } - _ => anyhow::bail!("typed-string clone cannot lower non-straight-line statement"), - } - } - match last { - Stmt::Return(Some(expr)) => lower_typed_string_expr_with_env(expr, &locals), - _ => anyhow::bail!("typed-string clone requires a final return value"), - } -} - -pub(crate) fn lower_typed_string_body( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], -) -> anyhow::Result { - lower_typed_string_body_with_seed_locals(blk, params, body, HashMap::new()) -} - -fn lower_typed_f64_receiver_field( - blk: &mut crate::block::LlBlock, - field_index: u32, - header_skip: u64, -) -> String { - let obj_ptr = blk.inttoptr(crate::types::I64, "%this_obj"); - let header_skip_str = header_skip.to_string(); - let fields_base = blk.gep( - crate::types::I8, - &obj_ptr, - &[(crate::types::I64, &header_skip_str)], - ); - let field_index_str = field_index.to_string(); - let field_ptr = blk.gep( - crate::types::DOUBLE, - &fields_base, - &[(crate::types::I64, &field_index_str)], - ); - blk.load(crate::types::DOUBLE, &field_ptr) -} - -fn lower_typed_f64_receiver_expr_with_env( - blk: &mut crate::block::LlBlock, - expr: &Expr, - locals: &HashMap, - receiver: &TypedReceiverMethodInfo, - header_skip: u64, -) -> anyhow::Result { - match expr { - Expr::Number(n) => Ok(crate::nanbox::double_literal(*n)), - Expr::Integer(n) => Ok(format!("{}.0", *n)), - Expr::LocalGet(id) => Ok(locals - .get(id) - .cloned() - .unwrap_or_else(|| format!("%arg{id}"))), - Expr::PropertyGet { - object, property, .. - } if matches!(object.as_ref(), Expr::This) => { - let Some(field_index) = receiver.field_index(property) else { - anyhow::bail!("typed-f64 receiver clone cannot lower unproven receiver field") - }; - Ok(lower_typed_f64_receiver_field( - blk, - field_index, - header_skip, - )) - } - Expr::Unary { - op: UnaryOp::Pos, - operand, - } => lower_typed_f64_receiver_expr_with_env(blk, operand, locals, receiver, header_skip), - Expr::Unary { - op: UnaryOp::Neg, - operand, - } => { - let v = lower_typed_f64_receiver_expr_with_env( - blk, - operand, - locals, - receiver, - header_skip, - )?; - Ok(blk.fneg(&v)) - } - Expr::Binary { op, left, right } => { - let l = - lower_typed_f64_receiver_expr_with_env(blk, left, locals, receiver, header_skip)?; - let r = - lower_typed_f64_receiver_expr_with_env(blk, right, locals, receiver, header_skip)?; - Ok(match op { - BinaryOp::Add => blk.fadd(&l, &r), - BinaryOp::Sub => blk.fsub(&l, &r), - BinaryOp::Mul => blk.fmul(&l, &r), - BinaryOp::Div => blk.fdiv(&l, &r), - BinaryOp::Mod => blk.frem(&l, &r), - _ => { - anyhow::bail!("typed-f64 receiver clone cannot lower non-arithmetic expression") - } - }) - } - _ => anyhow::bail!( - "typed-f64 receiver clone cannot lower expression kind {}", - crate::expr::variant_name(expr) - ), - } -} - -pub(crate) fn lower_typed_f64_receiver_body( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], - receiver: &TypedReceiverMethodInfo, - header_skip: u64, -) -> anyhow::Result { - let mut locals = HashMap::new(); - for param in params { - locals.insert(param.id, format!("%arg{}", param.id)); - } - let Some((last, prefix)) = body.split_last() else { - anyhow::bail!("typed-f64 receiver clone cannot lower empty body"); - }; - for stmt in prefix { - match stmt { - Stmt::Let { - id, - ty, - mutable: false, - init: Some(expr), - .. - } if is_f64_type(ty) => { - let value = lower_typed_f64_receiver_expr_with_env( - blk, - expr, - &locals, - receiver, - header_skip, - )?; - locals.insert(*id, value); - } - _ => anyhow::bail!("typed-f64 receiver clone cannot lower non-straight-line statement"), - } - } - match last { - Stmt::Return(Some(expr)) => { - lower_typed_f64_receiver_expr_with_env(blk, expr, &locals, receiver, header_skip) - } - _ => anyhow::bail!("typed-f64 receiver clone requires a final return value"), - } -} - -pub(crate) fn lower_typed_i1_body_with_seed_locals( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], - mut locals: HashMap, - mut reps: HashMap, -) -> anyhow::Result { - for param in params { - locals.insert(param.id, format!("%arg{}", param.id)); - if let Some(rep) = typed_param_rep_for_type(¶m.ty) { - reps.insert(param.id, rep); - } - } - let Some((last, prefix)) = body.split_last() else { - anyhow::bail!("typed-i1 clone cannot lower empty body"); - }; - for stmt in prefix { - match stmt { - Stmt::Let { - id, - ty: Type::Boolean, - mutable: false, - init: Some(expr), - .. - } => { - let value = lower_typed_i1_expr_with_env(blk, expr, &locals, &reps)?; - locals.insert(*id, value); - reps.insert(*id, TypedParamRep::I1); - } - Stmt::Let { - id, - ty, - mutable: false, - init: Some(expr), - .. - } if is_f64_type(ty) => { - let value = lower_typed_f64_expr_with_env(blk, expr, &locals, &reps)?; - locals.insert(*id, value); - reps.insert(*id, TypedParamRep::F64); - } - Stmt::Let { - id, - ty: Type::Int32, - mutable: false, - init: Some(expr), - .. - } => { - let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; - locals.insert(*id, value); - reps.insert(*id, TypedParamRep::I32); - } - _ => anyhow::bail!("typed-i1 clone cannot lower non-straight-line statement"), - } - } - match last { - Stmt::Return(Some(expr)) => lower_typed_i1_expr_with_env(blk, expr, &locals, &reps), - _ => anyhow::bail!("typed-i1 clone requires a final return value"), - } -} - -pub(crate) fn lower_typed_i1_body( - blk: &mut crate::block::LlBlock, - params: &[perry_hir::Param], - body: &[Stmt], -) -> anyhow::Result { - lower_typed_i1_body_with_seed_locals(blk, params, body, HashMap::new(), HashMap::new()) -} - -#[cfg(test)] -mod tests { - use super::*; - use perry_hir::{Class, Param}; - - fn param(id: u32, name: &str, ty: Type) -> Param { - Param { - id, - name: name.to_string(), - ty, - default: None, - decorators: Vec::new(), - is_rest: false, - arguments_object: None, - } - } - - fn function(return_type: Type, params: Vec, body: Vec) -> Function { - Function { - id: 1, - name: "mixed".to_string(), - type_params: Vec::new(), - params, - return_type, - body, - is_async: false, - is_generator: false, - is_strict: false, - is_exported: false, - captures: Vec::new(), - decorators: Vec::new(), - was_plain_async: false, - was_unrolled: false, - } - } - - fn ret(expr: Expr) -> Vec { - vec![Stmt::Return(Some(expr))] - } - - fn class(id: u32, name: &str, extends: Option, extends_name: Option<&str>) -> Class { - Class { - id, - name: name.to_string(), - type_params: Vec::new(), - extends, - extends_name: extends_name.map(str::to_string), - native_extends: None, - extends_expr: None, - heritage_lexically_shadowed: false, - fields: Vec::new(), - constructor: None, - methods: Vec::new(), - getters: Vec::new(), - setters: Vec::new(), - static_accessor_names: Vec::new(), - static_accessor_fn_ids: Vec::new(), - static_fields: Vec::new(), - static_methods: Vec::new(), - computed_members: Vec::new(), - decorators: Vec::new(), - is_exported: false, - aliases: Vec::new(), - is_nested: false, - alloc_width_hint: 0, - } - } - - #[test] - fn receiver_clone_rejects_name_only_imported_parent_chain() { - let parent = class(1, "Parent", None, None); - let child = class(2, "Child", None, Some("Parent")); - let classes = HashMap::from([("Parent".to_string(), &parent)]); - - assert!(matches!( - typed_receiver_chain_fields(&classes, &child), - Err(TypedCloneRejectionReason::ReceiverClassExtends) - )); - } - - #[test] - fn f64_clone_accepts_mixed_raw_params_when_return_expr_is_numeric_safe() { - let f = function( - Type::Number, - vec![ - param(10, "n", Type::Number), - param(11, "i", Type::Int32), - param(12, "flag", Type::Boolean), - ], - ret(Expr::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::LocalGet(10)), - right: Box::new(Expr::LocalGet(11)), - }), - ); - - assert_eq!(typed_f64_function_rejection_reason(&f), None); - assert_eq!( - typed_param_reps_for_params(&f.params), - Some(vec![ - TypedParamRep::F64, - TypedParamRep::I32, - TypedParamRep::I1 - ]) - ); - } - - #[test] - fn f64_clone_accepts_raw_i32_locals_before_numeric_return() { - let f = function( - Type::Number, - vec![param(10, "n", Type::Number), param(11, "i", Type::Int32)], - vec![ - Stmt::Let { - id: 12, - name: "mask".to_string(), - ty: Type::Int32, - mutable: false, - init: Some(Expr::Binary { - op: BinaryOp::BitOr, - left: Box::new(Expr::LocalGet(11)), - right: Box::new(Expr::Integer(1)), - }), - }, - Stmt::Return(Some(Expr::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::LocalGet(10)), - right: Box::new(Expr::LocalGet(12)), - })), - ], - ); - - assert_eq!(typed_f64_function_rejection_reason(&f), None); - } - - #[test] - fn f64_clone_rejects_unsafe_mixed_rep_use() { - let f = function( - Type::Number, - vec![ - param(10, "n", Type::Number), - param(11, "flag", Type::Boolean), - ], - ret(Expr::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::LocalGet(10)), - right: Box::new(Expr::LocalGet(11)), - }), - ); - - assert_eq!( - typed_f64_function_rejection_reason(&f), - Some(TypedCloneRejectionReason::ReturnExprNotTypedF64Safe) - ); - } - - #[test] - fn string_clone_accepts_mixed_params_when_only_string_rep_flows_to_return() { - let f = function( - Type::String, - vec![ - param(10, "s", Type::String), - param(11, "i", Type::Int32), - param(12, "flag", Type::Boolean), - ], - ret(Expr::LocalGet(10)), - ); - - assert_eq!(typed_string_function_rejection_reason(&f), None); - } - - #[test] - fn closure_clone_accepts_mixed_immutable_captures_for_numeric_return() { - let expr = Expr::Closure { - func_id: 7, - params: vec![param(20, "scale", Type::Number)], - return_type: Type::Number, - body: ret(Expr::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::LocalGet(20)), - right: Box::new(Expr::LocalGet(30)), - }), - captures: vec![30, 31], - mutable_captures: Vec::new(), - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: true, - is_async: false, - is_generator: false, - is_strict: false, - }; - let module_local_types = HashMap::from([(30, Type::Int32), (31, Type::Boolean)]); - - assert_eq!( - typed_f64_closure_rejection_reason_with_types(&expr, &module_local_types), - None - ); - assert_eq!( - typed_f64_closure_capture_reps(&expr, &module_local_types), - Some(vec![(30, TypedParamRep::I32), (31, TypedParamRep::I1)]) - ); - } -} diff --git a/crates/perry-codegen/src/codegen/typed_abi/lower.rs b/crates/perry-codegen/src/codegen/typed_abi/lower.rs new file mode 100644 index 0000000000..1446a528c3 --- /dev/null +++ b/crates/perry-codegen/src/codegen/typed_abi/lower.rs @@ -0,0 +1,555 @@ +//! Typed-ABI clone body lowering. +//! +//! Split out of `typed_abi.rs` to keep that file under the 2000-line cap. +//! Pure mechanical move — every item below is a verbatim copy. + +use std::collections::HashMap; + +use perry_hir::types::Type; +use perry_hir::{BinaryOp, CompareOp, Expr, LogicalOp, Stmt, UnaryOp}; + +use super::{ + expr_is_typed_f64_safe, expr_is_typed_i1_safe, expr_is_typed_i32_safe, + integer_literal_fits_i32, is_f64_type, is_string_type, typed_param_rep_for_type, TypedParamRep, + TypedReceiverMethodInfo, +}; + +fn lower_typed_f64_expr_with_env( + blk: &mut crate::block::LlBlock, + expr: &Expr, + locals: &HashMap, + reps: &HashMap, +) -> anyhow::Result { + match expr { + Expr::Number(n) => Ok(crate::nanbox::double_literal(*n)), + Expr::Integer(n) => Ok(format!("{}.0", *n)), + Expr::LocalGet(id) => { + let value = locals + .get(id) + .cloned() + .unwrap_or_else(|| format!("%arg{id}")); + if matches!(reps.get(id), Some(TypedParamRep::I32)) { + Ok(blk.sitofp(crate::types::I32, &value, crate::types::DOUBLE)) + } else { + Ok(value) + } + } + Expr::Unary { + op: UnaryOp::Pos, + operand, + } => lower_typed_f64_expr_with_env(blk, operand, locals, reps), + Expr::Unary { + op: UnaryOp::Neg, + operand, + } => { + let v = lower_typed_f64_expr_with_env(blk, operand, locals, reps)?; + Ok(blk.fneg(&v)) + } + Expr::Binary { op, left, right } => { + let l = lower_typed_f64_expr_with_env(blk, left, locals, reps)?; + let r = lower_typed_f64_expr_with_env(blk, right, locals, reps)?; + Ok(match op { + BinaryOp::Add => blk.fadd(&l, &r), + BinaryOp::Sub => blk.fsub(&l, &r), + BinaryOp::Mul => blk.fmul(&l, &r), + BinaryOp::Div => blk.fdiv(&l, &r), + BinaryOp::Mod => blk.frem(&l, &r), + _ => { + anyhow::bail!("typed-f64 clone cannot lower non-arithmetic expression") + } + }) + } + _ => anyhow::bail!( + "typed-f64 clone cannot lower expression kind {}", + crate::expr::variant_name(expr) + ), + } +} + +fn lower_typed_i32_expr_with_env( + blk: &mut crate::block::LlBlock, + expr: &Expr, + locals: &HashMap, +) -> anyhow::Result { + match expr { + Expr::Integer(n) if integer_literal_fits_i32(*n) => Ok(n.to_string()), + Expr::LocalGet(id) => Ok(locals + .get(id) + .cloned() + .unwrap_or_else(|| format!("%arg{id}"))), + Expr::Unary { + op: UnaryOp::BitNot, + operand, + } => { + let v = lower_typed_i32_expr_with_env(blk, operand, locals)?; + Ok(blk.xor(crate::types::I32, &v, "-1")) + } + Expr::Binary { op, left, right } => { + let l = lower_typed_i32_expr_with_env(blk, left, locals)?; + let r_raw = lower_typed_i32_expr_with_env(blk, right, locals)?; + let r = if matches!(op, BinaryOp::Shl | BinaryOp::Shr) { + blk.and(crate::types::I32, &r_raw, "31") + } else { + r_raw + }; + Ok(match op { + BinaryOp::BitAnd => blk.and(crate::types::I32, &l, &r), + BinaryOp::BitOr => blk.or(crate::types::I32, &l, &r), + BinaryOp::BitXor => blk.xor(crate::types::I32, &l, &r), + BinaryOp::Shl => blk.shl(crate::types::I32, &l, &r), + BinaryOp::Shr => blk.ashr(crate::types::I32, &l, &r), + _ => anyhow::bail!("typed-i32 clone cannot lower non-bitwise expression"), + }) + } + _ => anyhow::bail!( + "typed-i32 clone cannot lower expression kind {}", + crate::expr::variant_name(expr) + ), + } +} + +fn lower_typed_i1_expr_with_env( + blk: &mut crate::block::LlBlock, + expr: &Expr, + locals: &HashMap, + reps: &HashMap, +) -> anyhow::Result { + match expr { + Expr::Bool(value) => Ok(value.to_string()), + Expr::LocalGet(id) => Ok(locals + .get(id) + .cloned() + .unwrap_or_else(|| format!("%arg{id}"))), + Expr::Unary { + op: UnaryOp::Not, + operand, + } => { + let v = lower_typed_i1_expr_with_env(blk, operand, locals, reps)?; + Ok(blk.xor(crate::types::I1, &v, "true")) + } + Expr::Logical { op, left, right } => { + let l = lower_typed_i1_expr_with_env(blk, left, locals, reps)?; + let r = lower_typed_i1_expr_with_env(blk, right, locals, reps)?; + Ok(match op { + LogicalOp::And => blk.and(crate::types::I1, &l, &r), + LogicalOp::Or => blk.or(crate::types::I1, &l, &r), + LogicalOp::Coalesce => { + anyhow::bail!("typed-i1 clone cannot lower nullish coalesce") + } + }) + } + Expr::Compare { op, left, right } => { + if expr_is_typed_i1_safe(left, reps) + && expr_is_typed_i1_safe(right, reps) + && matches!( + op, + CompareOp::Eq | CompareOp::Ne | CompareOp::LooseEq | CompareOp::LooseNe + ) + { + let l = lower_typed_i1_expr_with_env(blk, left, locals, reps)?; + let r = lower_typed_i1_expr_with_env(blk, right, locals, reps)?; + return Ok(match op { + CompareOp::Eq | CompareOp::LooseEq => blk.icmp_eq(crate::types::I1, &l, &r), + CompareOp::Ne | CompareOp::LooseNe => blk.icmp_ne(crate::types::I1, &l, &r), + _ => unreachable!("guarded boolean comparison op"), + }); + } + if expr_is_typed_f64_safe(left, reps) && expr_is_typed_f64_safe(right, reps) { + if expr_is_typed_i32_safe(left, reps) && expr_is_typed_i32_safe(right, reps) { + let l = lower_typed_i32_expr_with_env(blk, left, locals)?; + let r = lower_typed_i32_expr_with_env(blk, right, locals)?; + return Ok(match op { + CompareOp::Eq | CompareOp::LooseEq => { + blk.icmp_eq(crate::types::I32, &l, &r) + } + CompareOp::Ne | CompareOp::LooseNe => { + blk.icmp_ne(crate::types::I32, &l, &r) + } + CompareOp::Lt => blk.icmp_slt(crate::types::I32, &l, &r), + CompareOp::Le => blk.icmp_sle(crate::types::I32, &l, &r), + CompareOp::Gt => blk.icmp_sgt(crate::types::I32, &l, &r), + CompareOp::Ge => blk.icmp_sge(crate::types::I32, &l, &r), + }); + } + let l = lower_typed_f64_expr_with_env(blk, left, locals, reps)?; + let r = lower_typed_f64_expr_with_env(blk, right, locals, reps)?; + let cond = match op { + CompareOp::Eq | CompareOp::LooseEq => "oeq", + CompareOp::Ne | CompareOp::LooseNe => "une", + CompareOp::Lt => "olt", + CompareOp::Le => "ole", + CompareOp::Gt => "ogt", + CompareOp::Ge => "oge", + }; + return Ok(blk.fcmp(cond, &l, &r)); + } + anyhow::bail!("typed-i1 clone cannot lower mixed comparison") + } + _ => anyhow::bail!( + "typed-i1 clone cannot lower expression kind {}", + crate::expr::variant_name(expr) + ), + } +} + +fn lower_typed_string_expr_with_env( + expr: &Expr, + locals: &HashMap, +) -> anyhow::Result { + match expr { + Expr::LocalGet(id) => Ok(locals + .get(id) + .cloned() + .unwrap_or_else(|| format!("%arg{id}"))), + _ => anyhow::bail!( + "typed-string clone cannot lower expression kind {}", + crate::expr::variant_name(expr) + ), + } +} + +pub(crate) fn lower_typed_f64_body_with_seed_locals( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + locals: HashMap, +) -> anyhow::Result { + lower_typed_f64_body_with_seed_locals_and_reps(blk, params, body, locals, HashMap::new()) +} + +pub(crate) fn lower_typed_f64_body_with_seed_locals_and_reps( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + mut locals: HashMap, + mut reps: HashMap, +) -> anyhow::Result { + for param in params { + locals.insert(param.id, format!("%arg{}", param.id)); + if let Some(rep) = typed_param_rep_for_type(¶m.ty) { + reps.insert(param.id, rep); + } + } + let Some((last, prefix)) = body.split_last() else { + anyhow::bail!("typed-f64 clone cannot lower empty body"); + }; + for stmt in prefix { + match stmt { + Stmt::Let { + id, + ty, + mutable: false, + init: Some(expr), + .. + } if is_f64_type(ty) => { + let value = lower_typed_f64_expr_with_env(blk, expr, &locals, &reps)?; + locals.insert(*id, value); + reps.insert(*id, TypedParamRep::F64); + } + Stmt::Let { + id, + ty: Type::Int32, + mutable: false, + init: Some(expr), + .. + } => { + let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; + locals.insert(*id, value); + reps.insert(*id, TypedParamRep::I32); + } + _ => anyhow::bail!("typed-f64 clone cannot lower non-straight-line statement"), + } + } + match last { + Stmt::Return(Some(expr)) => lower_typed_f64_expr_with_env(blk, expr, &locals, &reps), + _ => anyhow::bail!("typed-f64 clone requires a final return value"), + } +} + +pub(crate) fn lower_typed_f64_body( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], +) -> anyhow::Result { + lower_typed_f64_body_with_seed_locals(blk, params, body, HashMap::new()) +} + +pub(crate) fn lower_typed_i32_body( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], +) -> anyhow::Result { + lower_typed_i32_body_with_seed_locals(blk, params, body, HashMap::new()) +} + +pub(crate) fn lower_typed_i32_body_with_seed_locals( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + mut locals: HashMap, +) -> anyhow::Result { + for param in params { + locals.insert(param.id, format!("%arg{}", param.id)); + } + let Some((last, prefix)) = body.split_last() else { + anyhow::bail!("typed-i32 clone cannot lower empty body"); + }; + for stmt in prefix { + match stmt { + Stmt::Let { + id, + ty: Type::Int32, + mutable: false, + init: Some(expr), + .. + } => { + let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; + locals.insert(*id, value); + } + _ => anyhow::bail!("typed-i32 clone cannot lower non-straight-line statement"), + } + } + match last { + Stmt::Return(Some(expr)) => lower_typed_i32_expr_with_env(blk, expr, &locals), + _ => anyhow::bail!("typed-i32 clone requires a final return value"), + } +} + +pub(crate) fn lower_typed_string_body_with_seed_locals( + _blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + mut locals: HashMap, +) -> anyhow::Result { + for param in params { + locals.insert(param.id, format!("%arg{}", param.id)); + } + let Some((last, prefix)) = body.split_last() else { + anyhow::bail!("typed-string clone cannot lower empty body"); + }; + for stmt in prefix { + match stmt { + Stmt::Let { + id, + ty, + mutable: false, + init: Some(expr), + .. + } if is_string_type(ty) => { + let value = lower_typed_string_expr_with_env(expr, &locals)?; + locals.insert(*id, value); + } + _ => anyhow::bail!("typed-string clone cannot lower non-straight-line statement"), + } + } + match last { + Stmt::Return(Some(expr)) => lower_typed_string_expr_with_env(expr, &locals), + _ => anyhow::bail!("typed-string clone requires a final return value"), + } +} + +pub(crate) fn lower_typed_string_body( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], +) -> anyhow::Result { + lower_typed_string_body_with_seed_locals(blk, params, body, HashMap::new()) +} + +fn lower_typed_f64_receiver_field( + blk: &mut crate::block::LlBlock, + field_index: u32, + header_skip: u64, +) -> String { + let obj_ptr = blk.inttoptr(crate::types::I64, "%this_obj"); + let header_skip_str = header_skip.to_string(); + let fields_base = blk.gep( + crate::types::I8, + &obj_ptr, + &[(crate::types::I64, &header_skip_str)], + ); + let field_index_str = field_index.to_string(); + let field_ptr = blk.gep( + crate::types::DOUBLE, + &fields_base, + &[(crate::types::I64, &field_index_str)], + ); + blk.load(crate::types::DOUBLE, &field_ptr) +} + +fn lower_typed_f64_receiver_expr_with_env( + blk: &mut crate::block::LlBlock, + expr: &Expr, + locals: &HashMap, + receiver: &TypedReceiverMethodInfo, + header_skip: u64, +) -> anyhow::Result { + match expr { + Expr::Number(n) => Ok(crate::nanbox::double_literal(*n)), + Expr::Integer(n) => Ok(format!("{}.0", *n)), + Expr::LocalGet(id) => Ok(locals + .get(id) + .cloned() + .unwrap_or_else(|| format!("%arg{id}"))), + Expr::PropertyGet { + object, property, .. + } if matches!(object.as_ref(), Expr::This) => { + let Some(field_index) = receiver.field_index(property) else { + anyhow::bail!("typed-f64 receiver clone cannot lower unproven receiver field") + }; + Ok(lower_typed_f64_receiver_field( + blk, + field_index, + header_skip, + )) + } + Expr::Unary { + op: UnaryOp::Pos, + operand, + } => lower_typed_f64_receiver_expr_with_env(blk, operand, locals, receiver, header_skip), + Expr::Unary { + op: UnaryOp::Neg, + operand, + } => { + let v = lower_typed_f64_receiver_expr_with_env( + blk, + operand, + locals, + receiver, + header_skip, + )?; + Ok(blk.fneg(&v)) + } + Expr::Binary { op, left, right } => { + let l = + lower_typed_f64_receiver_expr_with_env(blk, left, locals, receiver, header_skip)?; + let r = + lower_typed_f64_receiver_expr_with_env(blk, right, locals, receiver, header_skip)?; + Ok(match op { + BinaryOp::Add => blk.fadd(&l, &r), + BinaryOp::Sub => blk.fsub(&l, &r), + BinaryOp::Mul => blk.fmul(&l, &r), + BinaryOp::Div => blk.fdiv(&l, &r), + BinaryOp::Mod => blk.frem(&l, &r), + _ => { + anyhow::bail!("typed-f64 receiver clone cannot lower non-arithmetic expression") + } + }) + } + _ => anyhow::bail!( + "typed-f64 receiver clone cannot lower expression kind {}", + crate::expr::variant_name(expr) + ), + } +} + +pub(crate) fn lower_typed_f64_receiver_body( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + receiver: &TypedReceiverMethodInfo, + header_skip: u64, +) -> anyhow::Result { + let mut locals = HashMap::new(); + for param in params { + locals.insert(param.id, format!("%arg{}", param.id)); + } + let Some((last, prefix)) = body.split_last() else { + anyhow::bail!("typed-f64 receiver clone cannot lower empty body"); + }; + for stmt in prefix { + match stmt { + Stmt::Let { + id, + ty, + mutable: false, + init: Some(expr), + .. + } if is_f64_type(ty) => { + let value = lower_typed_f64_receiver_expr_with_env( + blk, + expr, + &locals, + receiver, + header_skip, + )?; + locals.insert(*id, value); + } + _ => anyhow::bail!("typed-f64 receiver clone cannot lower non-straight-line statement"), + } + } + match last { + Stmt::Return(Some(expr)) => { + lower_typed_f64_receiver_expr_with_env(blk, expr, &locals, receiver, header_skip) + } + _ => anyhow::bail!("typed-f64 receiver clone requires a final return value"), + } +} + +pub(crate) fn lower_typed_i1_body_with_seed_locals( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + mut locals: HashMap, + mut reps: HashMap, +) -> anyhow::Result { + for param in params { + locals.insert(param.id, format!("%arg{}", param.id)); + if let Some(rep) = typed_param_rep_for_type(¶m.ty) { + reps.insert(param.id, rep); + } + } + let Some((last, prefix)) = body.split_last() else { + anyhow::bail!("typed-i1 clone cannot lower empty body"); + }; + for stmt in prefix { + match stmt { + Stmt::Let { + id, + ty: Type::Boolean, + mutable: false, + init: Some(expr), + .. + } => { + let value = lower_typed_i1_expr_with_env(blk, expr, &locals, &reps)?; + locals.insert(*id, value); + reps.insert(*id, TypedParamRep::I1); + } + Stmt::Let { + id, + ty, + mutable: false, + init: Some(expr), + .. + } if is_f64_type(ty) => { + let value = lower_typed_f64_expr_with_env(blk, expr, &locals, &reps)?; + locals.insert(*id, value); + reps.insert(*id, TypedParamRep::F64); + } + Stmt::Let { + id, + ty: Type::Int32, + mutable: false, + init: Some(expr), + .. + } => { + let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; + locals.insert(*id, value); + reps.insert(*id, TypedParamRep::I32); + } + _ => anyhow::bail!("typed-i1 clone cannot lower non-straight-line statement"), + } + } + match last { + Stmt::Return(Some(expr)) => lower_typed_i1_expr_with_env(blk, expr, &locals, &reps), + _ => anyhow::bail!("typed-i1 clone requires a final return value"), + } +} + +pub(crate) fn lower_typed_i1_body( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], +) -> anyhow::Result { + lower_typed_i1_body_with_seed_locals(blk, params, body, HashMap::new(), HashMap::new()) +} diff --git a/crates/perry-codegen/src/codegen/typed_abi/tests.rs b/crates/perry-codegen/src/codegen/typed_abi/tests.rs new file mode 100644 index 0000000000..9bc1138151 --- /dev/null +++ b/crates/perry-codegen/src/codegen/typed_abi/tests.rs @@ -0,0 +1,201 @@ +use super::*; +use perry_hir::{Class, Param}; + +fn param(id: u32, name: &str, ty: Type) -> Param { + Param { + id, + name: name.to_string(), + ty, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + } +} + +fn function(return_type: Type, params: Vec, body: Vec) -> Function { + Function { + id: 1, + name: "mixed".to_string(), + type_params: Vec::new(), + params, + return_type, + body, + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + } +} + +fn ret(expr: Expr) -> Vec { + vec![Stmt::Return(Some(expr))] +} + +fn class(id: u32, name: &str, extends: Option, extends_name: Option<&str>) -> Class { + Class { + id, + name: name.to_string(), + type_params: Vec::new(), + extends, + extends_name: extends_name.map(str::to_string), + native_extends: None, + extends_expr: None, + heritage_lexically_shadowed: false, + fields: Vec::new(), + constructor: None, + methods: Vec::new(), + getters: Vec::new(), + setters: Vec::new(), + static_accessor_names: Vec::new(), + static_accessor_fn_ids: Vec::new(), + static_fields: Vec::new(), + static_methods: Vec::new(), + computed_members: Vec::new(), + decorators: Vec::new(), + is_exported: false, + aliases: Vec::new(), + is_nested: false, + alloc_width_hint: 0, + } +} + +#[test] +fn receiver_clone_rejects_name_only_imported_parent_chain() { + let parent = class(1, "Parent", None, None); + let child = class(2, "Child", None, Some("Parent")); + let classes = HashMap::from([("Parent".to_string(), &parent)]); + + assert!(matches!( + typed_receiver_chain_fields(&classes, &child), + Err(TypedCloneRejectionReason::ReceiverClassExtends) + )); +} + +#[test] +fn f64_clone_accepts_mixed_raw_params_when_return_expr_is_numeric_safe() { + let f = function( + Type::Number, + vec![ + param(10, "n", Type::Number), + param(11, "i", Type::Int32), + param(12, "flag", Type::Boolean), + ], + ret(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(10)), + right: Box::new(Expr::LocalGet(11)), + }), + ); + + assert_eq!(typed_f64_function_rejection_reason(&f), None); + assert_eq!( + typed_param_reps_for_params(&f.params), + Some(vec![ + TypedParamRep::F64, + TypedParamRep::I32, + TypedParamRep::I1 + ]) + ); +} + +#[test] +fn f64_clone_accepts_raw_i32_locals_before_numeric_return() { + let f = function( + Type::Number, + vec![param(10, "n", Type::Number), param(11, "i", Type::Int32)], + vec![ + Stmt::Let { + id: 12, + name: "mask".to_string(), + ty: Type::Int32, + mutable: false, + init: Some(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(Expr::LocalGet(11)), + right: Box::new(Expr::Integer(1)), + }), + }, + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(10)), + right: Box::new(Expr::LocalGet(12)), + })), + ], + ); + + assert_eq!(typed_f64_function_rejection_reason(&f), None); +} + +#[test] +fn f64_clone_rejects_unsafe_mixed_rep_use() { + let f = function( + Type::Number, + vec![ + param(10, "n", Type::Number), + param(11, "flag", Type::Boolean), + ], + ret(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(10)), + right: Box::new(Expr::LocalGet(11)), + }), + ); + + assert_eq!( + typed_f64_function_rejection_reason(&f), + Some(TypedCloneRejectionReason::ReturnExprNotTypedF64Safe) + ); +} + +#[test] +fn string_clone_accepts_mixed_params_when_only_string_rep_flows_to_return() { + let f = function( + Type::String, + vec![ + param(10, "s", Type::String), + param(11, "i", Type::Int32), + param(12, "flag", Type::Boolean), + ], + ret(Expr::LocalGet(10)), + ); + + assert_eq!(typed_string_function_rejection_reason(&f), None); +} + +#[test] +fn closure_clone_accepts_mixed_immutable_captures_for_numeric_return() { + let expr = Expr::Closure { + func_id: 7, + params: vec![param(20, "scale", Type::Number)], + return_type: Type::Number, + body: ret(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(20)), + right: Box::new(Expr::LocalGet(30)), + }), + captures: vec![30, 31], + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: true, + is_async: false, + is_generator: false, + is_strict: false, + }; + let module_local_types = HashMap::from([(30, Type::Int32), (31, Type::Boolean)]); + + assert_eq!( + typed_f64_closure_rejection_reason_with_types(&expr, &module_local_types), + None + ); + assert_eq!( + typed_f64_closure_capture_reps(&expr, &module_local_types), + Some(vec![(30, TypedParamRep::I32), (31, TypedParamRep::I1)]) + ); +} diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 7f2ba03be3..d478d9028a 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -13,7 +13,7 @@ use crate::native_value::{ BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, SemanticKind, }; use crate::type_analysis::{is_array_expr, is_numeric_expr, is_string_expr, receiver_class_name}; -use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8}; +use crate::types::{DOUBLE, I1, I16, I32, I64, I8}; use super::{ array_kind_fact, buffer_access_materialization_reason, emit_typed_feedback_register_site, @@ -24,6 +24,14 @@ use super::{ TypedFeedbackKind, }; +mod guarded_array; +mod inline_dyn_typed_array; + +use guarded_array::{ + lower_guarded_array_index_get, lower_packed_f64_loop_index_get, packed_f64_loop_fact, +}; +use inline_dyn_typed_array::lower_inline_dyn_typed_array_get; + fn is_width_tracked_typed_array_receiver(ctx: &FnCtx<'_>, object: &Expr) -> bool { matches!( receiver_class_name(ctx, object).as_deref(), @@ -326,791 +334,6 @@ fn receiver_repair_slot(ctx: &FnCtx<'_>, object: &Expr) -> Option { ctx.locals.get(id).cloned() } -fn lower_guarded_array_index_get( - ctx: &mut FnCtx<'_>, - arr_box: &str, - idx_i32: &str, - block_prefix: &str, - require_numeric_layout: bool, - coerce_numeric_fallback: bool, - receiver_slot: Option<&str>, -) -> Result { - let contract = if require_numeric_layout { - TypedFeedbackContract::numeric_array_get_index() - } else { - TypedFeedbackContract::array_get_index() - }; - let feedback_site_id = emit_typed_feedback_register_site( - ctx, - TypedFeedbackKind::ArrayElement, - "array[index]", - contract, - ); - let fast_idx = ctx.new_block(&format!("{}.fast", block_prefix)); - let fallback_idx = ctx.new_block(&format!("{}.fallback", block_prefix)); - let merge_idx = ctx.new_block(&format!("{}.merge", block_prefix)); - let fast_label = ctx.block_label(fast_idx); - let fallback_label = ctx.block_label(fallback_idx); - let merge_label = ctx.block_label(merge_idx); - - if !typed_feedback_emission_enabled() { - // Normal builds do not collect feedback. Inline the plain-array - // structural guard instead of paying an out-of-line call merely to - // rediscover the same header facts before the direct slot load below. - // Prototype-chain invalidators are summarized by one sticky runtime - // byte; per-array descriptors and forwarding remain receiver-local. - // - // Repsel 4a.1: the NUMERIC tier gets the same inline guard — plus an - // `_reserved & GC_ARRAY_RAW_F64_LAYOUT (0x80)` dense-proof test on the - // header word the plain guard already loads. A dense-flagged array - // needs no runtime call at all (the raw-f64 slot IS the value, no - // hole select). Arrays not yet flagged take a COLD out-of-line - // `js_typed_feedback_numeric_array_index_get_guard` call, whose - // first-touch path verifies-and-rewrites the layout (setting the - // flag), so the steady state is the inline tier. This ends the - // typed-`number[]`-slower-than-untyped inversion for reads. - let deref_idx = ctx.new_block(&format!("{}.guard.deref", block_prefix)); - let deref_label = ctx.block_label(deref_idx); - let cold_guard_idx = if require_numeric_layout { - Some(ctx.new_block(&format!("{}.guard.cold", block_prefix))) - } else { - None - }; - let guard_fail_label = match cold_guard_idx { - Some(idx) => ctx.block_label(idx), - None => fallback_label.clone(), - }; - { - let blk = ctx.block(); - let arr_bits = blk.bitcast_double_to_i64(arr_box); - let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); - let tag = blk.lshr(I64, &arr_bits, "48"); - let is_pointer = blk.icmp_eq(I64, &tag, "32765"); // POINTER_TAG - let above_handle_band = blk.icmp_ugt(I64, &arr_handle, "1048575"); - let heap_candidate = blk.and(I1, &is_pointer, &above_handle_band); - blk.cond_br(&heap_candidate, &deref_label, &guard_fail_label); - } - - ctx.current_block = deref_idx; - { - let blk = ctx.block(); - let arr_bits = blk.bitcast_double_to_i64(arr_box); - let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); - - let gc_type_addr = blk.sub(I64, &arr_handle, "8"); - let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); - let gc_type = blk.load(I8, &gc_type_ptr); - let is_array = blk.icmp_eq(I8, &gc_type, "1"); // GC_TYPE_ARRAY - - let gc_flags_addr = blk.sub(I64, &arr_handle, "7"); - let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr); - let gc_flags = blk.load(I8, &gc_flags_ptr); - let forwarded_bits = blk.and(I8, &gc_flags, "128"); - let not_forwarded = blk.icmp_eq(I8, &forwarded_bits, "0"); - - let reserved_addr = blk.sub(I64, &arr_handle, "6"); - let reserved_ptr = blk.inttoptr(I64, &reserved_addr); - let reserved = blk.load(I16, &reserved_ptr); - let descriptor_bits = blk.and(I16, &reserved, "1024"); - let no_descriptors = blk.icmp_eq(I16, &descriptor_bits, "0"); - - let invalidated = blk.load_volatile(I8, "@PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED"); - let default_prototype_chain = blk.icmp_eq(I8, &invalidated, "0"); - - let arr_ptr = blk.inttoptr(I64, &arr_handle); - let length = blk.load(I32, &arr_ptr); - let capacity_ptr = blk.gep(I8, &arr_ptr, &[(I64, "4")]); - let capacity = blk.load(I32, &capacity_ptr); - let index_nonnegative = blk.icmp_slt(I32, idx_i32, "0"); - let index_nonnegative = blk.icmp_eq(I1, &index_nonnegative, "false"); - let index_in_bounds = blk.icmp_ult(I32, idx_i32, &length); - let length_sane = blk.icmp_ule(I32, &length, "16000000"); - let capacity_sane = blk.icmp_ule(I32, &capacity, "16000000"); - let length_within_capacity = blk.icmp_ule(I32, &length, &capacity); - - let mut guard_ok = blk.and(I1, &is_array, ¬_forwarded); - guard_ok = blk.and(I1, &guard_ok, &no_descriptors); - guard_ok = blk.and(I1, &guard_ok, &default_prototype_chain); - guard_ok = blk.and(I1, &guard_ok, &index_nonnegative); - guard_ok = blk.and(I1, &guard_ok, &index_in_bounds); - guard_ok = blk.and(I1, &guard_ok, &length_sane); - guard_ok = blk.and(I1, &guard_ok, &capacity_sane); - guard_ok = blk.and(I1, &guard_ok, &length_within_capacity); - if require_numeric_layout { - // Dense raw-f64 proof: every slot in [0, length) holds - // canonical raw f64 bits (GC_ARRAY_RAW_F64_LAYOUT, 0x80). - // - // Repsel 4a.2 (#6904): a NUMBER-CONTEXT read (the caller will - // ToNumber the element regardless — `coerce_numeric_fallback`) - // additionally accepts the hole-tolerant invariant - // (GC_ARRAY_RAW_F64_HOLES, 0x1000): every slot is canonical - // raw f64 OR TAG_HOLE, and the fast arm canonicalizes any NaN - // payload (TAG_HOLE included) to the quiet NaN — bit-exact - // with ToNumber(undefined) for a hole and with ToNumber(NaN) - // for a stored NaN. This is the `new Array(n)` mid-fill axis: - // such arrays are provably-not-dense until the last slot is - // written, so the dense-only tier never fired for them. - let raw_mask = if coerce_numeric_fallback { - "4224" // 0x1080 = RAW_F64_LAYOUT | RAW_F64_HOLES - } else { - "128" // dense only: the raw slot is exposed verbatim - }; - let raw_bits = blk.and(I16, &reserved, raw_mask); - let is_raw = blk.icmp_ne(I16, &raw_bits, "0"); - guard_ok = blk.and(I1, &guard_ok, &is_raw); - } - blk.cond_br(&guard_ok, &fast_label, &guard_fail_label); - } - - if let Some(cold_idx) = cold_guard_idx { - // Cold arm: the out-of-line guard rebuilds unmarked-but-numeric - // arrays into raw-f64 layout (then this call site goes inline on - // every later read); everything else routes to the boxed fallback. - ctx.current_block = cold_idx; - // Self-heal a stale growth-forwarded binding first (see - // `receiver_repair_slot`): follow the chain, write the live head - // back to the local slot. This iteration still takes the guard - // on the ORIGINAL value (a forwarded head fails it → boxed - // fallback, which follows the chain — correct either way); every - // later iteration re-loads the repaired slot and goes inline. - if let Some(slot) = receiver_slot { - let blk = ctx.block(); - let fresh = blk.call(DOUBLE, "js_array_refresh_local_head", &[(DOUBLE, arr_box)]); - blk.store(DOUBLE, &fresh, slot); - } - let guard_ok = { - let blk = ctx.block(); - let guard_i32 = blk.call( - I32, - "js_typed_feedback_numeric_array_index_get_guard", - &[ - (I64, &feedback_site_id), - (DOUBLE, arr_box), - (I32, idx_i32), - (I32, "1"), - ], - ); - blk.icmp_ne(I32, &guard_i32, "0") - }; - ctx.block().cond_br(&guard_ok, &fast_label, &fallback_label); - } - } else { - let guard_ok = { - let blk = ctx.block(); - let guard_fn = if require_numeric_layout { - "js_typed_feedback_numeric_array_index_get_guard" - } else { - "js_typed_feedback_plain_array_index_get_guard" - }; - let guard_i32 = blk.call( - I32, - guard_fn, - &[ - (I64, &feedback_site_id), - (DOUBLE, arr_box), - (I32, idx_i32), - (I32, "1"), - ], - ); - blk.icmp_ne(I32, &guard_i32, "0") - }; - ctx.block().cond_br(&guard_ok, &fast_label, &fallback_label); - } - - ctx.current_block = fallback_idx; - // Materialize the f64 index only here (cold path) so the int→fp conversion - // stays out of the numeric loop's hot region. - let idx_box = ctx.block().sitofp(I32, idx_i32, DOUBLE); - let fallback_boxed = ctx.block().call( - DOUBLE, - "js_typed_feedback_array_index_get_fallback_boxed", - &[ - (I64, &feedback_site_id), - (DOUBLE, arr_box), - (DOUBLE, &idx_box), - ], - ); - let fallback_val = if require_numeric_layout && coerce_numeric_fallback { - ctx.block() - .call(DOUBLE, "js_number_coerce", &[(DOUBLE, &fallback_boxed)]) - } else { - fallback_boxed.clone() - }; - let fallback_end_label = ctx.block().label.clone(); - ctx.block().br(&merge_label); - if require_numeric_layout { - let fallback = LoweredValue::js_value(fallback_boxed.clone()); - ctx.record_lowered_value_with_access_mode_and_facts( - "NumericArrayIndexGet", - None, - "js_typed_feedback_array_index_get_fallback_boxed", - &fallback, - Some(BoundsState::Unknown), - None, - Some(BufferAccessMode::DynamicFallback), - Some(MaterializationReason::RuntimeApi), - None, - None, - Vec::new(), - vec![ - raw_f64_layout_fact( - None, - "rejected", - "numeric_array_index_get_guard", - Some(MaterializationReason::RuntimeApi), - ), - raw_f64_layout_fact( - None, - "invalidated", - "runtime_api", - Some(MaterializationReason::RuntimeApi), - ), - ], - false, - false, - Vec::new(), - ); - } - - ctx.current_block = fast_idx; - let fast_blk = ctx.block(); - let arr_bits = fast_blk.bitcast_double_to_i64(arr_box); - let arr_handle = fast_blk.and(I64, &arr_bits, POINTER_MASK_I64); - let fast_val = if require_numeric_layout { - // The guard on the way into this block (inline tier or the runtime - // `numeric_array_index_get_guard`) already proved: a plain, - // non-forwarded `Array`, in raw-f64 (or, for number-context reads, - // raw-f64-or-holes) layout, with `index` in bounds. So load the slot - // inline instead of calling `js_array_numeric_get_f64_unboxed`, - // whose hot path re-validates exactly those same conditions and then - // does this load. - let idx_i64 = fast_blk.zext(I32, idx_i32, I64); - let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); - let with_header = fast_blk.add(I64, &byte_offset, "8"); - let element_addr = fast_blk.add(I64, &arr_handle, &with_header); - let element_ptr = fast_blk.inttoptr(I64, &element_addr); - let raw = fast_blk.load(DOUBLE, &element_ptr); - if coerce_numeric_fallback { - // Repsel 4a.2: number-context canonicalization — any NaN payload - // (a TAG_HOLE slot under the raw-f64-or-holes proof, or a stored - // canonical NaN) becomes the quiet NaN. Bit-exact: - // ToNumber(undefined) = NaN for a hole, ToNumber(NaN) = NaN for a - // stored NaN, identity for every real number. PROOF-GATED: only - // sound because the guard admitted raw-f64-or-holes slots — an - // arbitrary NaN-boxed tag would be wrongly collapsed to NaN. - let is_ord = fast_blk.fcmp("ord", &raw, &raw); - fast_blk.select(I1, &is_ord, DOUBLE, &raw, "0x7FF8000000000000") - } else { - // Dense-only proof: no HOLE slots exist; the raw slot IS the - // element value, exposed verbatim. - raw - } - } else { - let idx_i64 = fast_blk.zext(I32, idx_i32, I64); - let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); - let with_header = fast_blk.add(I64, &byte_offset, "8"); - let element_addr = fast_blk.add(I64, &arr_handle, &with_header); - let element_ptr = fast_blk.inttoptr(I64, &element_addr); - let fast_raw = fast_blk.load(DOUBLE, &element_ptr); - // `new Array(n)` slots are TAG_HOLE internally; JavaScript reads expose - // `undefined`. - let fast_raw_bits = fast_blk.bitcast_double_to_i64(&fast_raw); - let is_hole = fast_blk.icmp_eq(I64, &fast_raw_bits, crate::nanbox::TAG_HOLE_I64); - let undef_d = fast_blk.bitcast_i64_to_double(crate::nanbox::TAG_UNDEFINED_I64); - fast_blk.select(I1, &is_hole, DOUBLE, &undef_d, &fast_raw) - }; - let fast_end_label = fast_blk.label.clone(); - fast_blk.br(&merge_label); - if require_numeric_layout { - let fast = LoweredValue { - semantic: SemanticKind::JsNumber, - rep: NativeRep::F64, - llvm_ty: DOUBLE, - value: fast_val.clone(), - }; - ctx.record_lowered_value_with_access_mode_and_facts( - "NumericArrayIndexGet", - None, - "js_array_numeric_get_f64_unboxed", - &fast, - Some(BoundsState::Guarded { - guard_id: "numeric_array_index_get_guard".to_string(), - }), - None, - Some(BufferAccessMode::CheckedNative), - None, - None, - None, - vec![raw_f64_layout_fact( - None, - "consumed", - "numeric_array_index_get_guard", - None, - )], - Vec::new(), - false, - false, - Vec::new(), - ); - } - - ctx.current_block = merge_idx; - Ok(ctx.block().phi( - DOUBLE, - &[ - (&fast_val, &fast_end_label), - (&fallback_val, &fallback_end_label), - ], - )) -} - -fn packed_f64_loop_fact(ctx: &FnCtx<'_>, arr_id: u32, idx_id: u32) -> Option { - ctx.packed_f64_loop_facts - .iter() - .find(|fact| fact.array_local_id == arr_id && fact.index_local_id == idx_id) - .cloned() -} - -fn lower_packed_f64_loop_index_get( - ctx: &mut FnCtx<'_>, - arr_id: u32, - arr_box: &str, - idx_i32: &str, - fact: &PackedF64LoopFact, -) -> String { - let guard_id = fact.guard_id.as_str(); - let array_kind = fact.array_kind; - let value = { - let blk = ctx.block(); - let arr_bits = blk.bitcast_double_to_i64(arr_box); - let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); - let idx_i64 = blk.zext(I32, idx_i32, I64); - let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); - let element_ptr = blk.inttoptr(I64, &element_addr); - blk.load(DOUBLE, &element_ptr) - }; - if fact.allow_holes { - // #6011: hole-tolerant range-guarded loop — the guard proved every - // slot in the window is a raw-f64 number OR TAG_HOLE. Reading a hole - // must observe `undefined` (or a polluted prototype), so side-exit to - // the slow preheader, which re-executes the current iteration through - // the generic read path. The side exit fires before any effect of the - // iteration (matcher invariant), so the re-run cannot double-apply. - let is_hole = { - let blk = ctx.block(); - let raw_bits = blk.bitcast_double_to_i64(&value); - blk.icmp_eq(I64, &raw_bits, crate::nanbox::TAG_HOLE_I64) - }; - let cont_idx = ctx.new_block("packed_f64_range.load.cont"); - let cont_label = ctx.block_label(cont_idx); - ctx.block() - .cond_br(&is_hole, &fact.store_side_exit_label, &cont_label); - ctx.current_block = cont_idx; - } - let lowered = LoweredValue { - semantic: SemanticKind::JsNumber, - rep: NativeRep::F64, - llvm_ty: DOUBLE, - value: value.clone(), - }; - ctx.record_lowered_value_with_access_mode_and_facts( - array_kind.load_expr_kind(), - Some(arr_id), - array_kind.load_consumer_f64(), - &lowered, - Some(BoundsState::Guarded { - guard_id: guard_id.to_string(), - }), - None, - Some(BufferAccessMode::CheckedNative), - None, - None, - None, - vec![ - array_kind_fact( - Some(arr_id), - "consumed", - array_kind.array_kind_label(), - None, - ), - raw_f64_layout_fact(Some(arr_id), "consumed", guard_id, None), - ], - Vec::new(), - false, - false, - vec![ - "index_range=nonnegative_i32".to_string(), - "length_range=guarded_i32".to_string(), - "storage_layout=raw_f64_numeric_slots".to_string(), - ], - ); - value -} - -/// #5525 follow-up: emit a guarded **inline** typed-array element read for an -/// `obj[i]` whose receiver static type is erased (`any`/unknown) but is, at -/// runtime, commonly an owning numeric typed array reached through an untyped -/// param — exactly bcryptjs's `S[i]`/`P[i]` Blowfish boxes (~600M reads for one -/// cost-12 `compareSync`). Instead of an out-of-line `js_dyn_index_get` call + -/// `lookup_typed_array_kind` + `js_number_coerce` per element, this inlines: -/// 1. receiver-is-pointer NaN-box guard, -/// 2. a read of the process-global `PERRY_TA_VIEW_GUARD` (must be 0 → every -/// live typed array uses inline storage, so `data_ptr == header + 16`), -/// 3. a probe of the `PERRY_TA_KIND_CACHE` slot for the receiver address -/// (matches the cached `(addr << 8) | tag` word; the tag is the element -/// kind and must be a non-BigInt kind ≤ `KIND_UINT8_CLAMPED`), -/// 4. an index validity + bounds check against the header `length`, -/// 5. a direct per-kind element load + int↔f64 widen, -/// and falls back to the existing `js_dyn_index_get` slow path on ANY guard -/// miss (non-pointer, cache miss, view live, BigInt/Float16 kind, OOB / -/// fractional / negative index, runtime-string or symbol key). Because every -/// rejected case defers to the unchanged runtime helper, semantics are -/// identical; only the hot monomorphic numeric-typed-array case is short-cut. -/// `obj_box` / `idx_d` are the already-lowered receiver and index (DOUBLE). -/// -/// `coerce_slow_to_number`: when the read is used in a context that will -/// `ToNumber` the result regardless (a non-`+` arithmetic / bitwise operand — -/// `^`, `-`, `*`, `<<`, …, all of which `ToNumber` their operands; see -/// [`lower_unknown_local_index_get_for_number_context`]), the cold slow branch's -/// `js_dyn_index_get` result is wrapped in `js_number_coerce` here so the merged -/// value is *always* a Number. The hot per-kind fast branches already produce a -/// Number, so the caller can skip the per-element site `js_number_coerce` it -/// would otherwise emit — moving that coercion off bcrypt's ~600M-read hot path -/// and onto the cache-miss path only. `false` leaves the slow result boxed -/// (the general `obj[i]` read, whose result may legitimately be a non-Number). -fn lower_inline_dyn_typed_array_get( - ctx: &mut FnCtx<'_>, - obj_box: &str, - idx_d: &str, - coerce_slow_to_number: bool, -) -> String { - // TAG_MASK / POINTER_TAG / POINTER_MASK as signed-i64 LLVM literals. - let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK); - let pointer_tag = crate::nanbox::POINTER_TAG_I64; - let pointer_mask = crate::nanbox::POINTER_MASK_I64; - - let fast_idx = ctx.new_block("tav.get.fast"); - let load_idx = ctx.new_block("tav.get.load"); - let slow_idx = ctx.new_block("tav.get.slow"); - let merge_idx = ctx.new_block("tav.get.merge"); - let fast_label = ctx.block_label(fast_idx); - let load_label = ctx.block_label(load_idx); - let slow_label = ctx.block_label(slow_idx); - let merge_label = ctx.block_label(merge_idx); - - // ---- entry: combined cache/kind/range guard -> fast | slow ---- - let entry_guard = { - let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(obj_box); - let raw = blk.and(I64, &obj_bits, pointer_mask); - // is_pointer: (bits & TAG_MASK) == POINTER_TAG - let tagged = blk.and(I64, &obj_bits, &tag_mask); - let is_ptr = blk.icmp_eq(I64, &tagged, pointer_tag); - // view guard must be 0 (all typed arrays inline-storage) - let vg = blk.load(I64, "@PERRY_TA_VIEW_GUARD"); - let vg_zero = blk.icmp_eq(I64, &vg, "0"); - // cache slot = (raw >> 3) & 63 - let slot = blk.lshr(I64, &raw, "3"); - let slot = blk.and(I64, &slot, "63"); - let entry_ptr = blk.gep( - "[64 x i64]", - "@PERRY_TA_KIND_CACHE", - &[(I64, "0"), (I64, &slot)], - ); - let entry_val = blk.load(I64, &entry_ptr); - // addr match: (entry_val u>> 8) == raw (also rejects empty slot = 0) - let entry_addr = blk.lshr(I64, &entry_val, "8"); - let addr_match = blk.icmp_eq(I64, &entry_addr, &raw); - // kind = entry_val & 0xFF; loadable numeric kind = kind <= 8 - // (KIND_INT8=0 .. KIND_UINT8_CLAMPED=8; rejects BigInt 9/10, - // Float16 11, and the 0xFF "not a typed array" sentinel). - let kind = blk.and(I64, &entry_val, "255"); - let kind_ok = blk.icmp_ule(I64, &kind, "8"); - // index float-range pre-checks (well-defined on NaN → false): the - // fptosi in the load block is only reached when these hold, so its - // result is never poison there. - let idx_ge0 = blk.fcmp("oge", idx_d, "0.0"); - let idx_lt = blk.fcmp("olt", idx_d, "4294967296.0"); - // AND-reduce all guards. - let g = blk.and(I1, &is_ptr, &vg_zero); - let g = blk.and(I1, &g, &addr_match); - let g = blk.and(I1, &g, &kind_ok); - let g = blk.and(I1, &g, &idx_ge0); - blk.and(I1, &g, &idx_lt) - }; - ctx.block().cond_br(&entry_guard, &fast_label, &slow_label); - - // ---- fast: validate integer index + bounds -> load | slow ---- - ctx.current_block = fast_idx; - let (raw, idx_i64, kind) = { - let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(obj_box); - let raw = blk.and(I64, &obj_bits, pointer_mask); - // kind re-read from cache (cheap; keeps the fast block self-contained). - let slot = blk.lshr(I64, &raw, "3"); - let slot = blk.and(I64, &slot, "63"); - let entry_ptr = blk.gep( - "[64 x i64]", - "@PERRY_TA_KIND_CACHE", - &[(I64, "0"), (I64, &slot)], - ); - let entry_val = blk.load(I64, &entry_ptr); - let kind = blk.and(I64, &entry_val, "255"); - // idx is in [0, 2^32) (entry guard) so fptosi i64 is well-defined. - let idx_i64 = blk.fptosi(DOUBLE, idx_d, I64); - (raw, idx_i64, kind) - }; - let fast_ok = { - let blk = ctx.block(); - // reject fractional indices: sitofp(idx_i64) == idx_d - let idx_back = blk.sitofp(I64, &idx_i64, DOUBLE); - let is_int = blk.fcmp("oeq", &idx_back, idx_d); - // bounds: idx < header.length (u32 at offset 0) - let hdr_ptr = blk.inttoptr(I64, &raw); - let len = blk.load(I32, &hdr_ptr); - let len_i64 = blk.zext(I32, &len, I64); - let in_bounds = blk.icmp_ult(I64, &idx_i64, &len_i64); - blk.and(I1, &is_int, &in_bounds) - }; - ctx.block().cond_br(&fast_ok, &load_label, &slow_label); - - // ---- load: per-kind direct element load (data = header + 16) ---- - ctx.current_block = load_idx; - // (value, end_label) for each per-kind load block, collected for the merge. - let kind_incoming: Vec<(String, String)>; - { - // Per-kind load blocks. Each computes the element address from - // `data = raw + 16` and `off = idx * elem_size`, loads the native - // slot, and widens to f64. We branch on `kind` via a cond_br chain. - // kinds: 0 I8, 1 U8, 2 I16, 3 U16, 4 I32, 5 U32, 6 F32, 7 F64, - // 8 U8Clamped (== U8 load). All others were excluded by the entry - // guard (kind <= 8). - let data_base = { - let blk = ctx.block(); - blk.add(I64, &raw, "16") - }; - // Helper closure-like inline: build a block that loads with a given - // element byte-width shift + LLVM elem type + widen, then brs to merge. - // We emit explicit blocks since closures can't borrow ctx mutably here. - - // Create the per-kind blocks up front. - let b_i8 = ctx.new_block("tav.k.i8"); - let b_u8 = ctx.new_block("tav.k.u8"); - let b_i16 = ctx.new_block("tav.k.i16"); - let b_u16 = ctx.new_block("tav.k.u16"); - let b_i32 = ctx.new_block("tav.k.i32"); - let b_u32 = ctx.new_block("tav.k.u32"); - let b_f32 = ctx.new_block("tav.k.f32"); - let b_f64 = ctx.new_block("tav.k.f64"); - let l_i8 = ctx.block_label(b_i8); - let l_u8 = ctx.block_label(b_u8); - let l_i16 = ctx.block_label(b_i16); - let l_u16 = ctx.block_label(b_u16); - let l_i32 = ctx.block_label(b_i32); - let l_u32 = ctx.block_label(b_u32); - let l_f32 = ctx.block_label(b_f32); - let l_f64 = ctx.block_label(b_f64); - - // Dispatch chain on `kind` (in the load block). - let chk = |ctx: &mut FnCtx<'_>, k: &str, hit: &str, next_idx: usize| { - let next_label = ctx.block_label(next_idx); - let cond = ctx.block().icmp_eq(I64, &kind, k); - ctx.block().cond_br(&cond, hit, &next_label); - }; - // 0..7 explicit; kind 8 (U8Clamped) shares the U8 load as the final - // else (no further branch needed — entry guard already proved kind<=8). - let c1 = ctx.new_block("tav.kd1"); - let c2 = ctx.new_block("tav.kd2"); - let c3 = ctx.new_block("tav.kd3"); - let c4 = ctx.new_block("tav.kd4"); - let c5 = ctx.new_block("tav.kd5"); - let c6 = ctx.new_block("tav.kd6"); - let c7 = ctx.new_block("tav.kd7"); - chk(ctx, "0", &l_i8, c1); - ctx.current_block = c1; - chk(ctx, "1", &l_u8, c2); - ctx.current_block = c2; - chk(ctx, "2", &l_i16, c3); - ctx.current_block = c3; - chk(ctx, "3", &l_u16, c4); - ctx.current_block = c4; - chk(ctx, "4", &l_i32, c5); - ctx.current_block = c5; - chk(ctx, "5", &l_u32, c6); - ctx.current_block = c6; - chk(ctx, "6", &l_f32, c7); - ctx.current_block = c7; - // remaining: kind 7 → f64, else (8) → u8. - let is_f64 = ctx.block().icmp_eq(I64, &kind, "7"); - ctx.block().cond_br(&is_f64, &l_f64, &l_u8); - - // Each per-kind block: compute elem addr, load, widen, br merge. - // off = idx << shift; addr = data_base + off. - let mut incoming: Vec<(String, String)> = Vec::new(); - // I8 (sext), U8 (zext), I16 (sext), U16 (zext) via the small-int helper. - incoming.push(emit_inline_ta_int_load( - ctx, - b_i8, - &idx_i64, - &data_base, - &merge_label, - "0", - I8, - true, - )); - incoming.push(emit_inline_ta_int_load( - ctx, - b_u8, - &idx_i64, - &data_base, - &merge_label, - "0", - I8, - false, - )); - incoming.push(emit_inline_ta_int_load( - ctx, - b_i16, - &idx_i64, - &data_base, - &merge_label, - "1", - I16, - true, - )); - incoming.push(emit_inline_ta_int_load( - ctx, - b_u16, - &idx_i64, - &data_base, - &merge_label, - "1", - I16, - false, - )); - // I32: load i32, sitofp directly (sext to i32 is a no-op). - { - ctx.current_block = b_i32; - let blk = ctx.block(); - let off = blk.shl(I64, &idx_i64, "2"); - let addr = blk.add(I64, &data_base, &off); - let ptr = blk.inttoptr(I64, &addr); - let raw_elem = blk.load(I32, &ptr); - let val = blk.sitofp(I32, &raw_elem, DOUBLE); - let end_label = blk.label.clone(); - blk.br(&merge_label); - incoming.push((val, end_label)); - } - // U32: load i32, treat as unsigned → uitofp. - { - ctx.current_block = b_u32; - let blk = ctx.block(); - let off = blk.shl(I64, &idx_i64, "2"); - let addr = blk.add(I64, &data_base, &off); - let ptr = blk.inttoptr(I64, &addr); - let raw_elem = blk.load(I32, &ptr); - let val = blk.uitofp(I32, &raw_elem, DOUBLE); - let end_label = blk.label.clone(); - blk.br(&merge_label); - incoming.push((val, end_label)); - } - // F32: load float, fpext. - { - ctx.current_block = b_f32; - let blk = ctx.block(); - let off = blk.shl(I64, &idx_i64, "2"); - let addr = blk.add(I64, &data_base, &off); - let ptr = blk.inttoptr(I64, &addr); - let raw_elem = blk.load(F32, &ptr); - let val = blk.fpext(F32, &raw_elem, DOUBLE); - let end_label = blk.label.clone(); - blk.br(&merge_label); - incoming.push((val, end_label)); - } - // F64: load double raw. - { - ctx.current_block = b_f64; - let blk = ctx.block(); - let off = blk.shl(I64, &idx_i64, "3"); - let addr = blk.add(I64, &data_base, &off); - let ptr = blk.inttoptr(I64, &addr); - let val = blk.load(DOUBLE, &ptr); - let end_label = blk.label.clone(); - blk.br(&merge_label); - incoming.push((val, end_label)); - } - - // Hand the collected per-kind (value,label) pairs to the final merge. - kind_incoming = incoming; - } - - // ---- slow: the unchanged runtime dispatcher ---- - ctx.current_block = slow_idx; - let slow_raw = ctx.block().call( - DOUBLE, - "js_dyn_index_get", - &[(DOUBLE, obj_box), (DOUBLE, idx_d)], - ); - // In a number context, coerce the (possibly boxed) slow result here so the - // merge phi is uniformly a Number and the arithmetic caller skips its own - // per-element coerce. A plain double already shortcuts `js_number_coerce`'s - // first branch, so re-coercing a fast-path-shaped value is a cheap no-op on - // the rare cache-miss path. - let slow_val = if coerce_slow_to_number { - ctx.block() - .call(DOUBLE, "js_number_coerce", &[(DOUBLE, &slow_raw)]) - } else { - slow_raw - }; - let slow_end_label = ctx.block().label.clone(); - ctx.block().br(&merge_label); - - // ---- final merge: one phi over every per-kind fast end + the slow end ---- - ctx.current_block = merge_idx; - let mut incoming_refs: Vec<(&str, &str)> = kind_incoming - .iter() - .map(|(v, l)| (v.as_str(), l.as_str())) - .collect(); - incoming_refs.push((slow_val.as_str(), slow_end_label.as_str())); - ctx.block().phi(DOUBLE, &incoming_refs) -} - -/// Emit one per-kind small-integer (1/2-byte) typed-array element load block for -/// [`lower_inline_dyn_typed_array_get`]: switches to `blk_idx`, computes the -/// element address (`data_base + (idx << shift)`), loads `elem_ty`, sign-/zero- -/// extends to i32, converts to f64, and branches to `merge_label`. Returns the -/// `(value, end_label)` pair for the merge phi. -#[allow(clippy::too_many_arguments)] -fn emit_inline_ta_int_load( - ctx: &mut FnCtx<'_>, - blk_idx: usize, - idx_i64: &str, - data_base: &str, - merge_label: &str, - shift: &str, - elem_ty: crate::types::LlvmType, - signed: bool, -) -> (String, String) { - ctx.current_block = blk_idx; - let blk = ctx.block(); - let off = blk.shl(I64, idx_i64, shift); - let addr = blk.add(I64, data_base, &off); - let ptr = blk.inttoptr(I64, &addr); - let raw_elem = blk.load(elem_ty, &ptr); - let val = if signed { - let i32v = blk.sext(elem_ty, &raw_elem, I32); - blk.sitofp(I32, &i32v, DOUBLE) - } else { - let i32v = blk.zext(elem_ty, &raw_elem, I32); - blk.uitofp(I32, &i32v, DOUBLE) - }; - let end_label = blk.label.clone(); - blk.br(merge_label); - (val, end_label) -} - pub(crate) fn lower_numeric_index_get_for_number_context( ctx: &mut FnCtx<'_>, expr: &Expr, diff --git a/crates/perry-codegen/src/expr/index_get/guarded_array.rs b/crates/perry-codegen/src/expr/index_get/guarded_array.rs new file mode 100644 index 0000000000..e5a18f2a55 --- /dev/null +++ b/crates/perry-codegen/src/expr/index_get/guarded_array.rs @@ -0,0 +1,447 @@ +//! Guarded and packed-f64 array element reads for `IndexGet`. +//! +//! Split out of `index_get.rs` to keep that file under the 2000-line cap. +//! Pure mechanical move — the items below are verbatim copies (only the +//! visibility of the three entry points is widened to `pub(super)` so the +//! trunk's call sites keep compiling). + +use anyhow::Result; + +use crate::nanbox::POINTER_MASK_I64; +use crate::native_value::{ + BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, SemanticKind, +}; +use crate::types::{DOUBLE, I1, I16, I32, I64, I8}; + +use super::{ + array_kind_fact, emit_typed_feedback_register_site, raw_f64_layout_fact, + typed_feedback_emission_enabled, FnCtx, PackedF64LoopFact, TypedFeedbackContract, + TypedFeedbackKind, +}; + +pub(super) fn lower_guarded_array_index_get( + ctx: &mut FnCtx<'_>, + arr_box: &str, + idx_i32: &str, + block_prefix: &str, + require_numeric_layout: bool, + coerce_numeric_fallback: bool, + receiver_slot: Option<&str>, +) -> Result { + let contract = if require_numeric_layout { + TypedFeedbackContract::numeric_array_get_index() + } else { + TypedFeedbackContract::array_get_index() + }; + let feedback_site_id = emit_typed_feedback_register_site( + ctx, + TypedFeedbackKind::ArrayElement, + "array[index]", + contract, + ); + let fast_idx = ctx.new_block(&format!("{}.fast", block_prefix)); + let fallback_idx = ctx.new_block(&format!("{}.fallback", block_prefix)); + let merge_idx = ctx.new_block(&format!("{}.merge", block_prefix)); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + + if !typed_feedback_emission_enabled() { + // Normal builds do not collect feedback. Inline the plain-array + // structural guard instead of paying an out-of-line call merely to + // rediscover the same header facts before the direct slot load below. + // Prototype-chain invalidators are summarized by one sticky runtime + // byte; per-array descriptors and forwarding remain receiver-local. + // + // Repsel 4a.1: the NUMERIC tier gets the same inline guard — plus an + // `_reserved & GC_ARRAY_RAW_F64_LAYOUT (0x80)` dense-proof test on the + // header word the plain guard already loads. A dense-flagged array + // needs no runtime call at all (the raw-f64 slot IS the value, no + // hole select). Arrays not yet flagged take a COLD out-of-line + // `js_typed_feedback_numeric_array_index_get_guard` call, whose + // first-touch path verifies-and-rewrites the layout (setting the + // flag), so the steady state is the inline tier. This ends the + // typed-`number[]`-slower-than-untyped inversion for reads. + let deref_idx = ctx.new_block(&format!("{}.guard.deref", block_prefix)); + let deref_label = ctx.block_label(deref_idx); + let cold_guard_idx = if require_numeric_layout { + Some(ctx.new_block(&format!("{}.guard.cold", block_prefix))) + } else { + None + }; + let guard_fail_label = match cold_guard_idx { + Some(idx) => ctx.block_label(idx), + None => fallback_label.clone(), + }; + { + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(arr_box); + let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); + let tag = blk.lshr(I64, &arr_bits, "48"); + let is_pointer = blk.icmp_eq(I64, &tag, "32765"); // POINTER_TAG + let above_handle_band = blk.icmp_ugt(I64, &arr_handle, "1048575"); + let heap_candidate = blk.and(I1, &is_pointer, &above_handle_band); + blk.cond_br(&heap_candidate, &deref_label, &guard_fail_label); + } + + ctx.current_block = deref_idx; + { + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(arr_box); + let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); + + let gc_type_addr = blk.sub(I64, &arr_handle, "8"); + let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); + let gc_type = blk.load(I8, &gc_type_ptr); + let is_array = blk.icmp_eq(I8, &gc_type, "1"); // GC_TYPE_ARRAY + + let gc_flags_addr = blk.sub(I64, &arr_handle, "7"); + let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr); + let gc_flags = blk.load(I8, &gc_flags_ptr); + let forwarded_bits = blk.and(I8, &gc_flags, "128"); + let not_forwarded = blk.icmp_eq(I8, &forwarded_bits, "0"); + + let reserved_addr = blk.sub(I64, &arr_handle, "6"); + let reserved_ptr = blk.inttoptr(I64, &reserved_addr); + let reserved = blk.load(I16, &reserved_ptr); + let descriptor_bits = blk.and(I16, &reserved, "1024"); + let no_descriptors = blk.icmp_eq(I16, &descriptor_bits, "0"); + + let invalidated = blk.load_volatile(I8, "@PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED"); + let default_prototype_chain = blk.icmp_eq(I8, &invalidated, "0"); + + let arr_ptr = blk.inttoptr(I64, &arr_handle); + let length = blk.load(I32, &arr_ptr); + let capacity_ptr = blk.gep(I8, &arr_ptr, &[(I64, "4")]); + let capacity = blk.load(I32, &capacity_ptr); + let index_nonnegative = blk.icmp_slt(I32, idx_i32, "0"); + let index_nonnegative = blk.icmp_eq(I1, &index_nonnegative, "false"); + let index_in_bounds = blk.icmp_ult(I32, idx_i32, &length); + let length_sane = blk.icmp_ule(I32, &length, "16000000"); + let capacity_sane = blk.icmp_ule(I32, &capacity, "16000000"); + let length_within_capacity = blk.icmp_ule(I32, &length, &capacity); + + let mut guard_ok = blk.and(I1, &is_array, ¬_forwarded); + guard_ok = blk.and(I1, &guard_ok, &no_descriptors); + guard_ok = blk.and(I1, &guard_ok, &default_prototype_chain); + guard_ok = blk.and(I1, &guard_ok, &index_nonnegative); + guard_ok = blk.and(I1, &guard_ok, &index_in_bounds); + guard_ok = blk.and(I1, &guard_ok, &length_sane); + guard_ok = blk.and(I1, &guard_ok, &capacity_sane); + guard_ok = blk.and(I1, &guard_ok, &length_within_capacity); + if require_numeric_layout { + // Dense raw-f64 proof: every slot in [0, length) holds + // canonical raw f64 bits (GC_ARRAY_RAW_F64_LAYOUT, 0x80). + // + // Repsel 4a.2 (#6904): a NUMBER-CONTEXT read (the caller will + // ToNumber the element regardless — `coerce_numeric_fallback`) + // additionally accepts the hole-tolerant invariant + // (GC_ARRAY_RAW_F64_HOLES, 0x1000): every slot is canonical + // raw f64 OR TAG_HOLE, and the fast arm canonicalizes any NaN + // payload (TAG_HOLE included) to the quiet NaN — bit-exact + // with ToNumber(undefined) for a hole and with ToNumber(NaN) + // for a stored NaN. This is the `new Array(n)` mid-fill axis: + // such arrays are provably-not-dense until the last slot is + // written, so the dense-only tier never fired for them. + let raw_mask = if coerce_numeric_fallback { + "4224" // 0x1080 = RAW_F64_LAYOUT | RAW_F64_HOLES + } else { + "128" // dense only: the raw slot is exposed verbatim + }; + let raw_bits = blk.and(I16, &reserved, raw_mask); + let is_raw = blk.icmp_ne(I16, &raw_bits, "0"); + guard_ok = blk.and(I1, &guard_ok, &is_raw); + } + blk.cond_br(&guard_ok, &fast_label, &guard_fail_label); + } + + if let Some(cold_idx) = cold_guard_idx { + // Cold arm: the out-of-line guard rebuilds unmarked-but-numeric + // arrays into raw-f64 layout (then this call site goes inline on + // every later read); everything else routes to the boxed fallback. + ctx.current_block = cold_idx; + // Self-heal a stale growth-forwarded binding first (see + // `receiver_repair_slot`): follow the chain, write the live head + // back to the local slot. This iteration still takes the guard + // on the ORIGINAL value (a forwarded head fails it → boxed + // fallback, which follows the chain — correct either way); every + // later iteration re-loads the repaired slot and goes inline. + if let Some(slot) = receiver_slot { + let blk = ctx.block(); + let fresh = blk.call(DOUBLE, "js_array_refresh_local_head", &[(DOUBLE, arr_box)]); + blk.store(DOUBLE, &fresh, slot); + } + let guard_ok = { + let blk = ctx.block(); + let guard_i32 = blk.call( + I32, + "js_typed_feedback_numeric_array_index_get_guard", + &[ + (I64, &feedback_site_id), + (DOUBLE, arr_box), + (I32, idx_i32), + (I32, "1"), + ], + ); + blk.icmp_ne(I32, &guard_i32, "0") + }; + ctx.block().cond_br(&guard_ok, &fast_label, &fallback_label); + } + } else { + let guard_ok = { + let blk = ctx.block(); + let guard_fn = if require_numeric_layout { + "js_typed_feedback_numeric_array_index_get_guard" + } else { + "js_typed_feedback_plain_array_index_get_guard" + }; + let guard_i32 = blk.call( + I32, + guard_fn, + &[ + (I64, &feedback_site_id), + (DOUBLE, arr_box), + (I32, idx_i32), + (I32, "1"), + ], + ); + blk.icmp_ne(I32, &guard_i32, "0") + }; + ctx.block().cond_br(&guard_ok, &fast_label, &fallback_label); + } + + ctx.current_block = fallback_idx; + // Materialize the f64 index only here (cold path) so the int→fp conversion + // stays out of the numeric loop's hot region. + let idx_box = ctx.block().sitofp(I32, idx_i32, DOUBLE); + let fallback_boxed = ctx.block().call( + DOUBLE, + "js_typed_feedback_array_index_get_fallback_boxed", + &[ + (I64, &feedback_site_id), + (DOUBLE, arr_box), + (DOUBLE, &idx_box), + ], + ); + let fallback_val = if require_numeric_layout && coerce_numeric_fallback { + ctx.block() + .call(DOUBLE, "js_number_coerce", &[(DOUBLE, &fallback_boxed)]) + } else { + fallback_boxed.clone() + }; + let fallback_end_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + if require_numeric_layout { + let fallback = LoweredValue::js_value(fallback_boxed.clone()); + ctx.record_lowered_value_with_access_mode_and_facts( + "NumericArrayIndexGet", + None, + "js_typed_feedback_array_index_get_fallback_boxed", + &fallback, + Some(BoundsState::Unknown), + None, + Some(BufferAccessMode::DynamicFallback), + Some(MaterializationReason::RuntimeApi), + None, + None, + Vec::new(), + vec![ + raw_f64_layout_fact( + None, + "rejected", + "numeric_array_index_get_guard", + Some(MaterializationReason::RuntimeApi), + ), + raw_f64_layout_fact( + None, + "invalidated", + "runtime_api", + Some(MaterializationReason::RuntimeApi), + ), + ], + false, + false, + Vec::new(), + ); + } + + ctx.current_block = fast_idx; + let fast_blk = ctx.block(); + let arr_bits = fast_blk.bitcast_double_to_i64(arr_box); + let arr_handle = fast_blk.and(I64, &arr_bits, POINTER_MASK_I64); + let fast_val = if require_numeric_layout { + // The guard on the way into this block (inline tier or the runtime + // `numeric_array_index_get_guard`) already proved: a plain, + // non-forwarded `Array`, in raw-f64 (or, for number-context reads, + // raw-f64-or-holes) layout, with `index` in bounds. So load the slot + // inline instead of calling `js_array_numeric_get_f64_unboxed`, + // whose hot path re-validates exactly those same conditions and then + // does this load. + let idx_i64 = fast_blk.zext(I32, idx_i32, I64); + let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); + let with_header = fast_blk.add(I64, &byte_offset, "8"); + let element_addr = fast_blk.add(I64, &arr_handle, &with_header); + let element_ptr = fast_blk.inttoptr(I64, &element_addr); + let raw = fast_blk.load(DOUBLE, &element_ptr); + if coerce_numeric_fallback { + // Repsel 4a.2: number-context canonicalization — any NaN payload + // (a TAG_HOLE slot under the raw-f64-or-holes proof, or a stored + // canonical NaN) becomes the quiet NaN. Bit-exact: + // ToNumber(undefined) = NaN for a hole, ToNumber(NaN) = NaN for a + // stored NaN, identity for every real number. PROOF-GATED: only + // sound because the guard admitted raw-f64-or-holes slots — an + // arbitrary NaN-boxed tag would be wrongly collapsed to NaN. + let is_ord = fast_blk.fcmp("ord", &raw, &raw); + fast_blk.select(I1, &is_ord, DOUBLE, &raw, "0x7FF8000000000000") + } else { + // Dense-only proof: no HOLE slots exist; the raw slot IS the + // element value, exposed verbatim. + raw + } + } else { + let idx_i64 = fast_blk.zext(I32, idx_i32, I64); + let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); + let with_header = fast_blk.add(I64, &byte_offset, "8"); + let element_addr = fast_blk.add(I64, &arr_handle, &with_header); + let element_ptr = fast_blk.inttoptr(I64, &element_addr); + let fast_raw = fast_blk.load(DOUBLE, &element_ptr); + // `new Array(n)` slots are TAG_HOLE internally; JavaScript reads expose + // `undefined`. + let fast_raw_bits = fast_blk.bitcast_double_to_i64(&fast_raw); + let is_hole = fast_blk.icmp_eq(I64, &fast_raw_bits, crate::nanbox::TAG_HOLE_I64); + let undef_d = fast_blk.bitcast_i64_to_double(crate::nanbox::TAG_UNDEFINED_I64); + fast_blk.select(I1, &is_hole, DOUBLE, &undef_d, &fast_raw) + }; + let fast_end_label = fast_blk.label.clone(); + fast_blk.br(&merge_label); + if require_numeric_layout { + let fast = LoweredValue { + semantic: SemanticKind::JsNumber, + rep: NativeRep::F64, + llvm_ty: DOUBLE, + value: fast_val.clone(), + }; + ctx.record_lowered_value_with_access_mode_and_facts( + "NumericArrayIndexGet", + None, + "js_array_numeric_get_f64_unboxed", + &fast, + Some(BoundsState::Guarded { + guard_id: "numeric_array_index_get_guard".to_string(), + }), + None, + Some(BufferAccessMode::CheckedNative), + None, + None, + None, + vec![raw_f64_layout_fact( + None, + "consumed", + "numeric_array_index_get_guard", + None, + )], + Vec::new(), + false, + false, + Vec::new(), + ); + } + + ctx.current_block = merge_idx; + Ok(ctx.block().phi( + DOUBLE, + &[ + (&fast_val, &fast_end_label), + (&fallback_val, &fallback_end_label), + ], + )) +} + +pub(super) fn packed_f64_loop_fact( + ctx: &FnCtx<'_>, + arr_id: u32, + idx_id: u32, +) -> Option { + ctx.packed_f64_loop_facts + .iter() + .find(|fact| fact.array_local_id == arr_id && fact.index_local_id == idx_id) + .cloned() +} + +pub(super) fn lower_packed_f64_loop_index_get( + ctx: &mut FnCtx<'_>, + arr_id: u32, + arr_box: &str, + idx_i32: &str, + fact: &PackedF64LoopFact, +) -> String { + let guard_id = fact.guard_id.as_str(); + let array_kind = fact.array_kind; + let value = { + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(arr_box); + let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); + let idx_i64 = blk.zext(I32, idx_i32, I64); + let byte_offset = blk.shl(I64, &idx_i64, "3"); + let with_header = blk.add(I64, &byte_offset, "8"); + let element_addr = blk.add(I64, &arr_handle, &with_header); + let element_ptr = blk.inttoptr(I64, &element_addr); + blk.load(DOUBLE, &element_ptr) + }; + if fact.allow_holes { + // #6011: hole-tolerant range-guarded loop — the guard proved every + // slot in the window is a raw-f64 number OR TAG_HOLE. Reading a hole + // must observe `undefined` (or a polluted prototype), so side-exit to + // the slow preheader, which re-executes the current iteration through + // the generic read path. The side exit fires before any effect of the + // iteration (matcher invariant), so the re-run cannot double-apply. + let is_hole = { + let blk = ctx.block(); + let raw_bits = blk.bitcast_double_to_i64(&value); + blk.icmp_eq(I64, &raw_bits, crate::nanbox::TAG_HOLE_I64) + }; + let cont_idx = ctx.new_block("packed_f64_range.load.cont"); + let cont_label = ctx.block_label(cont_idx); + ctx.block() + .cond_br(&is_hole, &fact.store_side_exit_label, &cont_label); + ctx.current_block = cont_idx; + } + let lowered = LoweredValue { + semantic: SemanticKind::JsNumber, + rep: NativeRep::F64, + llvm_ty: DOUBLE, + value: value.clone(), + }; + ctx.record_lowered_value_with_access_mode_and_facts( + array_kind.load_expr_kind(), + Some(arr_id), + array_kind.load_consumer_f64(), + &lowered, + Some(BoundsState::Guarded { + guard_id: guard_id.to_string(), + }), + None, + Some(BufferAccessMode::CheckedNative), + None, + None, + None, + vec![ + array_kind_fact( + Some(arr_id), + "consumed", + array_kind.array_kind_label(), + None, + ), + raw_f64_layout_fact(Some(arr_id), "consumed", guard_id, None), + ], + Vec::new(), + false, + false, + vec![ + "index_range=nonnegative_i32".to_string(), + "length_range=guarded_i32".to_string(), + "storage_layout=raw_f64_numeric_slots".to_string(), + ], + ); + value +} diff --git a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs new file mode 100644 index 0000000000..f7151cfe19 --- /dev/null +++ b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs @@ -0,0 +1,372 @@ +//! Inline guarded typed-array element read for a dynamically-typed receiver. +//! +//! Split out of `index_get.rs` to keep that file under the 2000-line cap. +//! Pure mechanical move — the items below are verbatim copies (only the +//! visibility of the entry point is widened to `pub(super)` so the trunk's +//! call sites keep compiling). + +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8}; + +use super::FnCtx; + +/// #5525 follow-up: emit a guarded **inline** typed-array element read for an +/// `obj[i]` whose receiver static type is erased (`any`/unknown) but is, at +/// runtime, commonly an owning numeric typed array reached through an untyped +/// param — exactly bcryptjs's `S[i]`/`P[i]` Blowfish boxes (~600M reads for one +/// cost-12 `compareSync`). Instead of an out-of-line `js_dyn_index_get` call + +/// `lookup_typed_array_kind` + `js_number_coerce` per element, this inlines: +/// 1. receiver-is-pointer NaN-box guard, +/// 2. a read of the process-global `PERRY_TA_VIEW_GUARD` (must be 0 → every +/// live typed array uses inline storage, so `data_ptr == header + 16`), +/// 3. a probe of the `PERRY_TA_KIND_CACHE` slot for the receiver address +/// (matches the cached `(addr << 8) | tag` word; the tag is the element +/// kind and must be a non-BigInt kind ≤ `KIND_UINT8_CLAMPED`), +/// 4. an index validity + bounds check against the header `length`, +/// 5. a direct per-kind element load + int↔f64 widen, +/// and falls back to the existing `js_dyn_index_get` slow path on ANY guard +/// miss (non-pointer, cache miss, view live, BigInt/Float16 kind, OOB / +/// fractional / negative index, runtime-string or symbol key). Because every +/// rejected case defers to the unchanged runtime helper, semantics are +/// identical; only the hot monomorphic numeric-typed-array case is short-cut. +/// `obj_box` / `idx_d` are the already-lowered receiver and index (DOUBLE). +/// +/// `coerce_slow_to_number`: when the read is used in a context that will +/// `ToNumber` the result regardless (a non-`+` arithmetic / bitwise operand — +/// `^`, `-`, `*`, `<<`, …, all of which `ToNumber` their operands; see +/// [`lower_unknown_local_index_get_for_number_context`]), the cold slow branch's +/// `js_dyn_index_get` result is wrapped in `js_number_coerce` here so the merged +/// value is *always* a Number. The hot per-kind fast branches already produce a +/// Number, so the caller can skip the per-element site `js_number_coerce` it +/// would otherwise emit — moving that coercion off bcrypt's ~600M-read hot path +/// and onto the cache-miss path only. `false` leaves the slow result boxed +/// (the general `obj[i]` read, whose result may legitimately be a non-Number). +pub(super) fn lower_inline_dyn_typed_array_get( + ctx: &mut FnCtx<'_>, + obj_box: &str, + idx_d: &str, + coerce_slow_to_number: bool, +) -> String { + // TAG_MASK / POINTER_TAG / POINTER_MASK as signed-i64 LLVM literals. + let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK); + let pointer_tag = crate::nanbox::POINTER_TAG_I64; + let pointer_mask = crate::nanbox::POINTER_MASK_I64; + + let fast_idx = ctx.new_block("tav.get.fast"); + let load_idx = ctx.new_block("tav.get.load"); + let slow_idx = ctx.new_block("tav.get.slow"); + let merge_idx = ctx.new_block("tav.get.merge"); + let fast_label = ctx.block_label(fast_idx); + let load_label = ctx.block_label(load_idx); + let slow_label = ctx.block_label(slow_idx); + let merge_label = ctx.block_label(merge_idx); + + // ---- entry: combined cache/kind/range guard -> fast | slow ---- + let entry_guard = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, pointer_mask); + // is_pointer: (bits & TAG_MASK) == POINTER_TAG + let tagged = blk.and(I64, &obj_bits, &tag_mask); + let is_ptr = blk.icmp_eq(I64, &tagged, pointer_tag); + // view guard must be 0 (all typed arrays inline-storage) + let vg = blk.load(I64, "@PERRY_TA_VIEW_GUARD"); + let vg_zero = blk.icmp_eq(I64, &vg, "0"); + // cache slot = (raw >> 3) & 63 + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + // addr match: (entry_val u>> 8) == raw (also rejects empty slot = 0) + let entry_addr = blk.lshr(I64, &entry_val, "8"); + let addr_match = blk.icmp_eq(I64, &entry_addr, &raw); + // kind = entry_val & 0xFF; loadable numeric kind = kind <= 8 + // (KIND_INT8=0 .. KIND_UINT8_CLAMPED=8; rejects BigInt 9/10, + // Float16 11, and the 0xFF "not a typed array" sentinel). + let kind = blk.and(I64, &entry_val, "255"); + let kind_ok = blk.icmp_ule(I64, &kind, "8"); + // index float-range pre-checks (well-defined on NaN → false): the + // fptosi in the load block is only reached when these hold, so its + // result is never poison there. + let idx_ge0 = blk.fcmp("oge", idx_d, "0.0"); + let idx_lt = blk.fcmp("olt", idx_d, "4294967296.0"); + // AND-reduce all guards. + let g = blk.and(I1, &is_ptr, &vg_zero); + let g = blk.and(I1, &g, &addr_match); + let g = blk.and(I1, &g, &kind_ok); + let g = blk.and(I1, &g, &idx_ge0); + blk.and(I1, &g, &idx_lt) + }; + ctx.block().cond_br(&entry_guard, &fast_label, &slow_label); + + // ---- fast: validate integer index + bounds -> load | slow ---- + ctx.current_block = fast_idx; + let (raw, idx_i64, kind) = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, pointer_mask); + // kind re-read from cache (cheap; keeps the fast block self-contained). + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + let kind = blk.and(I64, &entry_val, "255"); + // idx is in [0, 2^32) (entry guard) so fptosi i64 is well-defined. + let idx_i64 = blk.fptosi(DOUBLE, idx_d, I64); + (raw, idx_i64, kind) + }; + let fast_ok = { + let blk = ctx.block(); + // reject fractional indices: sitofp(idx_i64) == idx_d + let idx_back = blk.sitofp(I64, &idx_i64, DOUBLE); + let is_int = blk.fcmp("oeq", &idx_back, idx_d); + // bounds: idx < header.length (u32 at offset 0) + let hdr_ptr = blk.inttoptr(I64, &raw); + let len = blk.load(I32, &hdr_ptr); + let len_i64 = blk.zext(I32, &len, I64); + let in_bounds = blk.icmp_ult(I64, &idx_i64, &len_i64); + blk.and(I1, &is_int, &in_bounds) + }; + ctx.block().cond_br(&fast_ok, &load_label, &slow_label); + + // ---- load: per-kind direct element load (data = header + 16) ---- + ctx.current_block = load_idx; + // (value, end_label) for each per-kind load block, collected for the merge. + let kind_incoming: Vec<(String, String)>; + { + // Per-kind load blocks. Each computes the element address from + // `data = raw + 16` and `off = idx * elem_size`, loads the native + // slot, and widens to f64. We branch on `kind` via a cond_br chain. + // kinds: 0 I8, 1 U8, 2 I16, 3 U16, 4 I32, 5 U32, 6 F32, 7 F64, + // 8 U8Clamped (== U8 load). All others were excluded by the entry + // guard (kind <= 8). + let data_base = { + let blk = ctx.block(); + blk.add(I64, &raw, "16") + }; + // Helper closure-like inline: build a block that loads with a given + // element byte-width shift + LLVM elem type + widen, then brs to merge. + // We emit explicit blocks since closures can't borrow ctx mutably here. + + // Create the per-kind blocks up front. + let b_i8 = ctx.new_block("tav.k.i8"); + let b_u8 = ctx.new_block("tav.k.u8"); + let b_i16 = ctx.new_block("tav.k.i16"); + let b_u16 = ctx.new_block("tav.k.u16"); + let b_i32 = ctx.new_block("tav.k.i32"); + let b_u32 = ctx.new_block("tav.k.u32"); + let b_f32 = ctx.new_block("tav.k.f32"); + let b_f64 = ctx.new_block("tav.k.f64"); + let l_i8 = ctx.block_label(b_i8); + let l_u8 = ctx.block_label(b_u8); + let l_i16 = ctx.block_label(b_i16); + let l_u16 = ctx.block_label(b_u16); + let l_i32 = ctx.block_label(b_i32); + let l_u32 = ctx.block_label(b_u32); + let l_f32 = ctx.block_label(b_f32); + let l_f64 = ctx.block_label(b_f64); + + // Dispatch chain on `kind` (in the load block). + let chk = |ctx: &mut FnCtx<'_>, k: &str, hit: &str, next_idx: usize| { + let next_label = ctx.block_label(next_idx); + let cond = ctx.block().icmp_eq(I64, &kind, k); + ctx.block().cond_br(&cond, hit, &next_label); + }; + // 0..7 explicit; kind 8 (U8Clamped) shares the U8 load as the final + // else (no further branch needed — entry guard already proved kind<=8). + let c1 = ctx.new_block("tav.kd1"); + let c2 = ctx.new_block("tav.kd2"); + let c3 = ctx.new_block("tav.kd3"); + let c4 = ctx.new_block("tav.kd4"); + let c5 = ctx.new_block("tav.kd5"); + let c6 = ctx.new_block("tav.kd6"); + let c7 = ctx.new_block("tav.kd7"); + chk(ctx, "0", &l_i8, c1); + ctx.current_block = c1; + chk(ctx, "1", &l_u8, c2); + ctx.current_block = c2; + chk(ctx, "2", &l_i16, c3); + ctx.current_block = c3; + chk(ctx, "3", &l_u16, c4); + ctx.current_block = c4; + chk(ctx, "4", &l_i32, c5); + ctx.current_block = c5; + chk(ctx, "5", &l_u32, c6); + ctx.current_block = c6; + chk(ctx, "6", &l_f32, c7); + ctx.current_block = c7; + // remaining: kind 7 → f64, else (8) → u8. + let is_f64 = ctx.block().icmp_eq(I64, &kind, "7"); + ctx.block().cond_br(&is_f64, &l_f64, &l_u8); + + // Each per-kind block: compute elem addr, load, widen, br merge. + // off = idx << shift; addr = data_base + off. + let mut incoming: Vec<(String, String)> = Vec::new(); + // I8 (sext), U8 (zext), I16 (sext), U16 (zext) via the small-int helper. + incoming.push(emit_inline_ta_int_load( + ctx, + b_i8, + &idx_i64, + &data_base, + &merge_label, + "0", + I8, + true, + )); + incoming.push(emit_inline_ta_int_load( + ctx, + b_u8, + &idx_i64, + &data_base, + &merge_label, + "0", + I8, + false, + )); + incoming.push(emit_inline_ta_int_load( + ctx, + b_i16, + &idx_i64, + &data_base, + &merge_label, + "1", + I16, + true, + )); + incoming.push(emit_inline_ta_int_load( + ctx, + b_u16, + &idx_i64, + &data_base, + &merge_label, + "1", + I16, + false, + )); + // I32: load i32, sitofp directly (sext to i32 is a no-op). + { + ctx.current_block = b_i32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(I32, &ptr); + let val = blk.sitofp(I32, &raw_elem, DOUBLE); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + // U32: load i32, treat as unsigned → uitofp. + { + ctx.current_block = b_u32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(I32, &ptr); + let val = blk.uitofp(I32, &raw_elem, DOUBLE); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + // F32: load float, fpext. + { + ctx.current_block = b_f32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(F32, &ptr); + let val = blk.fpext(F32, &raw_elem, DOUBLE); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + // F64: load double raw. + { + ctx.current_block = b_f64; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "3"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let val = blk.load(DOUBLE, &ptr); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + + // Hand the collected per-kind (value,label) pairs to the final merge. + kind_incoming = incoming; + } + + // ---- slow: the unchanged runtime dispatcher ---- + ctx.current_block = slow_idx; + let slow_raw = ctx.block().call( + DOUBLE, + "js_dyn_index_get", + &[(DOUBLE, obj_box), (DOUBLE, idx_d)], + ); + // In a number context, coerce the (possibly boxed) slow result here so the + // merge phi is uniformly a Number and the arithmetic caller skips its own + // per-element coerce. A plain double already shortcuts `js_number_coerce`'s + // first branch, so re-coercing a fast-path-shaped value is a cheap no-op on + // the rare cache-miss path. + let slow_val = if coerce_slow_to_number { + ctx.block() + .call(DOUBLE, "js_number_coerce", &[(DOUBLE, &slow_raw)]) + } else { + slow_raw + }; + let slow_end_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + + // ---- final merge: one phi over every per-kind fast end + the slow end ---- + ctx.current_block = merge_idx; + let mut incoming_refs: Vec<(&str, &str)> = kind_incoming + .iter() + .map(|(v, l)| (v.as_str(), l.as_str())) + .collect(); + incoming_refs.push((slow_val.as_str(), slow_end_label.as_str())); + ctx.block().phi(DOUBLE, &incoming_refs) +} + +/// Emit one per-kind small-integer (1/2-byte) typed-array element load block for +/// [`lower_inline_dyn_typed_array_get`]: switches to `blk_idx`, computes the +/// element address (`data_base + (idx << shift)`), loads `elem_ty`, sign-/zero- +/// extends to i32, converts to f64, and branches to `merge_label`. Returns the +/// `(value, end_label)` pair for the merge phi. +#[allow(clippy::too_many_arguments)] +fn emit_inline_ta_int_load( + ctx: &mut FnCtx<'_>, + blk_idx: usize, + idx_i64: &str, + data_base: &str, + merge_label: &str, + shift: &str, + elem_ty: crate::types::LlvmType, + signed: bool, +) -> (String, String) { + ctx.current_block = blk_idx; + let blk = ctx.block(); + let off = blk.shl(I64, idx_i64, shift); + let addr = blk.add(I64, data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(elem_ty, &ptr); + let val = if signed { + let i32v = blk.sext(elem_ty, &raw_elem, I32); + blk.sitofp(I32, &i32v, DOUBLE) + } else { + let i32v = blk.zext(elem_ty, &raw_elem, I32); + blk.uitofp(I32, &i32v, DOUBLE) + }; + let end_label = blk.label.clone(); + blk.br(merge_label); + (val, end_label) +} diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 9b5187d953..6feb32f98e 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -39,18 +39,14 @@ use perry_ffi::{ JsPromise, JsValue, RawClosureHeader, StringHeader, }; use std::collections::HashMap; -use std::io; use std::net::SocketAddr; use std::pin::Pin; use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{mpsc, oneshot}; -use tokio_rustls::client::TlsStream; - // #1852 — topical sub-modules split out to keep this file under the // 2000-line size gate. `tls` holds the rustls config + handshake; `ip` // holds the `net.isIP*` + auto-select-family helpers. @@ -111,72 +107,12 @@ pub(crate) use jsvalue::{ use crate::tls::do_tls_handshake; // ─── Transport enum (plain or TLS, swappable at runtime) ───────────────────── - -pub(crate) enum Transport { - Plain(TcpStream), - Tls(Box>), -} - -impl Transport { - /// Set `TCP_NODELAY` on the underlying TCP socket. For a TLS transport the - /// option lives on the wrapped TCP stream (`get_ref().0`), so reach through - /// the rustls wrapper to the kernel socket. Matches Node's `socket.setNoDelay`, - /// which toggles Nagle's algorithm on the raw connection regardless of TLS. - fn set_nodelay(&self, nodelay: bool) -> io::Result<()> { - match self { - Transport::Plain(s) => s.set_nodelay(nodelay), - Transport::Tls(s) => s.get_ref().0.set_nodelay(nodelay), - } - } - - /// Read the current `TCP_NODELAY` state off the underlying socket. - /// Test-only observability seam for the nodelay command-path test. - #[cfg(test)] - fn nodelay(&self) -> io::Result { - match self { - Transport::Plain(s) => s.nodelay(), - Transport::Tls(s) => s.get_ref().0.nodelay(), - } - } -} - -impl AsyncRead for Transport { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.get_mut() { - Transport::Plain(s) => Pin::new(s).poll_read(cx, buf), - Transport::Tls(s) => Pin::new(&mut **s).poll_read(cx, buf), - } - } -} - -impl AsyncWrite for Transport { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.get_mut() { - Transport::Plain(s) => Pin::new(s).poll_write(cx, buf), - Transport::Tls(s) => Pin::new(&mut **s).poll_write(cx, buf), - } - } - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - Transport::Plain(s) => Pin::new(s).poll_flush(cx), - Transport::Tls(s) => Pin::new(&mut **s).poll_flush(cx), - } - } - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - Transport::Plain(s) => Pin::new(s).poll_shutdown(cx), - Transport::Tls(s) => Pin::new(&mut **s).poll_shutdown(cx), - } - } -} +// +// Split out to `transport.rs` for the 2000-line file-size gate. The +// `pub(crate)` re-export keeps `crate::Transport` resolving unchanged for +// the `adopt` / `nodelay_tests` siblings and for this file. +mod transport; +pub(crate) use transport::Transport; // ─── Handle storage ────────────────────────────────────────────────────────── // @@ -1849,162 +1785,4 @@ pub extern "C" fn js_ext_net_has_active_handles() -> i32 { } #[cfg(test)] -mod tests { - use super::*; - use std::sync::{Mutex, MutexGuard}; - - static GC_TEST_LOCK: Mutex<()> = Mutex::new(()); - - struct GcTestGuard { - frame: u64, - _lock: MutexGuard<'static, ()>, - } - - impl GcTestGuard { - fn new() -> Self { - let lock = GC_TEST_LOCK - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - perry_runtime::gc::js_gc_write_barriers_emitted(1); - let frame = perry_runtime::gc::js_shadow_frame_push(0); - Self { frame, _lock: lock } - } - } - - impl Drop for GcTestGuard { - fn drop(&mut self) { - perry_runtime::gc::js_shadow_frame_pop(self.frame); - perry_runtime::gc::js_gc_write_barriers_emitted(0); - } - } - - struct NetHandleCleanup { - handles: Vec, - } - - impl NetHandleCleanup { - fn new(handles: Vec) -> Self { - Self { handles } - } - } - - impl Drop for NetHandleCleanup { - fn drop(&mut self) { - let mut listeners = statics::listeners().lock().unwrap(); - for handle in &self.handles { - listeners.remove(handle); - } - drop(listeners); - - let mut sockets = statics::sockets().lock().unwrap(); - for handle in &self.handles { - sockets.remove(handle); - } - } - } - - fn young_gc_root() -> i64 { - perry_runtime::arena::arena_alloc_gc(32, 8, perry_runtime::gc::GC_TYPE_STRING) as i64 - } - - fn assert_rewritten(before: i64, after: i64) { - assert_ne!(after, before); - assert!(perry_runtime::arena::pointer_in_nursery(after as usize)); - } - - #[test] - fn gc_mutable_scanner_rewrites_listener_roots() { - let _guard = GcTestGuard::new(); - perry_ffi::gc_register_mutable_root_scanner_named("perry-ext-net", scan_net_roots); - - let socket_id = -9_001; - let _cleanup = NetHandleCleanup::new(vec![socket_id]); - let callback = young_gc_root(); - { - let mut listeners = statics::listeners().lock().unwrap(); - listeners - .entry(socket_id) - .or_default() - .entry("data".to_string()) - .or_default() - .push(callback); - } - - let _ = perry_runtime::gc::gc_collect_minor(); - - let after = { - let listeners = statics::listeners().lock().unwrap(); - listeners - .get(&socket_id) - .and_then(|per_socket| per_socket.get("data")) - .and_then(|callbacks| callbacks.first()) - .copied() - }; - statics::listeners().lock().unwrap().remove(&socket_id); - assert_rewritten( - callback, - after.expect("listener callback should remain registered"), - ); - } - - /// Issuing two `js_net_socket_alloc()` calls must not panic and must - /// register the GC scanner exactly once. Both handles should be - /// distinct positive integers. - #[test] - fn alloc_is_idempotent() { - let _lock = GC_TEST_LOCK - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let h1 = unsafe { js_net_socket_alloc() }; - let h2 = unsafe { js_net_socket_alloc() }; - let _cleanup = NetHandleCleanup::new(vec![h1, h2]); - assert!(h1 > 0); - assert!(h2 > 0); - assert_ne!(h1, h2); - assert!(is_net_socket_handle(h1)); - assert!(is_net_socket_handle(h2)); - } - - /// `js_net_has_pending()` returns 0 when no sockets are registered - /// and no events are pending — the loop-keepalive baseline. - /// - /// We can't truly assert "no sockets registered" because earlier - /// tests in the same process leave entries behind (the registry is - /// process-wide). Instead, allocate a socket, drop it via the close - /// path, and check that has_pending eventually returns to 0. - #[test] - fn has_pending_false_when_idle() { - let _lock = GC_TEST_LOCK - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - // Drain any leftover events from sibling tests. - let _ = unsafe { js_net_process_pending() }; - // Snapshot: with no real connection in flight, has_pending may - // still return 1 because of the alloc-test sockets above leaving - // handles in the registry. The contract documented here is that - // it returns *some* non-negative integer without crashing. - let v = js_net_has_pending(); - assert!(v == 0 || v == 1, "has_pending must be 0 or 1, got {}", v); - } - - /// Listener registration round-trip: `.on('data', cb)` stores the - /// callback pointer in the per-socket listener map. We use a non-zero - /// sentinel so we never try to invoke it. - #[test] - fn listener_registration_round_trip() { - let _lock = GC_TEST_LOCK - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let h = unsafe { js_net_socket_alloc() }; - let _cleanup = NetHandleCleanup::new(vec![h]); - let event = alloc_string("data"); - unsafe { - js_net_socket_on(h, event.as_raw() as i64, 0xDEADBEEF_i64); - js_net_socket_on(h, event.as_raw() as i64, 0xCAFEBABE_i64); - } - let cbs = listeners_for(h, "data"); - assert_eq!(cbs.len(), 2); - assert_eq!(cbs[0], 0xDEADBEEF_i64); - assert_eq!(cbs[1], 0xCAFEBABE_i64); - } -} +mod tests; diff --git a/crates/perry-ext-net/src/tests.rs b/crates/perry-ext-net/src/tests.rs new file mode 100644 index 0000000000..373856e77b --- /dev/null +++ b/crates/perry-ext-net/src/tests.rs @@ -0,0 +1,157 @@ +use super::*; +use std::sync::{Mutex, MutexGuard}; + +static GC_TEST_LOCK: Mutex<()> = Mutex::new(()); + +struct GcTestGuard { + frame: u64, + _lock: MutexGuard<'static, ()>, +} + +impl GcTestGuard { + fn new() -> Self { + let lock = GC_TEST_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + perry_runtime::gc::js_gc_write_barriers_emitted(1); + let frame = perry_runtime::gc::js_shadow_frame_push(0); + Self { frame, _lock: lock } + } +} + +impl Drop for GcTestGuard { + fn drop(&mut self) { + perry_runtime::gc::js_shadow_frame_pop(self.frame); + perry_runtime::gc::js_gc_write_barriers_emitted(0); + } +} + +struct NetHandleCleanup { + handles: Vec, +} + +impl NetHandleCleanup { + fn new(handles: Vec) -> Self { + Self { handles } + } +} + +impl Drop for NetHandleCleanup { + fn drop(&mut self) { + let mut listeners = statics::listeners().lock().unwrap(); + for handle in &self.handles { + listeners.remove(handle); + } + drop(listeners); + + let mut sockets = statics::sockets().lock().unwrap(); + for handle in &self.handles { + sockets.remove(handle); + } + } +} + +fn young_gc_root() -> i64 { + perry_runtime::arena::arena_alloc_gc(32, 8, perry_runtime::gc::GC_TYPE_STRING) as i64 +} + +fn assert_rewritten(before: i64, after: i64) { + assert_ne!(after, before); + assert!(perry_runtime::arena::pointer_in_nursery(after as usize)); +} + +#[test] +fn gc_mutable_scanner_rewrites_listener_roots() { + let _guard = GcTestGuard::new(); + perry_ffi::gc_register_mutable_root_scanner_named("perry-ext-net", scan_net_roots); + + let socket_id = -9_001; + let _cleanup = NetHandleCleanup::new(vec![socket_id]); + let callback = young_gc_root(); + { + let mut listeners = statics::listeners().lock().unwrap(); + listeners + .entry(socket_id) + .or_default() + .entry("data".to_string()) + .or_default() + .push(callback); + } + + let _ = perry_runtime::gc::gc_collect_minor(); + + let after = { + let listeners = statics::listeners().lock().unwrap(); + listeners + .get(&socket_id) + .and_then(|per_socket| per_socket.get("data")) + .and_then(|callbacks| callbacks.first()) + .copied() + }; + statics::listeners().lock().unwrap().remove(&socket_id); + assert_rewritten( + callback, + after.expect("listener callback should remain registered"), + ); +} + +/// Issuing two `js_net_socket_alloc()` calls must not panic and must +/// register the GC scanner exactly once. Both handles should be +/// distinct positive integers. +#[test] +fn alloc_is_idempotent() { + let _lock = GC_TEST_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let h1 = unsafe { js_net_socket_alloc() }; + let h2 = unsafe { js_net_socket_alloc() }; + let _cleanup = NetHandleCleanup::new(vec![h1, h2]); + assert!(h1 > 0); + assert!(h2 > 0); + assert_ne!(h1, h2); + assert!(is_net_socket_handle(h1)); + assert!(is_net_socket_handle(h2)); +} + +/// `js_net_has_pending()` returns 0 when no sockets are registered +/// and no events are pending — the loop-keepalive baseline. +/// +/// We can't truly assert "no sockets registered" because earlier +/// tests in the same process leave entries behind (the registry is +/// process-wide). Instead, allocate a socket, drop it via the close +/// path, and check that has_pending eventually returns to 0. +#[test] +fn has_pending_false_when_idle() { + let _lock = GC_TEST_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Drain any leftover events from sibling tests. + let _ = unsafe { js_net_process_pending() }; + // Snapshot: with no real connection in flight, has_pending may + // still return 1 because of the alloc-test sockets above leaving + // handles in the registry. The contract documented here is that + // it returns *some* non-negative integer without crashing. + let v = js_net_has_pending(); + assert!(v == 0 || v == 1, "has_pending must be 0 or 1, got {}", v); +} + +/// Listener registration round-trip: `.on('data', cb)` stores the +/// callback pointer in the per-socket listener map. We use a non-zero +/// sentinel so we never try to invoke it. +#[test] +fn listener_registration_round_trip() { + let _lock = GC_TEST_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let h = unsafe { js_net_socket_alloc() }; + let _cleanup = NetHandleCleanup::new(vec![h]); + let event = alloc_string("data"); + unsafe { + js_net_socket_on(h, event.as_raw() as i64, 0xDEADBEEF_i64); + js_net_socket_on(h, event.as_raw() as i64, 0xCAFEBABE_i64); + } + let cbs = listeners_for(h, "data"); + assert_eq!(cbs.len(), 2); + assert_eq!(cbs[0], 0xDEADBEEF_i64); + assert_eq!(cbs[1], 0xCAFEBABE_i64); +} diff --git a/crates/perry-ext-net/src/transport.rs b/crates/perry-ext-net/src/transport.rs new file mode 100644 index 0000000000..6d1897bf36 --- /dev/null +++ b/crates/perry-ext-net/src/transport.rs @@ -0,0 +1,78 @@ +//! The `Transport` enum — a `net.Socket`'s underlying stream, either plain +//! TCP or a rustls-wrapped TLS stream, swappable at runtime by +//! `socket.upgradeToTLS`. Split out of `lib.rs` to keep that file under the +//! 2000-line CI gate; the logic is unchanged. + +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::TcpStream; +use tokio_rustls::client::TlsStream; + +pub(crate) enum Transport { + Plain(TcpStream), + Tls(Box>), +} + +impl Transport { + /// Set `TCP_NODELAY` on the underlying TCP socket. For a TLS transport the + /// option lives on the wrapped TCP stream (`get_ref().0`), so reach through + /// the rustls wrapper to the kernel socket. Matches Node's `socket.setNoDelay`, + /// which toggles Nagle's algorithm on the raw connection regardless of TLS. + pub(crate) fn set_nodelay(&self, nodelay: bool) -> io::Result<()> { + match self { + Transport::Plain(s) => s.set_nodelay(nodelay), + Transport::Tls(s) => s.get_ref().0.set_nodelay(nodelay), + } + } + + /// Read the current `TCP_NODELAY` state off the underlying socket. + /// Test-only observability seam for the nodelay command-path test. + #[cfg(test)] + pub(crate) fn nodelay(&self) -> io::Result { + match self { + Transport::Plain(s) => s.nodelay(), + Transport::Tls(s) => s.get_ref().0.nodelay(), + } + } +} + +impl AsyncRead for Transport { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + Transport::Plain(s) => Pin::new(s).poll_read(cx, buf), + Transport::Tls(s) => Pin::new(&mut **s).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for Transport { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.get_mut() { + Transport::Plain(s) => Pin::new(s).poll_write(cx, buf), + Transport::Tls(s) => Pin::new(&mut **s).poll_write(cx, buf), + } + } + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + Transport::Plain(s) => Pin::new(s).poll_flush(cx), + Transport::Tls(s) => Pin::new(&mut **s).poll_flush(cx), + } + } + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + Transport::Plain(s) => Pin::new(s).poll_shutdown(cx), + Transport::Tls(s) => Pin::new(&mut **s).poll_shutdown(cx), + } + } +} diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 14faff1e0c..bbb019570d 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -2,17 +2,21 @@ //! //! Extracted from `expr_call/mod.rs` as a mechanical move. -use crate::types::Type; use anyhow::Result; use swc_ecma_ast as ast; -use super::super::unimpl_hints; use super::reflect_args::{take_reflect_ktp_args, take_reflect_kvtp_args, take_reflect_tp_args}; use crate::ir::*; use super::super::{is_generator_call_expr, lower_expr, LoweringContext}; use super::os::user_info_expr_for_call; +mod buffer_statics; +mod imported_module_dispatch; +mod object_statics; +mod process_module; +mod reflect_statics; + fn path_submodule_name(module_name: &str) -> Option<&'static str> { match module_name.strip_prefix("node:").unwrap_or(module_name) { "path/posix" | "path.posix" => Some("posix"), @@ -309,7 +313,7 @@ pub(super) fn try_native_module_methods( ctx: &mut LoweringContext, call: &ast::CallExpr, expr: &ast::Expr, - args: Vec, + mut args: Vec, ) -> Result>> { // `Session` is a class export. A direct call is not construction, even // though the native constructor fast path handles `new Session()`. @@ -401,380 +405,10 @@ pub(super) fn try_native_module_methods( } } - // Check for process module methods. `import processModule from - // "node:process"` registers as the native `process` object, while - // `import * as processNamespace` registers as `process.namespace`; - // both must use the same strict method gate as the global object. - let process_name_is_shadowed = - obj_name == "process" && ctx.shadows_unqualified_global("process"); - let is_process_ref = !process_name_is_shadowed - && (obj_name == "process" - || ctx.lookup_builtin_module_alias(&obj_name) == Some("process") - || matches!( - ctx.lookup_native_module(&obj_name), - Some(("process", _)) | Some(("process.namespace", _)) - )); - if is_process_ref { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.as_ref(); - match method_name { - "uptime" => return Ok(Ok(Expr::ProcessUptime)), - "cwd" => return Ok(Ok(Expr::ProcessCwd)), - "memoryUsage" => return Ok(Ok(Expr::ProcessMemoryUsage)), - "nextTick" => { - if !args.is_empty() { - let mut iter = args.into_iter(); - let callback = iter.next().unwrap(); - let trailing: Vec = iter.collect(); - return Ok(Ok(Expr::ProcessNextTick { - callback: Box::new(callback), - args: trailing, - })); - } - } - "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "emit" - | "listeners" - | "rawListeners" - | "eventNames" - | "listenerCount" - | "removeListener" - | "off" - | "removeAllListeners" - | "setMaxListeners" - | "getMaxListeners" => { - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: method_name.to_string(), - args, - })); - } - "chdir" => { - if !args.is_empty() { - return Ok(Ok(Expr::ProcessChdir(Box::new( - args.into_iter().next().unwrap(), - )))); - } - } - "kill" => { - if !args.is_empty() { - let mut iter = args.into_iter(); - let pid = iter.next().unwrap(); - let signal = iter.next().map(Box::new); - return Ok(Ok(Expr::ProcessKill { - pid: Box::new(pid), - signal, - })); - } - } - "ref" | "unref" => { - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: method_name.to_string(), - args, - })); - } - method_name if is_process_active_array_helper(method_name) => { - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: method_name.to_string(), - args, - })); - } - "setSourceMapsEnabled" => { - // #1400 / #3108: process.setSourceMapsEnabled(bool) - // toggles the live source-map flag. Perry compiles - // AOT and has no resolver, so the flag drives - // nothing observable — but it round-trips through - // process.sourceMapsEnabled and validates that the - // argument is a boolean (else ERR_INVALID_ARG_TYPE), - // matching Node. Lower to the runtime setter, - // passing the original argument for validation. - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: "setSourceMapsEnabled".to_string(), - args, - })); - } - "getBuiltinModule" => { - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: "getBuiltinModule".to_string(), - args, - })); - } - "execve" => { - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: "execve".to_string(), - args, - })); - } - "dlopen" => { - // #1409: process.dlopen(module, filename, flags?) - // is Node's native-addon (.node) loader. Perry - // statically links every dependency at compile - // time — there's no dynamic loader to call. - // Returning undefined is the closest no-op: - // call sites that probe for the function before - // attempting to load an addon (a common pattern - // in optional-dep wrappers) see typeof "function" - // and a "loaded" non-error, then fall back to - // their pure-JS path. Real addon-loading - // attempts will surface as the addon's exports - // being undefined downstream. - return Ok(Ok(Expr::Undefined)); - } - "hasUncaughtExceptionCaptureCallback" => { - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: "hasUncaughtExceptionCaptureCallback".to_string(), - args, - })); - } - "setUncaughtExceptionCaptureCallback" - | "addUncaughtExceptionCaptureCallback" => { - let method_name = method_ident.sym.as_ref().to_string(); - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: method_name, - args, - })); - } - "loadEnvFile" => { - // #1399 / #2135: process.loadEnvFile(path?) - // (Node 20.12+) reads a `.env` file from disk and - // merges its KEY=value entries into `process.env`. - // Previously a no-op because `process.env.X = v` - // didn't persist; #1344 has since wired writes - // through `std::env::set_var`, so we lower to a - // runtime call that actually reads the file. - // Keep the original JS value: the runtime handles - // omitted/undefined/null defaulting plus Buffer - // and file-URL path objects. - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: "loadEnvFile".to_string(), - args, - })); - } - "exit" => { - // process.exit() / process.exit(code) — never - // returns, terminates the process. Until now this - // fell through to generic NativeMethodCall which - // silently no-op'd, so scripts that rely on it to - // end the event loop (e.g. `main().then(() => - // process.exit(0))` in a net-socket driver) would - // hang with the socket still keeping the loop alive. - let code = if !args.is_empty() { - Some(Box::new(args.into_iter().next().unwrap())) - } else { - None - }; - return Ok(Ok(Expr::ProcessExit(code))); - } - "abort" => { - // process.abort() — raises SIGABRT, no clean - // shutdown. Maps to libc::abort() at runtime. - return Ok(Ok(Expr::ProcessAbort)); - } - "umask" => { - // process.umask(mask?) — returns the current - // file-mode creation mask, optionally setting - // a new one first and returning the previous. - let mask = if !args.is_empty() { - Some(Box::new(args.into_iter().next().unwrap())) - } else { - None - }; - return Ok(Ok(Expr::ProcessUmask(mask))); - } - "threadCpuUsage" => { - // process.threadCpuUsage(prior?) — CPU time used - // by the current thread, as { user, system } in - // microseconds. If prior is given, returns the - // validated delta. - let prior = if !args.is_empty() { - Some(Box::new(args.into_iter().next().unwrap())) - } else { - None - }; - return Ok(Ok(Expr::ProcessThreadCpuUsage(prior))); - } - "availableMemory" => { - // process.availableMemory() — free system memory - // available to the process, in bytes. - return Ok(Ok(Expr::ProcessAvailableMemory)); - } - "constrainedMemory" => { - // process.constrainedMemory() — OS-imposed memory - // limit (cgroups/container), in bytes. 0 when no - // limit applies. - return Ok(Ok(Expr::ProcessConstrainedMemory)); - } - // POSIX credential accessors (#1408). All four delegate - // to libc::{getuid,geteuid,getgid,getegid}() at runtime. - "getuid" => { - return Ok(Ok(Expr::ProcessPosixCredential( - crate::ir::PosixCredentialKind::Uid, - ))); - } - "geteuid" => { - return Ok(Ok(Expr::ProcessPosixCredential( - crate::ir::PosixCredentialKind::Euid, - ))); - } - "getgid" => { - return Ok(Ok(Expr::ProcessPosixCredential( - crate::ir::PosixCredentialKind::Gid, - ))); - } - "getegid" => { - return Ok(Ok(Expr::ProcessPosixCredential( - crate::ir::PosixCredentialKind::Egid, - ))); - } - "getgroups" => { - // #2135: process.getgroups() — supplementary - // group IDs as a number array. Dispatch through - // the generic NativeMethodCall path; the - // node_core table row routes to - // `js_process_getgroups`. - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: "getgroups".to_string(), - args, - })); - } - // #2135: POSIX credential setters — single numeric - // ID arg, return undefined. Implemented as libc - // wrappers in the runtime (string-username form is - // a no-op today; see js_process_setuid for the - // out-of-scope note). - "setuid" | "seteuid" | "setgid" | "setegid" => { - let method_name = method_ident.sym.as_ref().to_string(); - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: method_name, - args, - })); - } - // #2135: process.setgroups(groups[]) takes an - // array of numeric GIDs; process.initgroups(user, - // extra_gid) takes a username string + numeric - // GID. The runtime decodes the JSValues itself, so - // both pass through the generic NativeMethodCall. - "setgroups" | "initgroups" => { - let method_name = method_ident.sym.as_ref().to_string(); - return Ok(Ok(Expr::NativeMethodCall { - module: "process".to_string(), - class_name: None, - object: None, - method: method_name, - args, - })); - } - "emitWarning" => { - // process.emitWarning(warning[, type, code, ctor]) - // — writes a formatted warning to stderr. Perry - // collapses the overloads into a positional Vec - // and lets the runtime do the formatting. - return Ok(Ok(Expr::ProcessEmitWarning(args))); - } - "cpuUsage" => { - // process.cpuUsage(prior?) — { user, system } in - // microseconds. If prior is given, returns the - // diff (clamped to >= 0). - let prior = if !args.is_empty() { - Some(Box::new(args.into_iter().next().unwrap())) - } else { - None - }; - return Ok(Ok(Expr::ProcessCpuUsage(prior))); - } - "resourceUsage" => { - return Ok(Ok(Expr::ProcessResourceUsage)); - } - "getActiveResourcesInfo" => { - return Ok(Ok(Expr::ProcessActiveResourcesInfo)); - } - "hrtime" => { - // process.hrtime(prior?) — [secs, nanos] from a - // monotonic clock. With prior, returns the diff. - // `process.hrtime.bigint()` is intercepted earlier. - let prior = if !args.is_empty() { - Some(Box::new(args.into_iter().next().unwrap())) - } else { - None - }; - return Ok(Ok(Expr::ProcessHrtime(prior))); - } - _ => { - let hint = unimpl_hints::module_member_hint("process", method_name) - .map(|h| format!(" {h}")) - .unwrap_or_default(); - let msg = format!( - "`process.{}` is not implemented in Perry — see `perry --print-api-manifest` for the supported surface, \ - or set `PERRY_ALLOW_UNIMPLEMENTED=1` to ignore. (#463){}", - method_name, hint, - ); - // #5245: default → throw-on-reach + notice; strict - // (`perry.strict` / `--strict-unimplemented`) → hard - // #463 refusal. Tree-shake deferral handled inside. - let api = format!("process.{method_name}"); - let location = crate::eval_classifier::location_string( - &ctx.source_file_path, - member.span.lo.0, - ); - match crate::check_unimplemented_api( - &msg, - &api, - &location, - member.span.lo.0, - ) { - crate::UnimplementedDecision::Refuse => { - crate::lower_bail!(member.span, "{}", msg); - } - crate::UnimplementedDecision::DeferToRuntimeError(runtime_msg) => { - return Ok(Ok( - super::super::const_fold_fn::synth_deferred_throw_value( - ctx, - &runtime_msg, - member.span, - )?, - )); - } - } - } - } - } - } + args = match process_module::try_process_module_methods(ctx, member, &obj_name, args)? { + Ok(expr) => return Ok(Ok(expr)), + Err(rest) => rest, + }; // Check for tty module methods (#347 Phase 3) let is_tty_module = @@ -832,480 +466,17 @@ pub(super) fn try_native_module_methods( } } - // Check for Buffer static methods. Issue #831: aliased - // imports of Buffer (`import { Buffer as RuntimeBuffer } from - // "node:buffer"`) must route through the same dedicated - // BufferFrom/BufferAlloc/etc HIR variants as the global - // `Buffer`; otherwise the lowering falls through to a - // receiver-less `NativeMethodCall { module: "buffer", method: - // "from", object: None }` for which the codegen has no - // dispatch table entry — it would silently return - // TAG_UNDEFINED, and any caller that subsequently treats the - // result as a Buffer (e.g. `b[0]` → Uint8ArrayGet) segfaults - // on the undefined value. - let is_buffer_ref = obj_name == "Buffer" - || matches!( - ctx.lookup_native_module(&obj_name), - Some(("buffer", Some("Buffer"))) - ); - if is_buffer_ref { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.as_ref(); - match method_name { - "from" => { - let data = args.first().cloned().unwrap_or(Expr::Undefined); - // Disambiguate `Buffer.from(data, encoding?)` vs - // `Buffer.from(arrayBuffer, byteOffset?, length?)`. - // Encoding args are strings, byteOffset/length are - // numbers. Issue #1273: previously any non-string - // literal second arg routed to BufferFromArrayBuffer, - // so `Buffer.from(str, encVar)` produced an empty - // buffer. Now: 3+ args, or a Number-literal second - // arg, or a string-literal first arg with a Number - // second arg → ArrayBuffer form. Otherwise default - // to BufferFrom (the runtime helper dispatches on - // the actual type of `data`, and routes through - // `js_encoding_tag_from_value` for runtime-string - // encodings). - let is_arraybuffer_form = args.len() >= 3 - || matches!(args.get(1), Some(Expr::Number(_) | Expr::Integer(_))); - if args.len() >= 2 && is_arraybuffer_form { - let byte_offset = args.get(1).cloned().unwrap_or(Expr::Number(0.0)); - let length = args.get(2).cloned().map(Box::new); - return Ok(Ok(Expr::BufferFromArrayBuffer { - data: Box::new(data), - byte_offset: Box::new(byte_offset), - length, - })); - } - let encoding = args.get(1).cloned().map(Box::new); - return Ok(Ok(Expr::BufferFrom { - data: Box::new(data), - encoding, - })); - } - "alloc" => { - // #2013: a missing `size` must surface Node's - // `ERR_INVALID_ARG_TYPE` (Received undefined), so - // default to `undefined` — not `0` — and let the - // runtime validator throw. - let size = args.first().cloned().unwrap_or(Expr::Undefined); - let fill = args.get(1).cloned().map(Box::new); - let encoding = args.get(2).cloned().map(Box::new); - return Ok(Ok(Expr::BufferAlloc { - size: Box::new(size), - fill, - encoding, - })); - } - "allocUnsafe" | "allocUnsafeSlow" => { - // #2013: missing `size` → Node ERR_INVALID_ARG_TYPE. - let size = args.first().cloned().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::BufferAllocUnsafe(Box::new(size)))); - } - "concat" => { - let list = args.first().cloned().unwrap_or(Expr::Array(vec![])); - if let Some(total_length) = args.get(1).cloned() { - return Ok(Ok(Expr::BufferConcatWithLength { - list: Box::new(list), - total_length: Box::new(total_length), - })); - } - return Ok(Ok(Expr::BufferConcat(Box::new(list)))); - } - "copyBytesFrom" => { - return Ok(Ok(Expr::NativeMethodCall { - module: "buffer".to_string(), - class_name: None, - object: None, - method: "copyBytesFrom".to_string(), - args, - })); - } - "of" => { - return Ok(Ok(Expr::BufferFrom { - data: Box::new(Expr::Array(args)), - encoding: None, - })); - } - "isBuffer" => { - let obj = args.first().cloned().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::BufferIsBuffer(Box::new(obj)))); - } - "isEncoding" => { - let obj = args.first().cloned().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::BufferIsEncoding(Box::new(obj)))); - } - "byteLength" => { - let data = args - .first() - .cloned() - .unwrap_or(Expr::String("".to_string())); - let encoding = args.get(1).cloned().map(Box::new); - return Ok(Ok(Expr::BufferByteLength { - data: Box::new(data), - encoding, - })); - } - // `Buffer.compare(a, b)` → `a.compare(b)` instance call - // (handled by runtime buffer dispatch). - "compare" if args.len() >= 2 => { - let mut iter = args.into_iter(); - let a = iter.next().unwrap(); - let b = iter.next().unwrap(); - return Ok(Ok(Expr::Call { - callee: Box::new(Expr::PropertyGet { - byte_offset: 0, - object: Box::new(a), - property: "compare".to_string(), - }), - args: vec![b], - type_args: vec![], - byte_offset: 0, - })); - } - _ => {} // Fall through to generic handling - } - } - } - - // Check for Uint8Array static methods - if obj_name == "Uint8Array" { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.as_ref(); - match method_name { - "from" => { - let data = args.first().cloned().unwrap_or(Expr::Undefined); - // #2774: `Uint8Array.from(src, mapFn, thisArg?)` — run - // the mapped materialization first (which validates - // mapFn + binds thisArg), then truncate to Uint8. - if let Some(map_fn) = args.get(1).cloned() { - let this_arg = args.get(2).cloned().map(Box::new); - return Ok(Ok(Expr::Uint8ArrayFrom(Box::new( - Expr::ArrayFromMapped { - iterable: Box::new(data), - map_fn: Box::new(map_fn), - this_arg, - }, - )))); - } - return Ok(Ok(Expr::Uint8ArrayFrom(Box::new(data)))); - } - // Issue #871 (part 2): `Uint8Array.of(a, b, c, ...)` — - // uuid's `sha1.js` calls this with 20 args (the SHA-1 - // hash output, byte by byte), which hit the - // `Call callee shape not supported (PropertyGet) with N args` - // bail in `crates/perry-codegen/src/lower_call.rs::~3226` - // because the receiver `Uint8Array` lowers to `GlobalGet(0)` - // (which `lower_expr` evaluates to `0.0`) so the closure-call - // fallback at `~3167` skipped past it, and there's no - // `js_closure_call17..20` to dispatch ≥17-arg calls anyway. - // - // Per ECMAScript: `Uint8Array.of(...items)` is `Uint8Array.from([...items])` - // — same shape as the existing `from` arm above, just wrap the - // varargs in an array literal first. Routes through `Expr::Array` - // → `Expr::Uint8ArrayFrom` which already lowers correctly for any - // arity (it's just `js_uint8array_from_array`-or-equivalent on the - // packed array). Mirrors the `Array.of` arm at :1618. - "of" => { - return Ok(Ok(Expr::Uint8ArrayFrom(Box::new(Expr::Array(args))))); - } - _ => {} // Fall through to generic handling - } - } - } + args = match buffer_statics::try_buffer_uint8array_statics( + ctx, member, &obj_name, args, + )? { + Ok(expr) => return Ok(Ok(expr)), + Err(rest) => rest, + }; - // Check for Object static methods. #6677: match BOTH the dot form - // and the string-literal computed form (`Object["keys"](...)`) so the - // computed key does not fall through to generic dispatch (→ - // `TypeError: value is not a function`). - if obj_name == "Object" { - if let Some(method_name) = super::static_call_prop_name(&member.prop) { - match method_name { - "keys" => { - let obj = args.first().cloned().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectKeys(Box::new(obj)))); - } - "values" => { - let obj = args.first().cloned().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectValues(Box::new(obj)))); - } - "entries" => { - let obj = args.first().cloned().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectEntries(Box::new(obj)))); - } - // Object.assign(target, ...sources) — per ECMAScript spec, this - // MUTATES target with each source's own enumerable string-keyed - // and Symbol-keyed properties, and RETURNS target (preserving - // identity, class_id, and the SYMBOL_PROPERTIES side-table). - // Refs #590: the previous lowering folded the call into - // ObjectSpread which allocates a fresh object — that breaks - // `result === target` and orphans target's symbol-keyed - // properties since the side table is keyed by raw pointer. - // - "assign" => { - let mut iter = args.into_iter(); - let target = iter.next().unwrap_or(Expr::Undefined); - let sources: Vec = iter.collect(); - // Real `Object.assign(target, ...sources)` — mutate target. - return Ok(Ok(Expr::ObjectAssign { - target: Box::new(target), - sources, - })); - } - "fromEntries" => { - let entries = args.into_iter().next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectFromEntries(Box::new(entries)))); - } - "groupBy" - // Object.groupBy(items, keyFn) — Node 22+ static method - if args.len() >= 2 => { - let mut iter = args.into_iter(); - let items = iter.next().unwrap(); - let key_fn = iter.next().unwrap(); - let key_fn = ctx.maybe_wrap_builtin_callback(key_fn, &call.args[1]); - return Ok(Ok(Expr::ObjectGroupBy { - items: Box::new(items), - key_fn: Box::new(key_fn), - })); - } - "is" => { - let mut iter = args.into_iter(); - let a = iter.next().unwrap_or(Expr::Undefined); - let b = iter.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectIs(Box::new(a), Box::new(b)))); - } - "hasOwn" => { - let mut iter = args.into_iter(); - let obj = iter.next().unwrap_or(Expr::Undefined); - let key = iter.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectHasOwn(Box::new(obj), Box::new(key)))); - } - "freeze" => { - return Ok(Ok(Expr::ObjectFreeze(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "seal" => { - return Ok(Ok(Expr::ObjectSeal(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "preventExtensions" => { - return Ok(Ok(Expr::ObjectPreventExtensions(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "create" => { - let mut it = args.into_iter(); - let proto = it.next().unwrap_or(Expr::Undefined); - let props = it.next().map(Box::new); - return Ok(Ok(Expr::ObjectCreate(Box::new(proto), props))); - } - "isFrozen" => { - return Ok(Ok(Expr::ObjectIsFrozen(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "isSealed" => { - return Ok(Ok(Expr::ObjectIsSealed(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "isExtensible" => { - return Ok(Ok(Expr::ObjectIsExtensible(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "getPrototypeOf" => { - return Ok(Ok(Expr::ObjectGetPrototypeOf(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "setPrototypeOf" => { - // `Object.setPrototypeOf(obj, proto)` is the foundation - // of chalk's "callable + getter-bag" shape (a closure has - // its `[[Prototype]]` reset to a Function-derived - // accessor-bag). Pre-fix this fell through to a generic - // `Object.setPrototypeOf` PropertyGet → Call where - // `Object.setPrototypeOf` resolves to undefined and the - // call throws `TypeError: value is not a function` — - // chalk's `import chalk from "chalk"` died at module init. - // - // Perry's runtime doesn't track mutable per-instance - // prototype chains (class IDs are baked at allocation), - // so we model setPrototypeOf as a no-op that still - // returns the target — matching the spec's "return obj" - // contract. The runtime helper registers (obj, proto) - // in a side-table that `Object.getPrototypeOf(obj)` is - // free to consult later if a downstream pattern needs it. - let mut iter = args.into_iter(); - let obj = iter.next().unwrap_or(Expr::Undefined); - let proto = iter.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectSetPrototypeOf( - Box::new(obj), - Box::new(proto), - ))); - } - "defineProperty" => { - let mut iter = args.into_iter(); - let obj = iter.next().unwrap_or(Expr::Undefined); - let key = iter.next().unwrap_or(Expr::Undefined); - let descriptor = iter.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectDefineProperty( - Box::new(obj), - Box::new(key), - Box::new(descriptor), - ))); - } - "defineProperties" => { - // `Object.defineProperties(target, descriptors)` — bulk - // form of `defineProperty`. Used by chalk's index.js to - // attach the `styles` getter-bag onto - // `createChalk.prototype`. Pre-fix this fell through to a - // generic `(Object).defineProperties(...)` call which - // throws `TypeError: value is not a function` at module - // init because `Object` isn't a runtime object with - // method dispatch. - // - // Desugar to a sequence of `ObjectDefineProperty` - // applications by reading `descriptors`'s own keys at - // compile time when it's an object literal, otherwise - // route through a runtime helper that iterates the - // descriptor object's keys. - let mut iter = args.into_iter(); - let target = iter.next().unwrap_or(Expr::Undefined); - let descs = iter.next().unwrap_or(Expr::Undefined); - if let Expr::Object(props) = &descs { - // Static descriptor literal — desugar to a Sequence - // of `defineProperty(target, key, desc)` calls and - // yield `target` as the result value. - // - // An EMPTY literal must NOT fold to a bare `target`: - // `Object.defineProperties(O, {})` still performs the - // spec's step-1 `If Type(O) is not Object, throw a - // TypeError`, so `Object.defineProperties(undefined, - // {})` must throw. With no keys there is no per-key - // `defineProperty` to enforce that, so route the - // empty case through the runtime helper (which - // validates the target). - if !props.is_empty() { - let target = target; - let mut exprs: Vec = Vec::with_capacity(props.len() + 1); - for (key_name, desc_expr) in props { - exprs.push(Expr::ObjectDefineProperty( - Box::new(target.clone()), - Box::new(Expr::String(key_name.clone())), - Box::new(desc_expr.clone()), - )); - } - exprs.push(target); - return Ok(Ok(Expr::Sequence(exprs))); - } - } - return Ok(Ok(Expr::ObjectDefineProperties( - Box::new(target), - Box::new(descs), - ))); - } - "getOwnPropertyDescriptor" => { - // #2144/#3655: built-in function `.name` / - // `.length` descriptors. - // - // `Object.getOwnPropertyDescriptor(, - // "name"|"length")` and - // `…(., "name"|"length")` - // need a compile-time fold because those builtin - // values are often intrinsic sentinels rather than - // first-class closures. Per spec both descriptors - // are non-writable, non-enumerable, configurable - // data properties. Fold when we can statically - // recognize the receiver shape — same gating logic - // as the direct `.name` / `.length` folds in - // `expr_member.rs`. - if call.args.len() >= 2 && args.len() >= 2 { - let key_name = match call.args[1].expr.as_ref() { - ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str(), - _ => None, - }; - if matches!(key_name, Some("name" | "length")) { - let lowered_obj_is_global_intrinsic = match &args[0] { - Expr::GlobalGet(0) => true, - Expr::PropertyGet { object: inner, .. } => { - matches!(inner.as_ref(), Expr::GlobalGet(0)) - } - _ => false, - }; - if lowered_obj_is_global_intrinsic { - match key_name { - Some("name") => { - let folded = - super::name_fold::builtin_fn_name_for_arg( - call.args[0].expr.as_ref(), - ); - if let Some(fname) = folded { - return Ok(Ok( - super::name_fold::name_data_descriptor( - fname, - ), - )); - } - } - Some("length") => { - let folded = - super::name_fold::builtin_fn_length_for_arg( - call.args[0].expr.as_ref(), - ) - .or_else(|| { - super::name_fold::builtin_fn_name_for_arg( - call.args[0].expr.as_ref(), - ) - .and_then(|name| { - crate::analysis::builtin_constructor_length( - &name, - ) - }) - }); - if let Some(len) = folded { - return Ok(Ok( - super::name_fold::builtin_data_descriptor( - Expr::Number(len as f64), - ), - )); - } - } - _ => {} - } - } - } - } - let mut iter = args.into_iter(); - let obj = iter.next().unwrap_or(Expr::Undefined); - let key = iter.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ObjectGetOwnPropertyDescriptor( - Box::new(obj), - Box::new(key), - ))); - } - "getOwnPropertyDescriptors" => { - return Ok(Ok(Expr::ObjectGetOwnPropertyDescriptors(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "getOwnPropertyNames" => { - return Ok(Ok(Expr::ObjectGetOwnPropertyNames(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - "getOwnPropertySymbols" => { - return Ok(Ok(Expr::ObjectGetOwnPropertySymbols(Box::new( - args.into_iter().next().unwrap_or(Expr::Undefined), - )))); - } - _ => {} // Fall through to generic handling - } - } - } + args = match object_statics::try_object_statics(ctx, call, member, &obj_name, args)? { + Ok(expr) => return Ok(Ok(expr)), + Err(rest) => rest, + }; // Check for Symbol static methods: Symbol.for / Symbol.keyFor. // Accept BOTH the dot form (`Symbol.for(...)`) and the @@ -1365,245 +536,10 @@ pub(super) fn try_native_module_methods( } } - if obj_name == "Reflect" { - // #6677: accept the string-literal computed form too. - if let Some(method_name) = super::static_call_prop_name(&member.prop) { - match method_name { - "get" => { - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let key = it.next().unwrap_or(Expr::Undefined); - // #2766: optional `receiver` (3rd arg) used as the - // `this` binding for accessor getters. Default to - // `undefined` — the runtime substitutes `target`. - let receiver = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectGet { - target: Box::new(target), - key: Box::new(key), - receiver: Box::new(receiver), - })); - } - "set" => { - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let key = it.next().unwrap_or(Expr::Undefined); - let value = it.next().unwrap_or(Expr::Undefined); - // Optional `receiver` (4th arg): default `undefined` - // and the runtime substitutes `target`. - let receiver = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectSet { - target: Box::new(target), - key: Box::new(key), - value: Box::new(value), - receiver: Box::new(receiver), - })); - } - "has" => { - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let key = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectHas { - target: Box::new(target), - key: Box::new(key), - })); - } - "deleteProperty" => { - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let key = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectDelete { - target: Box::new(target), - key: Box::new(key), - })); - } - "ownKeys" => { - let target = args.into_iter().next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectOwnKeys(Box::new(target)))); - } - "apply" => { - let mut it = args.into_iter(); - let func = it.next().unwrap_or(Expr::Undefined); - let this_arg = it.next().unwrap_or(Expr::Undefined); - // Spec sec-reflect.apply runs CreateListFromArrayLike - // on argumentsList, which throws a TypeError when - // Type(argumentsList) is not Object. An OMITTED - // argumentsList is `undefined` (not Object), so it - // must reach the runtime as `undefined` and throw — - // NOT default to an empty array, which would silently - // succeed with no args (test262 - // Reflect/apply/arguments-list-is-not-array-like - // `Reflect.apply(fn, null /* empty */)`). - let args_arr = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectApply { - func: Box::new(func), - this_arg: Box::new(this_arg), - args: Box::new(args_arr), - })); - } - "construct" => { - // Special case: `Reflect.construct(ClassName, [args...])` - // where ClassName is a known class — fold to a direct - // `new ClassName(...args)` expression. - // - // #2768: only fold the two-argument form. With an - // explicit `newTarget` (3rd arg) the result's prototype - // comes from `newTarget` and `newTarget` must be - // validated as a constructor — a plain `new ClassName` - // would silently drop both. Fall through to - // `ReflectConstruct` (runtime `js_reflect_construct`, - // which runs `js_new_function_construct_with_new_target` - // and the non-constructor `newTarget` TypeError check). - if call.args.len() == 2 { - if let ast::Expr::Ident(cls_ident) = call.args[0].expr.as_ref() { - let cls_name = cls_ident.sym.to_string(); - if ctx.lookup_class(&cls_name).is_some() { - if let ast::Expr::Array(arr_lit) = - call.args[1].expr.as_ref() - { - let new_args: Vec = arr_lit - .elems - .iter() - .filter_map(|e| e.as_ref()) - .map(|e| lower_expr(ctx, &e.expr)) - .collect::>>()?; - return Ok(Ok(Expr::New { - class_name: cls_name, - args: new_args, - type_args: vec![], - byte_offset: 0, - cap_args_appended: 0, - })); - } - } - } - } - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let args_arr = it.next().unwrap_or(Expr::Array(vec![])); - // 3rd arg = newTarget; defaults to `undefined` so the - // runtime falls back to the target/proxy itself. - let new_target = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectConstruct { - target: Box::new(target), - args: Box::new(args_arr), - new_target: Box::new(new_target), - })); - } - "defineProperty" => { - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let key = it.next().unwrap_or(Expr::Undefined); - let descriptor = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectDefineProperty { - target: Box::new(target), - key: Box::new(key), - descriptor: Box::new(descriptor), - })); - } - "getPrototypeOf" => { - let target = args.into_iter().next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectGetPrototypeOf(Box::new(target)))); - } - "getOwnPropertyDescriptor" => { - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let key = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectGetOwnPropertyDescriptor { - target: Box::new(target), - key: Box::new(key), - })); - } - "defineMetadata" => { - let (key, value, target, property_key) = take_reflect_kvtp_args(args); - return Ok(Ok(Expr::ReflectDefineMetadata { - key: Box::new(key), - value: Box::new(value), - target: Box::new(target), - property_key, - })); - } - "getMetadata" => { - let (key, target, property_key) = take_reflect_ktp_args(args); - return Ok(Ok(Expr::ReflectGetMetadata { - key: Box::new(key), - target: Box::new(target), - property_key, - })); - } - "getOwnMetadata" => { - let (key, target, property_key) = take_reflect_ktp_args(args); - return Ok(Ok(Expr::ReflectGetOwnMetadata { - key: Box::new(key), - target: Box::new(target), - property_key, - })); - } - "hasMetadata" => { - let (key, target, property_key) = take_reflect_ktp_args(args); - return Ok(Ok(Expr::ReflectHasMetadata { - key: Box::new(key), - target: Box::new(target), - property_key, - })); - } - "hasOwnMetadata" => { - let (key, target, property_key) = take_reflect_ktp_args(args); - return Ok(Ok(Expr::ReflectHasOwnMetadata { - key: Box::new(key), - target: Box::new(target), - property_key, - })); - } - "getMetadataKeys" => { - let (target, property_key) = take_reflect_tp_args(args); - return Ok(Ok(Expr::ReflectGetMetadataKeys { - target: Box::new(target), - property_key, - })); - } - "getOwnMetadataKeys" => { - let (target, property_key) = take_reflect_tp_args(args); - return Ok(Ok(Expr::ReflectGetOwnMetadataKeys { - target: Box::new(target), - property_key, - })); - } - "deleteMetadata" => { - let (key, target, property_key) = take_reflect_ktp_args(args); - return Ok(Ok(Expr::ReflectDeleteMetadata { - key: Box::new(key), - target: Box::new(target), - property_key, - })); - } - "setPrototypeOf" => { - // #2761: Reflect-specific — boolean result (false on - // rejected change) + TypeError on bad args, distinct - // from Object.setPrototypeOf (returns the object). - let mut it = args.into_iter(); - let target = it.next().unwrap_or(Expr::Undefined); - let proto = it.next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectSetPrototypeOf { - target: Box::new(target), - proto: Box::new(proto), - })); - } - "isExtensible" => { - // #2762: Reflect-specific semantics (boolean + - // TypeError on non-object), NOT Object.isExtensible. - let target = args.into_iter().next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectIsExtensible(Box::new(target)))); - } - "preventExtensions" => { - // #2762: Reflect-specific semantics (boolean + - // TypeError on non-object), NOT Object.preventExtensions. - let target = args.into_iter().next().unwrap_or(Expr::Undefined); - return Ok(Ok(Expr::ReflectPreventExtensions(Box::new(target)))); - } - _ => {} - } - } - } + args = match reflect_statics::try_reflect_statics(ctx, call, member, &obj_name, args)? { + Ok(expr) => return Ok(Ok(expr)), + Err(rest) => rest, + }; if obj_name == "Proxy" { if let ast::MemberProp::Ident(method_ident) = &member.prop { @@ -1691,278 +627,12 @@ pub(super) fn try_native_module_methods( } } - if let Some((module_name, imported_method)) = ctx.lookup_native_module(&obj_name) { - if module_name == "url" && imported_method == Some("URL") { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.as_ref(); - if method_name == "canParse" && !args.is_empty() { - let mut iter = args.into_iter(); - let input = iter.next().unwrap(); - if let Some(base) = iter.next() { - return Ok(Ok(Expr::UrlCanParseWithBase { - input: Box::new(input), - base: Box::new(base), - })); - } - return Ok(Ok(Expr::UrlCanParse(Box::new(input)))); - } - if method_name == "parse" && !args.is_empty() { - let mut iter = args.into_iter(); - let input = iter.next().unwrap(); - if let Some(base) = iter.next() { - return Ok(Ok(Expr::UrlParseWithBase { - input: Box::new(input), - base: Box::new(base), - })); - } - return Ok(Ok(Expr::UrlParse(Box::new(input)))); - } - } - } - - if let Some(submodule) = path_submodule_name(module_name) { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.to_string(); - return Ok( - match super::nested_namespace::dispatch_path_subnamespace( - submodule, - &method_name, - args, - ) { - Ok(expr) => Ok(expr), - Err(args) => Err(args), - }, - ); - } - } - - // Skip modules handled specifically below (path, fs, child_process, etc.) - // `net` used to be in this list back when its method calls - // were short-circuited into `Expr::NetCreateConnection` etc. - // After A1.5 `net` goes through the generic NativeMethodCall - // path so the LLVM backend's NATIVE_MODULE_TABLE dispatches - // to `js_net_socket_*` in perry-stdlib. - let is_handled_module = module_name == "path" - || module_name == "node:path" - || module_name == "fs" - || module_name == "node:fs" - || module_name == "child_process" - || module_name == "node:child_process" - || module_name == "crypto" - || module_name == "node:crypto" - || module_name == "os" - || module_name == "node:os"; - if !is_handled_module { - // This is a call on a native module (e.g., mysql.createConnection) - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.to_string(); - // A destructured/named DATA member of a native module - // (`const { METHODS } = require('node:http')` → - // `imported_method = Some("METHODS")`) holds a real - // Array/Object value. An Array/Object prototype method on it - // (`METHODS.map(...)`, `STATUS_CODES.hasOwnProperty(...)`) is - // a call on that VALUE — NOT a `module.method` native call. - // Bail to generic dynamic dispatch so it runs on the member's - // resolved value (express's `router` does - // `METHODS.map((m) => m.toLowerCase())`). Without this the - // call lowered to `NativeMethodCall { module: "http", method: - // "map" }`, which returned undefined / a deferred throw. - if imported_method.is_some() - && (super::super::array_fold::is_known_array_prototype_method( - &method_name, - ) || super::super::array_fold::is_known_object_prototype_method( - &method_name, - )) - { - return Ok(Err(args)); - } - if module_name == "worker_threads" && method_name == "workerData" { - return Ok(Err(args)); - } - if module_name.strip_prefix("node:").unwrap_or(module_name) == "vm" - && imported_method.is_none() - && method_name == "Module" - { - let mut exprs = args; - exprs.push(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_vm_module_call".to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: Vec::new(), - type_args: Vec::new(), - byte_offset: 0, - }); - return Ok(Ok(Expr::Sequence(exprs))); - } - let normalized_module = - module_name.strip_prefix("node:").unwrap_or(module_name); - if normalized_module == "cluster" - && matches!(imported_method, Some("default")) - && is_cluster_default_event_emitter_method(&method_name) - { - return Ok(Ok(Expr::NativeMethodCall { - module: module_name.to_string(), - class_name: None, - object: None, - method: method_name, - args, - })); - } - if method_name == "call" { - if normalized_module == "stream" - && matches!(imported_method, None | Some("Stream")) - { - return Ok(Ok(event_emitter_constructor_call(args))); - } - if normalized_module == "events" - && matches!(imported_method, Some("EventEmitter")) - { - return Ok(Ok(event_emitter_constructor_call(args))); - } - // #4973: named-import form of the inherits - // pattern — `const { Server } = require('http'); - // Server.call(this, handler)`. Same extern as - // the dotted `http.Server.call(...)` form in - // module_class_static.rs. - if matches!(normalized_module, "http" | "https") - && matches!(imported_method, Some("Server")) - && !args.is_empty() - { - let mut it = args.into_iter(); - let this_arg = it.next().unwrap(); - let mut rest: Vec = it.collect(); - rest.resize(2, Expr::Undefined); - let mut call_args = vec![this_arg]; - call_args.extend(rest); - let extern_name = if normalized_module == "https" { - "js_https_server_construct_with_this" - } else { - "js_http_server_construct_with_this" - }; - return Ok(Ok(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: extern_name.to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: call_args, - type_args: Vec::new(), - byte_offset: 0, - })); - } - } - // Unimplemented-API gate (#463 / #525) for the 2-deep - // `mod.method()` call form. Without this, perry/* and - // other native-module call sites short-circuited past - // the `lower_member` gate that fires for the property- - // read form, then bailed in codegen with a per-module - // message (`'X' is not a known function`) — different - // wording, different escape hatch, harder for users to - // recognize as the same class of mistake. Mirrors the - // 3-deep gate above for `mod.X.Y()`. - let manifest_entry = - perry_api_manifest::module_has_symbol(module_name, &method_name); - if perry_api_manifest::module_has_any_entries(module_name) - && manifest_entry.is_none() - // #wall4: an unmistakable `String.prototype` method - // (`endsWith`, `slice`, …) called on an identifier that - // shares a node-core module name (`url`, `path`) means - // the receiver is a runtime string, NOT the module — - // don't gate it as an unimplemented module API; fall - // through to dynamic dispatch on the real receiver. - // Next.js app-page-turbo calls `url.endsWith(...)` on a - // URL string bound to a local named `url`. - && !super::super::array_fold::is_known_string_prototype_method( - &method_name, - ) - // A destructured/named DATA member of a native module - // (`const { METHODS } = require('node:http')`, - // registered with `imported_method = Some("METHODS")`) - // holds a real Array/Object value, not a callable - // namespace. An Array/Object prototype method on it - // (`METHODS.map(...)`, `STATUS_CODES.hasOwnProperty(...)`) - // is a call on that VALUE — gating it as - // `http.map`/`http.hasOwnProperty` (#463) compiled it to - // a deferred "not implemented" throw, breaking express's - // `router` (`METHODS.map((m) => m.toLowerCase())`). Fall - // through to dynamic dispatch on the real member value. - && !(imported_method.is_some() - && (super::super::array_fold::is_known_array_prototype_method( - &method_name, - ) || super::super::array_fold::is_known_object_prototype_method( - &method_name, - ))) - { - // #925: this is the gate that fires - // for `crypto.hmacSha256(data, key)`. - let hint = super::super::unimpl_hints::module_member_hint( - module_name, - &method_name, - ) - .map(|h| format!(" {h}")) - .unwrap_or_default(); - let msg = format!( - "`{}.{}` is not implemented in Perry — see `perry --print-api-manifest` for the supported surface, \ - or set `PERRY_ALLOW_UNIMPLEMENTED=1` to ignore. (#463){}", - module_name, method_name, hint, - ); - // #5245: default → throw-on-reach + notice; strict → - // hard #463 refusal. #2309 tree-shake handled inside. - let api = format!("{module_name}.{method_name}"); - let location = crate::eval_classifier::location_string( - &ctx.source_file_path, - member.span.lo.0, - ); - match crate::check_unimplemented_api( - &msg, - &api, - &location, - member.span.lo.0, - ) { - crate::UnimplementedDecision::Refuse => { - crate::lower_bail!(member.span, "{}", msg); - } - crate::UnimplementedDecision::DeferToRuntimeError(runtime_msg) => { - return Ok(Ok( - super::super::const_fold_fn::synth_deferred_throw_value( - ctx, - &runtime_msg, - member.span, - )?, - )); - } - } - } - if let Some(entry) = manifest_entry { - if !matches!( - entry.kind, - perry_api_manifest::ApiKind::Method { - has_receiver: false, - class_filter: None - } - ) { - return Ok(Err(args)); - } - } - let class_name = if module_name == "stream" - && imported_method.is_some_and(is_node_stream_class_name) - { - imported_method.map(str::to_string) - } else { - None - }; - return Ok(Ok(Expr::NativeMethodCall { - module: module_name.to_string(), - class_name, - object: None, // Static call on module itself - method: method_name, - args, - })); - } - } - } + args = match imported_module_dispatch::try_imported_module_dispatch( + ctx, member, &obj_name, args, + )? { + Ok(expr) => return Ok(Ok(expr)), + Err(rest) => rest, + }; } } diff --git a/crates/perry-hir/src/lower/expr_call/native_module/buffer_statics.rs b/crates/perry-hir/src/lower/expr_call/native_module/buffer_statics.rs new file mode 100644 index 0000000000..087db24892 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/native_module/buffer_statics.rs @@ -0,0 +1,200 @@ +//! `Buffer` / `Uint8Array` static-method calls. +//! +//! Extracted from `expr_call/native_module.rs` as a mechanical move; the +//! block runs in the same position it occupied inline, and `Ok(Err(args))` +//! means "not one of these statics — keep checking the later receivers". + +use super::*; + +use anyhow::Result; +use swc_ecma_ast as ast; + +pub(super) fn try_buffer_uint8array_statics( + ctx: &LoweringContext, + member: &ast::MemberExpr, + obj_name: &str, + args: Vec, +) -> Result>> { + // Check for Buffer static methods. Issue #831: aliased + // imports of Buffer (`import { Buffer as RuntimeBuffer } from + // "node:buffer"`) must route through the same dedicated + // BufferFrom/BufferAlloc/etc HIR variants as the global + // `Buffer`; otherwise the lowering falls through to a + // receiver-less `NativeMethodCall { module: "buffer", method: + // "from", object: None }` for which the codegen has no + // dispatch table entry — it would silently return + // TAG_UNDEFINED, and any caller that subsequently treats the + // result as a Buffer (e.g. `b[0]` → Uint8ArrayGet) segfaults + // on the undefined value. + let is_buffer_ref = obj_name == "Buffer" + || matches!( + ctx.lookup_native_module(obj_name), + Some(("buffer", Some("Buffer"))) + ); + if is_buffer_ref { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.as_ref(); + match method_name { + "from" => { + let data = args.first().cloned().unwrap_or(Expr::Undefined); + // Disambiguate `Buffer.from(data, encoding?)` vs + // `Buffer.from(arrayBuffer, byteOffset?, length?)`. + // Encoding args are strings, byteOffset/length are + // numbers. Issue #1273: previously any non-string + // literal second arg routed to BufferFromArrayBuffer, + // so `Buffer.from(str, encVar)` produced an empty + // buffer. Now: 3+ args, or a Number-literal second + // arg, or a string-literal first arg with a Number + // second arg → ArrayBuffer form. Otherwise default + // to BufferFrom (the runtime helper dispatches on + // the actual type of `data`, and routes through + // `js_encoding_tag_from_value` for runtime-string + // encodings). + let is_arraybuffer_form = args.len() >= 3 + || matches!(args.get(1), Some(Expr::Number(_) | Expr::Integer(_))); + if args.len() >= 2 && is_arraybuffer_form { + let byte_offset = args.get(1).cloned().unwrap_or(Expr::Number(0.0)); + let length = args.get(2).cloned().map(Box::new); + return Ok(Ok(Expr::BufferFromArrayBuffer { + data: Box::new(data), + byte_offset: Box::new(byte_offset), + length, + })); + } + let encoding = args.get(1).cloned().map(Box::new); + return Ok(Ok(Expr::BufferFrom { + data: Box::new(data), + encoding, + })); + } + "alloc" => { + // #2013: a missing `size` must surface Node's + // `ERR_INVALID_ARG_TYPE` (Received undefined), so + // default to `undefined` — not `0` — and let the + // runtime validator throw. + let size = args.first().cloned().unwrap_or(Expr::Undefined); + let fill = args.get(1).cloned().map(Box::new); + let encoding = args.get(2).cloned().map(Box::new); + return Ok(Ok(Expr::BufferAlloc { + size: Box::new(size), + fill, + encoding, + })); + } + "allocUnsafe" | "allocUnsafeSlow" => { + // #2013: missing `size` → Node ERR_INVALID_ARG_TYPE. + let size = args.first().cloned().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::BufferAllocUnsafe(Box::new(size)))); + } + "concat" => { + let list = args.first().cloned().unwrap_or(Expr::Array(vec![])); + if let Some(total_length) = args.get(1).cloned() { + return Ok(Ok(Expr::BufferConcatWithLength { + list: Box::new(list), + total_length: Box::new(total_length), + })); + } + return Ok(Ok(Expr::BufferConcat(Box::new(list)))); + } + "copyBytesFrom" => { + return Ok(Ok(Expr::NativeMethodCall { + module: "buffer".to_string(), + class_name: None, + object: None, + method: "copyBytesFrom".to_string(), + args, + })); + } + "of" => { + return Ok(Ok(Expr::BufferFrom { + data: Box::new(Expr::Array(args)), + encoding: None, + })); + } + "isBuffer" => { + let obj = args.first().cloned().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::BufferIsBuffer(Box::new(obj)))); + } + "isEncoding" => { + let obj = args.first().cloned().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::BufferIsEncoding(Box::new(obj)))); + } + "byteLength" => { + let data = args + .first() + .cloned() + .unwrap_or(Expr::String("".to_string())); + let encoding = args.get(1).cloned().map(Box::new); + return Ok(Ok(Expr::BufferByteLength { + data: Box::new(data), + encoding, + })); + } + // `Buffer.compare(a, b)` → `a.compare(b)` instance call + // (handled by runtime buffer dispatch). + "compare" if args.len() >= 2 => { + let mut iter = args.into_iter(); + let a = iter.next().unwrap(); + let b = iter.next().unwrap(); + return Ok(Ok(Expr::Call { + callee: Box::new(Expr::PropertyGet { + byte_offset: 0, + object: Box::new(a), + property: "compare".to_string(), + }), + args: vec![b], + type_args: vec![], + byte_offset: 0, + })); + } + _ => {} // Fall through to generic handling + } + } + } + + // Check for Uint8Array static methods + if obj_name == "Uint8Array" { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.as_ref(); + match method_name { + "from" => { + let data = args.first().cloned().unwrap_or(Expr::Undefined); + // #2774: `Uint8Array.from(src, mapFn, thisArg?)` — run + // the mapped materialization first (which validates + // mapFn + binds thisArg), then truncate to Uint8. + if let Some(map_fn) = args.get(1).cloned() { + let this_arg = args.get(2).cloned().map(Box::new); + return Ok(Ok(Expr::Uint8ArrayFrom(Box::new(Expr::ArrayFromMapped { + iterable: Box::new(data), + map_fn: Box::new(map_fn), + this_arg, + })))); + } + return Ok(Ok(Expr::Uint8ArrayFrom(Box::new(data)))); + } + // Issue #871 (part 2): `Uint8Array.of(a, b, c, ...)` — + // uuid's `sha1.js` calls this with 20 args (the SHA-1 + // hash output, byte by byte), which hit the + // `Call callee shape not supported (PropertyGet) with N args` + // bail in `crates/perry-codegen/src/lower_call.rs::~3226` + // because the receiver `Uint8Array` lowers to `GlobalGet(0)` + // (which `lower_expr` evaluates to `0.0`) so the closure-call + // fallback at `~3167` skipped past it, and there's no + // `js_closure_call17..20` to dispatch ≥17-arg calls anyway. + // + // Per ECMAScript: `Uint8Array.of(...items)` is `Uint8Array.from([...items])` + // — same shape as the existing `from` arm above, just wrap the + // varargs in an array literal first. Routes through `Expr::Array` + // → `Expr::Uint8ArrayFrom` which already lowers correctly for any + // arity (it's just `js_uint8array_from_array`-or-equivalent on the + // packed array). Mirrors the `Array.of` arm at :1618. + "of" => { + return Ok(Ok(Expr::Uint8ArrayFrom(Box::new(Expr::Array(args))))); + } + _ => {} // Fall through to generic handling + } + } + } + + Ok(Err(args)) +} diff --git a/crates/perry-hir/src/lower/expr_call/native_module/imported_module_dispatch.rs b/crates/perry-hir/src/lower/expr_call/native_module/imported_module_dispatch.rs new file mode 100644 index 0000000000..a114f39321 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/native_module/imported_module_dispatch.rs @@ -0,0 +1,288 @@ +//! Generic dispatch for a receiver bound to an imported native module. +//! +//! Extracted from `expr_call/native_module.rs` as a mechanical move. This is +//! the last check in `try_native_module_methods`, so falling out of it with +//! `Ok(Err(args))` is exactly what the inline block did when it reached its +//! closing brace. + +use super::*; + +use crate::types::Type; +use anyhow::Result; +use swc_ecma_ast as ast; + +pub(super) fn try_imported_module_dispatch( + ctx: &mut LoweringContext, + member: &ast::MemberExpr, + obj_name: &str, + args: Vec, +) -> Result>> { + if let Some((module_name, imported_method)) = ctx.lookup_native_module(obj_name) { + if module_name == "url" && imported_method == Some("URL") { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.as_ref(); + if method_name == "canParse" && !args.is_empty() { + let mut iter = args.into_iter(); + let input = iter.next().unwrap(); + if let Some(base) = iter.next() { + return Ok(Ok(Expr::UrlCanParseWithBase { + input: Box::new(input), + base: Box::new(base), + })); + } + return Ok(Ok(Expr::UrlCanParse(Box::new(input)))); + } + if method_name == "parse" && !args.is_empty() { + let mut iter = args.into_iter(); + let input = iter.next().unwrap(); + if let Some(base) = iter.next() { + return Ok(Ok(Expr::UrlParseWithBase { + input: Box::new(input), + base: Box::new(base), + })); + } + return Ok(Ok(Expr::UrlParse(Box::new(input)))); + } + } + } + + if let Some(submodule) = path_submodule_name(module_name) { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.to_string(); + return Ok( + match super::super::nested_namespace::dispatch_path_subnamespace( + submodule, + &method_name, + args, + ) { + Ok(expr) => Ok(expr), + Err(args) => Err(args), + }, + ); + } + } + + // Skip modules handled specifically below (path, fs, child_process, etc.) + // `net` used to be in this list back when its method calls + // were short-circuited into `Expr::NetCreateConnection` etc. + // After A1.5 `net` goes through the generic NativeMethodCall + // path so the LLVM backend's NATIVE_MODULE_TABLE dispatches + // to `js_net_socket_*` in perry-stdlib. + let is_handled_module = module_name == "path" + || module_name == "node:path" + || module_name == "fs" + || module_name == "node:fs" + || module_name == "child_process" + || module_name == "node:child_process" + || module_name == "crypto" + || module_name == "node:crypto" + || module_name == "os" + || module_name == "node:os"; + if !is_handled_module { + // This is a call on a native module (e.g., mysql.createConnection) + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.to_string(); + // A destructured/named DATA member of a native module + // (`const { METHODS } = require('node:http')` → + // `imported_method = Some("METHODS")`) holds a real + // Array/Object value. An Array/Object prototype method on it + // (`METHODS.map(...)`, `STATUS_CODES.hasOwnProperty(...)`) is + // a call on that VALUE — NOT a `module.method` native call. + // Bail to generic dynamic dispatch so it runs on the member's + // resolved value (express's `router` does + // `METHODS.map((m) => m.toLowerCase())`). Without this the + // call lowered to `NativeMethodCall { module: "http", method: + // "map" }`, which returned undefined / a deferred throw. + if imported_method.is_some() + && (super::super::super::array_fold::is_known_array_prototype_method( + &method_name, + ) || super::super::super::array_fold::is_known_object_prototype_method( + &method_name, + )) + { + return Ok(Err(args)); + } + if module_name == "worker_threads" && method_name == "workerData" { + return Ok(Err(args)); + } + if module_name.strip_prefix("node:").unwrap_or(module_name) == "vm" + && imported_method.is_none() + && method_name == "Module" + { + let mut exprs = args; + exprs.push(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_vm_module_call".to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + }); + return Ok(Ok(Expr::Sequence(exprs))); + } + let normalized_module = module_name.strip_prefix("node:").unwrap_or(module_name); + if normalized_module == "cluster" + && matches!(imported_method, Some("default")) + && is_cluster_default_event_emitter_method(&method_name) + { + return Ok(Ok(Expr::NativeMethodCall { + module: module_name.to_string(), + class_name: None, + object: None, + method: method_name, + args, + })); + } + if method_name == "call" { + if normalized_module == "stream" + && matches!(imported_method, None | Some("Stream")) + { + return Ok(Ok(event_emitter_constructor_call(args))); + } + if normalized_module == "events" + && matches!(imported_method, Some("EventEmitter")) + { + return Ok(Ok(event_emitter_constructor_call(args))); + } + // #4973: named-import form of the inherits + // pattern — `const { Server } = require('http'); + // Server.call(this, handler)`. Same extern as + // the dotted `http.Server.call(...)` form in + // module_class_static.rs. + if matches!(normalized_module, "http" | "https") + && matches!(imported_method, Some("Server")) + && !args.is_empty() + { + let mut it = args.into_iter(); + let this_arg = it.next().unwrap(); + let mut rest: Vec = it.collect(); + rest.resize(2, Expr::Undefined); + let mut call_args = vec![this_arg]; + call_args.extend(rest); + let extern_name = if normalized_module == "https" { + "js_https_server_construct_with_this" + } else { + "js_http_server_construct_with_this" + }; + return Ok(Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: extern_name.to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: call_args, + type_args: Vec::new(), + byte_offset: 0, + })); + } + } + // Unimplemented-API gate (#463 / #525) for the 2-deep + // `mod.method()` call form. Without this, perry/* and + // other native-module call sites short-circuited past + // the `lower_member` gate that fires for the property- + // read form, then bailed in codegen with a per-module + // message (`'X' is not a known function`) — different + // wording, different escape hatch, harder for users to + // recognize as the same class of mistake. Mirrors the + // 3-deep gate above for `mod.X.Y()`. + let manifest_entry = + perry_api_manifest::module_has_symbol(module_name, &method_name); + if perry_api_manifest::module_has_any_entries(module_name) + && manifest_entry.is_none() + // #wall4: an unmistakable `String.prototype` method + // (`endsWith`, `slice`, …) called on an identifier that + // shares a node-core module name (`url`, `path`) means + // the receiver is a runtime string, NOT the module — + // don't gate it as an unimplemented module API; fall + // through to dynamic dispatch on the real receiver. + // Next.js app-page-turbo calls `url.endsWith(...)` on a + // URL string bound to a local named `url`. + && !super::super::super::array_fold::is_known_string_prototype_method( + &method_name, + ) + // A destructured/named DATA member of a native module + // (`const { METHODS } = require('node:http')`, + // registered with `imported_method = Some("METHODS")`) + // holds a real Array/Object value, not a callable + // namespace. An Array/Object prototype method on it + // (`METHODS.map(...)`, `STATUS_CODES.hasOwnProperty(...)`) + // is a call on that VALUE — gating it as + // `http.map`/`http.hasOwnProperty` (#463) compiled it to + // a deferred "not implemented" throw, breaking express's + // `router` (`METHODS.map((m) => m.toLowerCase())`). Fall + // through to dynamic dispatch on the real member value. + && !(imported_method.is_some() + && (super::super::super::array_fold::is_known_array_prototype_method( + &method_name, + ) || super::super::super::array_fold::is_known_object_prototype_method( + &method_name, + ))) + { + // #925: this is the gate that fires + // for `crypto.hmacSha256(data, key)`. + let hint = super::super::super::unimpl_hints::module_member_hint( + module_name, + &method_name, + ) + .map(|h| format!(" {h}")) + .unwrap_or_default(); + let msg = format!( + "`{}.{}` is not implemented in Perry — see `perry --print-api-manifest` for the supported surface, \ + or set `PERRY_ALLOW_UNIMPLEMENTED=1` to ignore. (#463){}", + module_name, method_name, hint, + ); + // #5245: default → throw-on-reach + notice; strict → + // hard #463 refusal. #2309 tree-shake handled inside. + let api = format!("{module_name}.{method_name}"); + let location = crate::eval_classifier::location_string( + &ctx.source_file_path, + member.span.lo.0, + ); + match crate::check_unimplemented_api(&msg, &api, &location, member.span.lo.0) { + crate::UnimplementedDecision::Refuse => { + crate::lower_bail!(member.span, "{}", msg); + } + crate::UnimplementedDecision::DeferToRuntimeError(runtime_msg) => { + return Ok(Ok( + super::super::super::const_fold_fn::synth_deferred_throw_value( + ctx, + &runtime_msg, + member.span, + )?, + )); + } + } + } + if let Some(entry) = manifest_entry { + if !matches!( + entry.kind, + perry_api_manifest::ApiKind::Method { + has_receiver: false, + class_filter: None + } + ) { + return Ok(Err(args)); + } + } + let class_name = if module_name == "stream" + && imported_method.is_some_and(is_node_stream_class_name) + { + imported_method.map(str::to_string) + } else { + None + }; + return Ok(Ok(Expr::NativeMethodCall { + module: module_name.to_string(), + class_name, + object: None, // Static call on module itself + method: method_name, + args, + })); + } + } + } + + Ok(Err(args)) +} diff --git a/crates/perry-hir/src/lower/expr_call/native_module/object_statics.rs b/crates/perry-hir/src/lower/expr_call/native_module/object_statics.rs new file mode 100644 index 0000000000..051f93b8d2 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/native_module/object_statics.rs @@ -0,0 +1,312 @@ +//! `Object` static-method calls. +//! +//! Extracted from `expr_call/native_module.rs` as a mechanical move; the +//! block runs in the same position it occupied inline, and `Ok(Err(args))` +//! means "not an `Object` static — keep checking the later receivers". + +use super::*; + +use anyhow::Result; +use swc_ecma_ast as ast; + +pub(super) fn try_object_statics( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + member: &ast::MemberExpr, + obj_name: &str, + args: Vec, +) -> Result>> { + // Check for Object static methods. #6677: match BOTH the dot form + // and the string-literal computed form (`Object["keys"](...)`) so the + // computed key does not fall through to generic dispatch (→ + // `TypeError: value is not a function`). + if obj_name == "Object" { + if let Some(method_name) = super::super::static_call_prop_name(&member.prop) { + match method_name { + "keys" => { + let obj = args.first().cloned().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectKeys(Box::new(obj)))); + } + "values" => { + let obj = args.first().cloned().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectValues(Box::new(obj)))); + } + "entries" => { + let obj = args.first().cloned().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectEntries(Box::new(obj)))); + } + // Object.assign(target, ...sources) — per ECMAScript spec, this + // MUTATES target with each source's own enumerable string-keyed + // and Symbol-keyed properties, and RETURNS target (preserving + // identity, class_id, and the SYMBOL_PROPERTIES side-table). + // Refs #590: the previous lowering folded the call into + // ObjectSpread which allocates a fresh object — that breaks + // `result === target` and orphans target's symbol-keyed + // properties since the side table is keyed by raw pointer. + // + "assign" => { + let mut iter = args.into_iter(); + let target = iter.next().unwrap_or(Expr::Undefined); + let sources: Vec = iter.collect(); + // Real `Object.assign(target, ...sources)` — mutate target. + return Ok(Ok(Expr::ObjectAssign { + target: Box::new(target), + sources, + })); + } + "fromEntries" => { + let entries = args.into_iter().next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectFromEntries(Box::new(entries)))); + } + "groupBy" + // Object.groupBy(items, keyFn) — Node 22+ static method + if args.len() >= 2 => { + let mut iter = args.into_iter(); + let items = iter.next().unwrap(); + let key_fn = iter.next().unwrap(); + let key_fn = ctx.maybe_wrap_builtin_callback(key_fn, &call.args[1]); + return Ok(Ok(Expr::ObjectGroupBy { + items: Box::new(items), + key_fn: Box::new(key_fn), + })); + } + "is" => { + let mut iter = args.into_iter(); + let a = iter.next().unwrap_or(Expr::Undefined); + let b = iter.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectIs(Box::new(a), Box::new(b)))); + } + "hasOwn" => { + let mut iter = args.into_iter(); + let obj = iter.next().unwrap_or(Expr::Undefined); + let key = iter.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectHasOwn(Box::new(obj), Box::new(key)))); + } + "freeze" => { + return Ok(Ok(Expr::ObjectFreeze(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "seal" => { + return Ok(Ok(Expr::ObjectSeal(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "preventExtensions" => { + return Ok(Ok(Expr::ObjectPreventExtensions(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "create" => { + let mut it = args.into_iter(); + let proto = it.next().unwrap_or(Expr::Undefined); + let props = it.next().map(Box::new); + return Ok(Ok(Expr::ObjectCreate(Box::new(proto), props))); + } + "isFrozen" => { + return Ok(Ok(Expr::ObjectIsFrozen(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "isSealed" => { + return Ok(Ok(Expr::ObjectIsSealed(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "isExtensible" => { + return Ok(Ok(Expr::ObjectIsExtensible(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "getPrototypeOf" => { + return Ok(Ok(Expr::ObjectGetPrototypeOf(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "setPrototypeOf" => { + // `Object.setPrototypeOf(obj, proto)` is the foundation + // of chalk's "callable + getter-bag" shape (a closure has + // its `[[Prototype]]` reset to a Function-derived + // accessor-bag). Pre-fix this fell through to a generic + // `Object.setPrototypeOf` PropertyGet → Call where + // `Object.setPrototypeOf` resolves to undefined and the + // call throws `TypeError: value is not a function` — + // chalk's `import chalk from "chalk"` died at module init. + // + // Perry's runtime doesn't track mutable per-instance + // prototype chains (class IDs are baked at allocation), + // so we model setPrototypeOf as a no-op that still + // returns the target — matching the spec's "return obj" + // contract. The runtime helper registers (obj, proto) + // in a side-table that `Object.getPrototypeOf(obj)` is + // free to consult later if a downstream pattern needs it. + let mut iter = args.into_iter(); + let obj = iter.next().unwrap_or(Expr::Undefined); + let proto = iter.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectSetPrototypeOf( + Box::new(obj), + Box::new(proto), + ))); + } + "defineProperty" => { + let mut iter = args.into_iter(); + let obj = iter.next().unwrap_or(Expr::Undefined); + let key = iter.next().unwrap_or(Expr::Undefined); + let descriptor = iter.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectDefineProperty( + Box::new(obj), + Box::new(key), + Box::new(descriptor), + ))); + } + "defineProperties" => { + // `Object.defineProperties(target, descriptors)` — bulk + // form of `defineProperty`. Used by chalk's index.js to + // attach the `styles` getter-bag onto + // `createChalk.prototype`. Pre-fix this fell through to a + // generic `(Object).defineProperties(...)` call which + // throws `TypeError: value is not a function` at module + // init because `Object` isn't a runtime object with + // method dispatch. + // + // Desugar to a sequence of `ObjectDefineProperty` + // applications by reading `descriptors`'s own keys at + // compile time when it's an object literal, otherwise + // route through a runtime helper that iterates the + // descriptor object's keys. + let mut iter = args.into_iter(); + let target = iter.next().unwrap_or(Expr::Undefined); + let descs = iter.next().unwrap_or(Expr::Undefined); + if let Expr::Object(props) = &descs { + // Static descriptor literal — desugar to a Sequence + // of `defineProperty(target, key, desc)` calls and + // yield `target` as the result value. + // + // An EMPTY literal must NOT fold to a bare `target`: + // `Object.defineProperties(O, {})` still performs the + // spec's step-1 `If Type(O) is not Object, throw a + // TypeError`, so `Object.defineProperties(undefined, + // {})` must throw. With no keys there is no per-key + // `defineProperty` to enforce that, so route the + // empty case through the runtime helper (which + // validates the target). + if !props.is_empty() { + let target = target; + let mut exprs: Vec = Vec::with_capacity(props.len() + 1); + for (key_name, desc_expr) in props { + exprs.push(Expr::ObjectDefineProperty( + Box::new(target.clone()), + Box::new(Expr::String(key_name.clone())), + Box::new(desc_expr.clone()), + )); + } + exprs.push(target); + return Ok(Ok(Expr::Sequence(exprs))); + } + } + return Ok(Ok(Expr::ObjectDefineProperties( + Box::new(target), + Box::new(descs), + ))); + } + "getOwnPropertyDescriptor" => { + // #2144/#3655: built-in function `.name` / + // `.length` descriptors. + // + // `Object.getOwnPropertyDescriptor(, + // "name"|"length")` and + // `…(., "name"|"length")` + // need a compile-time fold because those builtin + // values are often intrinsic sentinels rather than + // first-class closures. Per spec both descriptors + // are non-writable, non-enumerable, configurable + // data properties. Fold when we can statically + // recognize the receiver shape — same gating logic + // as the direct `.name` / `.length` folds in + // `expr_member.rs`. + if call.args.len() >= 2 && args.len() >= 2 { + let key_name = match call.args[1].expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str(), + _ => None, + }; + if matches!(key_name, Some("name" | "length")) { + let lowered_obj_is_global_intrinsic = match &args[0] { + Expr::GlobalGet(0) => true, + Expr::PropertyGet { object: inner, .. } => { + matches!(inner.as_ref(), Expr::GlobalGet(0)) + } + _ => false, + }; + if lowered_obj_is_global_intrinsic { + match key_name { + Some("name") => { + let folded = + super::super::name_fold::builtin_fn_name_for_arg( + call.args[0].expr.as_ref(), + ); + if let Some(fname) = folded { + return Ok(Ok( + super::super::name_fold::name_data_descriptor( + fname, + ), + )); + } + } + Some("length") => { + let folded = + super::super::name_fold::builtin_fn_length_for_arg( + call.args[0].expr.as_ref(), + ) + .or_else(|| { + super::super::name_fold::builtin_fn_name_for_arg( + call.args[0].expr.as_ref(), + ) + .and_then(|name| { + crate::analysis::builtin_constructor_length( + &name, + ) + }) + }); + if let Some(len) = folded { + return Ok(Ok( + super::super::name_fold::builtin_data_descriptor( + Expr::Number(len as f64), + ), + )); + } + } + _ => {} + } + } + } + } + let mut iter = args.into_iter(); + let obj = iter.next().unwrap_or(Expr::Undefined); + let key = iter.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ObjectGetOwnPropertyDescriptor( + Box::new(obj), + Box::new(key), + ))); + } + "getOwnPropertyDescriptors" => { + return Ok(Ok(Expr::ObjectGetOwnPropertyDescriptors(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "getOwnPropertyNames" => { + return Ok(Ok(Expr::ObjectGetOwnPropertyNames(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + "getOwnPropertySymbols" => { + return Ok(Ok(Expr::ObjectGetOwnPropertySymbols(Box::new( + args.into_iter().next().unwrap_or(Expr::Undefined), + )))); + } + _ => {} // Fall through to generic handling + } + } + } + + Ok(Err(args)) +} diff --git a/crates/perry-hir/src/lower/expr_call/native_module/process_module.rs b/crates/perry-hir/src/lower/expr_call/native_module/process_module.rs new file mode 100644 index 0000000000..b68a187037 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/native_module/process_module.rs @@ -0,0 +1,390 @@ +//! `process` / `node:process` module-object method calls. +//! +//! Extracted from `expr_call/native_module.rs` as a mechanical move; the +//! block runs in the same position it occupied inline, and `Ok(Err(args))` +//! means "not a process method — keep checking the later receivers". + +use super::*; + +use anyhow::Result; +use swc_ecma_ast as ast; + +use super::super::super::unimpl_hints; + +pub(super) fn try_process_module_methods( + ctx: &mut LoweringContext, + member: &ast::MemberExpr, + obj_name: &str, + args: Vec, +) -> Result>> { + // Check for process module methods. `import processModule from + // "node:process"` registers as the native `process` object, while + // `import * as processNamespace` registers as `process.namespace`; + // both must use the same strict method gate as the global object. + let process_name_is_shadowed = + obj_name == "process" && ctx.shadows_unqualified_global("process"); + let is_process_ref = !process_name_is_shadowed + && (obj_name == "process" + || ctx.lookup_builtin_module_alias(obj_name) == Some("process") + || matches!( + ctx.lookup_native_module(obj_name), + Some(("process", _)) | Some(("process.namespace", _)) + )); + if is_process_ref { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.as_ref(); + match method_name { + "uptime" => return Ok(Ok(Expr::ProcessUptime)), + "cwd" => return Ok(Ok(Expr::ProcessCwd)), + "memoryUsage" => return Ok(Ok(Expr::ProcessMemoryUsage)), + "nextTick" => { + if !args.is_empty() { + let mut iter = args.into_iter(); + let callback = iter.next().unwrap(); + let trailing: Vec = iter.collect(); + return Ok(Ok(Expr::ProcessNextTick { + callback: Box::new(callback), + args: trailing, + })); + } + } + "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "emit" + | "listeners" + | "rawListeners" + | "eventNames" + | "listenerCount" + | "removeListener" + | "off" + | "removeAllListeners" + | "setMaxListeners" + | "getMaxListeners" => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name.to_string(), + args, + })); + } + "chdir" => { + if !args.is_empty() { + return Ok(Ok(Expr::ProcessChdir(Box::new( + args.into_iter().next().unwrap(), + )))); + } + } + "kill" => { + if !args.is_empty() { + let mut iter = args.into_iter(); + let pid = iter.next().unwrap(); + let signal = iter.next().map(Box::new); + return Ok(Ok(Expr::ProcessKill { + pid: Box::new(pid), + signal, + })); + } + } + "ref" | "unref" => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name.to_string(), + args, + })); + } + method_name if is_process_active_array_helper(method_name) => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name.to_string(), + args, + })); + } + "setSourceMapsEnabled" => { + // #1400 / #3108: process.setSourceMapsEnabled(bool) + // toggles the live source-map flag. Perry compiles + // AOT and has no resolver, so the flag drives + // nothing observable — but it round-trips through + // process.sourceMapsEnabled and validates that the + // argument is a boolean (else ERR_INVALID_ARG_TYPE), + // matching Node. Lower to the runtime setter, + // passing the original argument for validation. + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: "setSourceMapsEnabled".to_string(), + args, + })); + } + "getBuiltinModule" => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: "getBuiltinModule".to_string(), + args, + })); + } + "execve" => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: "execve".to_string(), + args, + })); + } + "dlopen" => { + // #1409: process.dlopen(module, filename, flags?) + // is Node's native-addon (.node) loader. Perry + // statically links every dependency at compile + // time — there's no dynamic loader to call. + // Returning undefined is the closest no-op: + // call sites that probe for the function before + // attempting to load an addon (a common pattern + // in optional-dep wrappers) see typeof "function" + // and a "loaded" non-error, then fall back to + // their pure-JS path. Real addon-loading + // attempts will surface as the addon's exports + // being undefined downstream. + return Ok(Ok(Expr::Undefined)); + } + "hasUncaughtExceptionCaptureCallback" => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: "hasUncaughtExceptionCaptureCallback".to_string(), + args, + })); + } + "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback" => { + let method_name = method_ident.sym.as_ref().to_string(); + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name, + args, + })); + } + "loadEnvFile" => { + // #1399 / #2135: process.loadEnvFile(path?) + // (Node 20.12+) reads a `.env` file from disk and + // merges its KEY=value entries into `process.env`. + // Previously a no-op because `process.env.X = v` + // didn't persist; #1344 has since wired writes + // through `std::env::set_var`, so we lower to a + // runtime call that actually reads the file. + // Keep the original JS value: the runtime handles + // omitted/undefined/null defaulting plus Buffer + // and file-URL path objects. + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: "loadEnvFile".to_string(), + args, + })); + } + "exit" => { + // process.exit() / process.exit(code) — never + // returns, terminates the process. Until now this + // fell through to generic NativeMethodCall which + // silently no-op'd, so scripts that rely on it to + // end the event loop (e.g. `main().then(() => + // process.exit(0))` in a net-socket driver) would + // hang with the socket still keeping the loop alive. + let code = if !args.is_empty() { + Some(Box::new(args.into_iter().next().unwrap())) + } else { + None + }; + return Ok(Ok(Expr::ProcessExit(code))); + } + "abort" => { + // process.abort() — raises SIGABRT, no clean + // shutdown. Maps to libc::abort() at runtime. + return Ok(Ok(Expr::ProcessAbort)); + } + "umask" => { + // process.umask(mask?) — returns the current + // file-mode creation mask, optionally setting + // a new one first and returning the previous. + let mask = if !args.is_empty() { + Some(Box::new(args.into_iter().next().unwrap())) + } else { + None + }; + return Ok(Ok(Expr::ProcessUmask(mask))); + } + "threadCpuUsage" => { + // process.threadCpuUsage(prior?) — CPU time used + // by the current thread, as { user, system } in + // microseconds. If prior is given, returns the + // validated delta. + let prior = if !args.is_empty() { + Some(Box::new(args.into_iter().next().unwrap())) + } else { + None + }; + return Ok(Ok(Expr::ProcessThreadCpuUsage(prior))); + } + "availableMemory" => { + // process.availableMemory() — free system memory + // available to the process, in bytes. + return Ok(Ok(Expr::ProcessAvailableMemory)); + } + "constrainedMemory" => { + // process.constrainedMemory() — OS-imposed memory + // limit (cgroups/container), in bytes. 0 when no + // limit applies. + return Ok(Ok(Expr::ProcessConstrainedMemory)); + } + // POSIX credential accessors (#1408). All four delegate + // to libc::{getuid,geteuid,getgid,getegid}() at runtime. + "getuid" => { + return Ok(Ok(Expr::ProcessPosixCredential( + crate::ir::PosixCredentialKind::Uid, + ))); + } + "geteuid" => { + return Ok(Ok(Expr::ProcessPosixCredential( + crate::ir::PosixCredentialKind::Euid, + ))); + } + "getgid" => { + return Ok(Ok(Expr::ProcessPosixCredential( + crate::ir::PosixCredentialKind::Gid, + ))); + } + "getegid" => { + return Ok(Ok(Expr::ProcessPosixCredential( + crate::ir::PosixCredentialKind::Egid, + ))); + } + "getgroups" => { + // #2135: process.getgroups() — supplementary + // group IDs as a number array. Dispatch through + // the generic NativeMethodCall path; the + // node_core table row routes to + // `js_process_getgroups`. + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: "getgroups".to_string(), + args, + })); + } + // #2135: POSIX credential setters — single numeric + // ID arg, return undefined. Implemented as libc + // wrappers in the runtime (string-username form is + // a no-op today; see js_process_setuid for the + // out-of-scope note). + "setuid" | "seteuid" | "setgid" | "setegid" => { + let method_name = method_ident.sym.as_ref().to_string(); + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name, + args, + })); + } + // #2135: process.setgroups(groups[]) takes an + // array of numeric GIDs; process.initgroups(user, + // extra_gid) takes a username string + numeric + // GID. The runtime decodes the JSValues itself, so + // both pass through the generic NativeMethodCall. + "setgroups" | "initgroups" => { + let method_name = method_ident.sym.as_ref().to_string(); + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name, + args, + })); + } + "emitWarning" => { + // process.emitWarning(warning[, type, code, ctor]) + // — writes a formatted warning to stderr. Perry + // collapses the overloads into a positional Vec + // and lets the runtime do the formatting. + return Ok(Ok(Expr::ProcessEmitWarning(args))); + } + "cpuUsage" => { + // process.cpuUsage(prior?) — { user, system } in + // microseconds. If prior is given, returns the + // diff (clamped to >= 0). + let prior = if !args.is_empty() { + Some(Box::new(args.into_iter().next().unwrap())) + } else { + None + }; + return Ok(Ok(Expr::ProcessCpuUsage(prior))); + } + "resourceUsage" => { + return Ok(Ok(Expr::ProcessResourceUsage)); + } + "getActiveResourcesInfo" => { + return Ok(Ok(Expr::ProcessActiveResourcesInfo)); + } + "hrtime" => { + // process.hrtime(prior?) — [secs, nanos] from a + // monotonic clock. With prior, returns the diff. + // `process.hrtime.bigint()` is intercepted earlier. + let prior = if !args.is_empty() { + Some(Box::new(args.into_iter().next().unwrap())) + } else { + None + }; + return Ok(Ok(Expr::ProcessHrtime(prior))); + } + _ => { + let hint = unimpl_hints::module_member_hint("process", method_name) + .map(|h| format!(" {h}")) + .unwrap_or_default(); + let msg = format!( + "`process.{}` is not implemented in Perry — see `perry --print-api-manifest` for the supported surface, \ + or set `PERRY_ALLOW_UNIMPLEMENTED=1` to ignore. (#463){}", + method_name, hint, + ); + // #5245: default → throw-on-reach + notice; strict + // (`perry.strict` / `--strict-unimplemented`) → hard + // #463 refusal. Tree-shake deferral handled inside. + let api = format!("process.{method_name}"); + let location = crate::eval_classifier::location_string( + &ctx.source_file_path, + member.span.lo.0, + ); + match crate::check_unimplemented_api(&msg, &api, &location, member.span.lo.0) { + crate::UnimplementedDecision::Refuse => { + crate::lower_bail!(member.span, "{}", msg); + } + crate::UnimplementedDecision::DeferToRuntimeError(runtime_msg) => { + return Ok(Ok( + super::super::super::const_fold_fn::synth_deferred_throw_value( + ctx, + &runtime_msg, + member.span, + )?, + )); + } + } + } + } + } + } + + Ok(Err(args)) +} diff --git a/crates/perry-hir/src/lower/expr_call/native_module/reflect_statics.rs b/crates/perry-hir/src/lower/expr_call/native_module/reflect_statics.rs new file mode 100644 index 0000000000..9b0523a83e --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/native_module/reflect_statics.rs @@ -0,0 +1,258 @@ +//! `Reflect` static-method calls. +//! +//! Extracted from `expr_call/native_module.rs` as a mechanical move; the +//! block runs in the same position it occupied inline, and `Ok(Err(args))` +//! means "not a `Reflect` static — keep checking the later receivers". + +use super::*; + +use anyhow::Result; +use swc_ecma_ast as ast; + +pub(super) fn try_reflect_statics( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + member: &ast::MemberExpr, + obj_name: &str, + args: Vec, +) -> Result>> { + if obj_name == "Reflect" { + // #6677: accept the string-literal computed form too. + if let Some(method_name) = super::super::static_call_prop_name(&member.prop) { + match method_name { + "get" => { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let key = it.next().unwrap_or(Expr::Undefined); + // #2766: optional `receiver` (3rd arg) used as the + // `this` binding for accessor getters. Default to + // `undefined` — the runtime substitutes `target`. + let receiver = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectGet { + target: Box::new(target), + key: Box::new(key), + receiver: Box::new(receiver), + })); + } + "set" => { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let key = it.next().unwrap_or(Expr::Undefined); + let value = it.next().unwrap_or(Expr::Undefined); + // Optional `receiver` (4th arg): default `undefined` + // and the runtime substitutes `target`. + let receiver = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectSet { + target: Box::new(target), + key: Box::new(key), + value: Box::new(value), + receiver: Box::new(receiver), + })); + } + "has" => { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let key = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectHas { + target: Box::new(target), + key: Box::new(key), + })); + } + "deleteProperty" => { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let key = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectDelete { + target: Box::new(target), + key: Box::new(key), + })); + } + "ownKeys" => { + let target = args.into_iter().next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectOwnKeys(Box::new(target)))); + } + "apply" => { + let mut it = args.into_iter(); + let func = it.next().unwrap_or(Expr::Undefined); + let this_arg = it.next().unwrap_or(Expr::Undefined); + // Spec sec-reflect.apply runs CreateListFromArrayLike + // on argumentsList, which throws a TypeError when + // Type(argumentsList) is not Object. An OMITTED + // argumentsList is `undefined` (not Object), so it + // must reach the runtime as `undefined` and throw — + // NOT default to an empty array, which would silently + // succeed with no args (test262 + // Reflect/apply/arguments-list-is-not-array-like + // `Reflect.apply(fn, null /* empty */)`). + let args_arr = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectApply { + func: Box::new(func), + this_arg: Box::new(this_arg), + args: Box::new(args_arr), + })); + } + "construct" => { + // Special case: `Reflect.construct(ClassName, [args...])` + // where ClassName is a known class — fold to a direct + // `new ClassName(...args)` expression. + // + // #2768: only fold the two-argument form. With an + // explicit `newTarget` (3rd arg) the result's prototype + // comes from `newTarget` and `newTarget` must be + // validated as a constructor — a plain `new ClassName` + // would silently drop both. Fall through to + // `ReflectConstruct` (runtime `js_reflect_construct`, + // which runs `js_new_function_construct_with_new_target` + // and the non-constructor `newTarget` TypeError check). + if call.args.len() == 2 { + if let ast::Expr::Ident(cls_ident) = call.args[0].expr.as_ref() { + let cls_name = cls_ident.sym.to_string(); + if ctx.lookup_class(&cls_name).is_some() { + if let ast::Expr::Array(arr_lit) = call.args[1].expr.as_ref() { + let new_args: Vec = arr_lit + .elems + .iter() + .filter_map(|e| e.as_ref()) + .map(|e| lower_expr(ctx, &e.expr)) + .collect::>>()?; + return Ok(Ok(Expr::New { + class_name: cls_name, + args: new_args, + type_args: vec![], + byte_offset: 0, + cap_args_appended: 0, + })); + } + } + } + } + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let args_arr = it.next().unwrap_or(Expr::Array(vec![])); + // 3rd arg = newTarget; defaults to `undefined` so the + // runtime falls back to the target/proxy itself. + let new_target = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectConstruct { + target: Box::new(target), + args: Box::new(args_arr), + new_target: Box::new(new_target), + })); + } + "defineProperty" => { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let key = it.next().unwrap_or(Expr::Undefined); + let descriptor = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectDefineProperty { + target: Box::new(target), + key: Box::new(key), + descriptor: Box::new(descriptor), + })); + } + "getPrototypeOf" => { + let target = args.into_iter().next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectGetPrototypeOf(Box::new(target)))); + } + "getOwnPropertyDescriptor" => { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let key = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectGetOwnPropertyDescriptor { + target: Box::new(target), + key: Box::new(key), + })); + } + "defineMetadata" => { + let (key, value, target, property_key) = take_reflect_kvtp_args(args); + return Ok(Ok(Expr::ReflectDefineMetadata { + key: Box::new(key), + value: Box::new(value), + target: Box::new(target), + property_key, + })); + } + "getMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Ok(Expr::ReflectGetMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + })); + } + "getOwnMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Ok(Expr::ReflectGetOwnMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + })); + } + "hasMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Ok(Expr::ReflectHasMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + })); + } + "hasOwnMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Ok(Expr::ReflectHasOwnMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + })); + } + "getMetadataKeys" => { + let (target, property_key) = take_reflect_tp_args(args); + return Ok(Ok(Expr::ReflectGetMetadataKeys { + target: Box::new(target), + property_key, + })); + } + "getOwnMetadataKeys" => { + let (target, property_key) = take_reflect_tp_args(args); + return Ok(Ok(Expr::ReflectGetOwnMetadataKeys { + target: Box::new(target), + property_key, + })); + } + "deleteMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Ok(Expr::ReflectDeleteMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + })); + } + "setPrototypeOf" => { + // #2761: Reflect-specific — boolean result (false on + // rejected change) + TypeError on bad args, distinct + // from Object.setPrototypeOf (returns the object). + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let proto = it.next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectSetPrototypeOf { + target: Box::new(target), + proto: Box::new(proto), + })); + } + "isExtensible" => { + // #2762: Reflect-specific semantics (boolean + + // TypeError on non-object), NOT Object.isExtensible. + let target = args.into_iter().next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectIsExtensible(Box::new(target)))); + } + "preventExtensions" => { + // #2762: Reflect-specific semantics (boolean + + // TypeError on non-object), NOT Object.preventExtensions. + let target = args.into_iter().next().unwrap_or(Expr::Undefined); + return Ok(Ok(Expr::ReflectPreventExtensions(Box::new(target)))); + } + _ => {} + } + } + } + + Ok(Err(args)) +} diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index 52f6122e53..92d723dcec 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -9,6 +9,15 @@ use crate::lower::LoweringContext; use super::*; +mod closure_ident_scan; +mod var_names; + +use closure_ident_scan::{cic_expr, cic_stmt}; +pub(crate) use var_names::{ + collect_annexb_block_fn_decl_names, collect_lexical_decl_names, + collect_var_binding_names_from_pat, collect_var_binding_names_from_stmt, +}; + pub fn lower_block_stmt(ctx: &mut LoweringContext, block: &ast::BlockStmt) -> Result> { rebind_nested_forward_scope_lets(ctx, &block.stmts); lower_stmts_using_aware(ctx, &block.stmts) @@ -325,668 +334,6 @@ fn collect_pat_forward_idents(pat: &ast::Pat, out: &mut Vec<(String, u32)>) { } } -fn cic_stmt(s: &ast::Stmt, in_cl: bool, out: &mut std::collections::HashSet) { - use ast::Stmt::*; - match s { - Block(b) => b.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)), - Return(r) => { - if let Some(a) = &r.arg { - cic_expr(a, in_cl, out); - } - } - Expr(e) => cic_expr(&e.expr, in_cl, out), - If(i) => { - cic_expr(&i.test, in_cl, out); - cic_stmt(&i.cons, in_cl, out); - if let Some(a) = &i.alt { - cic_stmt(a, in_cl, out); - } - } - Throw(t) => cic_expr(&t.arg, in_cl, out), - While(w) => { - cic_expr(&w.test, in_cl, out); - cic_stmt(&w.body, in_cl, out); - } - DoWhile(w) => { - cic_expr(&w.test, in_cl, out); - cic_stmt(&w.body, in_cl, out); - } - For(f) => { - if let Some(init) = &f.init { - match init { - ast::VarDeclOrExpr::Expr(e) => cic_expr(e, in_cl, out), - ast::VarDeclOrExpr::VarDecl(vd) => vd.decls.iter().for_each(|d| { - if let Some(i) = &d.init { - cic_expr(i, in_cl, out); - } - }), - } - } - if let Some(t) = &f.test { - cic_expr(t, in_cl, out); - } - if let Some(u) = &f.update { - cic_expr(u, in_cl, out); - } - cic_stmt(&f.body, in_cl, out); - } - ForIn(f) => { - cic_expr(&f.right, in_cl, out); - cic_stmt(&f.body, in_cl, out); - } - ForOf(f) => { - cic_expr(&f.right, in_cl, out); - cic_stmt(&f.body, in_cl, out); - } - Try(t) => { - t.block.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)); - if let Some(h) = &t.handler { - h.body.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)); - } - if let Some(f) = &t.finalizer { - f.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)); - } - } - Switch(sw) => { - cic_expr(&sw.discriminant, in_cl, out); - for c in &sw.cases { - if let Some(t) = &c.test { - cic_expr(t, in_cl, out); - } - c.cons.iter().for_each(|st| cic_stmt(st, in_cl, out)); - } - } - Labeled(l) => cic_stmt(&l.body, in_cl, out), - With(w) => { - cic_expr(&w.obj, in_cl, out); - cic_stmt(&w.body, in_cl, out); - } - Decl(d) => cic_decl(d, in_cl, out), - _ => {} - } -} - -fn cic_decl(d: &ast::Decl, in_cl: bool, out: &mut std::collections::HashSet) { - match d { - ast::Decl::Var(vd) => vd.decls.iter().for_each(|de| { - if let Some(i) = &de.init { - cic_expr(i, in_cl, out); - } - }), - // A nested function declaration's body is a closure scope. - ast::Decl::Fn(f) => cic_function(&f.function, out), - ast::Decl::Class(c) => cic_class(&c.class, in_cl, out), - _ => {} - } -} - -/// Param patterns (defaults evaluate at CALL time) + body of a closure-scoped -/// `ast::Function` — nested fn declarations/expressions and class methods all -/// share this traversal. -fn cic_function(f: &ast::Function, out: &mut std::collections::HashSet) { - for p in &f.params { - cic_pat(&p.pat, true, out); - } - if let Some(b) = &f.body { - b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); - } -} - -fn cic_class(c: &ast::Class, in_cl: bool, out: &mut std::collections::HashSet) { - if let Some(sc) = &c.super_class { - cic_expr(sc, in_cl, out); - } - for m in &c.body { - match m { - ast::ClassMember::Method(mm) => cic_function(&mm.function, out), - ast::ClassMember::PrivateMethod(mm) => cic_function(&mm.function, out), - // #6523: the CONSTRUCTOR body runs at `new` time, not at class - // definition — a binding it references that is declared AFTER the - // class (`class C { constructor(){ a() } } const a = ...`) must be - // pre-registered as a forward-captured lexical exactly like a - // method-body reference. This arm was missing, so such refs never - // got a box: `collect_method_captures` dropped them (not in - // `ctx.locals` at the class decl) and the ref fell through to the - // global lookup — "a is not defined" at construction (bundled - // semver's Comparator debug/constant pattern). - ast::ClassMember::Constructor(ctor) => { - for p in &ctor.params { - match p { - ast::ParamOrTsParamProp::Param(p) => cic_pat(&p.pat, true, out), - ast::ParamOrTsParamProp::TsParamProp(tp) => { - if let ast::TsParamPropParam::Assign(a) = &tp.param { - cic_expr(&a.right, true, out); - } - } - } - } - if let Some(b) = &ctor.body { - b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); - } - } - ast::ClassMember::ClassProp(p) => { - if let Some(v) = &p.value { - cic_expr(v, true, out); - } - } - ast::ClassMember::PrivateProp(p) => { - if let Some(v) = &p.value { - cic_expr(v, true, out); - } - } - ast::ClassMember::StaticBlock(sb) => { - sb.body.stmts.iter().for_each(|st| cic_stmt(st, true, out)); - } - _ => {} - } - } -} - -fn cic_expr(e: &ast::Expr, in_cl: bool, out: &mut std::collections::HashSet) { - use ast::Expr::*; - match e { - Ident(i) => { - if in_cl { - out.insert(i.sym.to_string()); - } - } - Arrow(a) => { - for p in &a.params { - cic_pat(p, true, out); - } - match &*a.body { - ast::BlockStmtOrExpr::BlockStmt(b) => { - b.stmts.iter().for_each(|st| cic_stmt(st, true, out)) - } - ast::BlockStmtOrExpr::Expr(ex) => cic_expr(ex, true, out), - } - } - Fn(f) => cic_function(&f.function, out), - Class(c) => cic_class(&c.class, in_cl, out), - Array(a) => a - .elems - .iter() - .flatten() - .for_each(|el| cic_expr(&el.expr, in_cl, out)), - Object(o) => { - for p in &o.props { - match p { - ast::PropOrSpread::Spread(s) => cic_expr(&s.expr, in_cl, out), - ast::PropOrSpread::Prop(pr) => cic_prop(pr, in_cl, out), - } - } - } - Unary(u) => cic_expr(&u.arg, in_cl, out), - Update(u) => cic_expr(&u.arg, in_cl, out), - Bin(b) => { - cic_expr(&b.left, in_cl, out); - cic_expr(&b.right, in_cl, out); - } - Assign(a) => { - cic_assign_target(&a.left, in_cl, out); - cic_expr(&a.right, in_cl, out); - } - Member(m) => { - cic_expr(&m.obj, in_cl, out); - if let ast::MemberProp::Computed(c) = &m.prop { - cic_expr(&c.expr, in_cl, out); - } - } - Cond(c) => { - cic_expr(&c.test, in_cl, out); - cic_expr(&c.cons, in_cl, out); - cic_expr(&c.alt, in_cl, out); - } - Call(c) => { - if let ast::Callee::Expr(e) = &c.callee { - cic_expr(e, in_cl, out); - } - c.args.iter().for_each(|a| cic_expr(&a.expr, in_cl, out)); - } - New(n) => { - cic_expr(&n.callee, in_cl, out); - if let Some(args) = &n.args { - args.iter().for_each(|a| cic_expr(&a.expr, in_cl, out)); - } - } - Seq(s) => s.exprs.iter().for_each(|e| cic_expr(e, in_cl, out)), - Tpl(t) => t.exprs.iter().for_each(|e| cic_expr(e, in_cl, out)), - TaggedTpl(t) => { - cic_expr(&t.tag, in_cl, out); - t.tpl.exprs.iter().for_each(|e| cic_expr(e, in_cl, out)); - } - Paren(p) => cic_expr(&p.expr, in_cl, out), - Await(a) => cic_expr(&a.arg, in_cl, out), - Yield(y) => { - if let Some(a) = &y.arg { - cic_expr(a, in_cl, out); - } - } - OptChain(o) => match &*o.base { - ast::OptChainBase::Member(m) => { - cic_expr(&m.obj, in_cl, out); - if let ast::MemberProp::Computed(c) = &m.prop { - cic_expr(&c.expr, in_cl, out); - } - } - ast::OptChainBase::Call(c) => { - cic_expr(&c.callee, in_cl, out); - c.args.iter().for_each(|a| cic_expr(&a.expr, in_cl, out)); - } - }, - _ => {} - } -} - -fn cic_pat(p: &ast::Pat, in_cl: bool, out: &mut std::collections::HashSet) { - match p { - ast::Pat::Assign(a) => { - cic_pat(&a.left, in_cl, out); - cic_expr(&a.right, in_cl, out); - } - ast::Pat::Array(arr) => arr - .elems - .iter() - .flatten() - .for_each(|el| cic_pat(el, in_cl, out)), - ast::Pat::Object(o) => { - for pp in &o.props { - match pp { - ast::ObjectPatProp::KeyValue(kv) => cic_pat(&kv.value, in_cl, out), - ast::ObjectPatProp::Assign(a) => { - if let Some(v) = &a.value { - cic_expr(v, in_cl, out); - } - } - ast::ObjectPatProp::Rest(r) => cic_pat(&r.arg, in_cl, out), - } - } - } - ast::Pat::Rest(r) => cic_pat(&r.arg, in_cl, out), - _ => {} - } -} - -fn cic_prop(p: &ast::Prop, in_cl: bool, out: &mut std::collections::HashSet) { - match p { - ast::Prop::Shorthand(i) => { - if in_cl { - out.insert(i.sym.to_string()); - } - } - ast::Prop::KeyValue(kv) => { - if let ast::PropName::Computed(c) = &kv.key { - cic_expr(&c.expr, in_cl, out); - } - cic_expr(&kv.value, in_cl, out); - } - ast::Prop::Getter(g) => { - if let Some(b) = &g.body { - b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); - } - } - ast::Prop::Setter(s) => { - if let Some(b) = &s.body { - b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); - } - } - ast::Prop::Method(m) => { - if let Some(b) = &m.function.body { - b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); - } - } - ast::Prop::Assign(a) => cic_expr(&a.value, in_cl, out), - } -} - -fn cic_assign_target( - t: &ast::AssignTarget, - in_cl: bool, - out: &mut std::collections::HashSet, -) { - if let ast::AssignTarget::Simple(s) = t { - match s { - ast::SimpleAssignTarget::Ident(i) => { - if in_cl { - out.insert(i.id.sym.to_string()); - } - } - ast::SimpleAssignTarget::Member(m) => { - cic_expr(&m.obj, in_cl, out); - if let ast::MemberProp::Computed(c) = &m.prop { - cic_expr(&c.expr, in_cl, out); - } - } - ast::SimpleAssignTarget::Paren(p) => cic_expr(&p.expr, in_cl, out), - _ => {} - } - } -} - -pub(crate) fn collect_var_binding_names_from_pat(pat: &ast::Pat, out: &mut Vec) { - match pat { - ast::Pat::Ident(ident) => out.push(ident.id.sym.to_string()), - ast::Pat::Array(arr) => { - for elem in arr.elems.iter().flatten() { - collect_var_binding_names_from_pat(elem, out); - } - } - ast::Pat::Object(obj) => { - for prop in &obj.props { - match prop { - ast::ObjectPatProp::Assign(assign) => out.push(assign.key.sym.to_string()), - ast::ObjectPatProp::KeyValue(kv) => { - collect_var_binding_names_from_pat(&kv.value, out) - } - ast::ObjectPatProp::Rest(rest) => { - collect_var_binding_names_from_pat(&rest.arg, out) - } - } - } - } - ast::Pat::Assign(assign) => collect_var_binding_names_from_pat(&assign.left, out), - ast::Pat::Rest(rest) => collect_var_binding_names_from_pat(&rest.arg, out), - _ => {} - } -} - -fn collect_var_binding_names_from_var_decl(var_decl: &ast::VarDecl, out: &mut Vec) { - if var_decl.kind != ast::VarDeclKind::Var { - return; - } - for decl in &var_decl.decls { - collect_var_binding_names_from_pat(&decl.name, out); - } -} - -pub(crate) fn collect_var_binding_names_from_stmt(stmt: &ast::Stmt, out: &mut Vec) { - match stmt { - ast::Stmt::Block(block) => { - for stmt in &block.stmts { - collect_var_binding_names_from_stmt(stmt, out); - } - } - ast::Stmt::Decl(ast::Decl::Var(var_decl)) => { - collect_var_binding_names_from_var_decl(var_decl, out); - } - // Nested function/class bodies have their own var environments. - ast::Stmt::Decl(ast::Decl::Fn(_)) | ast::Stmt::Decl(ast::Decl::Class(_)) => {} - ast::Stmt::If(if_stmt) => { - collect_var_binding_names_from_stmt(&if_stmt.cons, out); - if let Some(alt) = &if_stmt.alt { - collect_var_binding_names_from_stmt(alt, out); - } - } - ast::Stmt::While(while_stmt) => collect_var_binding_names_from_stmt(&while_stmt.body, out), - ast::Stmt::DoWhile(do_while) => collect_var_binding_names_from_stmt(&do_while.body, out), - ast::Stmt::For(for_stmt) => { - if let Some(ast::VarDeclOrExpr::VarDecl(var_decl)) = &for_stmt.init { - collect_var_binding_names_from_var_decl(var_decl, out); - } - collect_var_binding_names_from_stmt(&for_stmt.body, out); - } - ast::Stmt::ForIn(for_in) => { - if let ast::ForHead::VarDecl(var_decl) = &for_in.left { - collect_var_binding_names_from_var_decl(var_decl, out); - } - collect_var_binding_names_from_stmt(&for_in.body, out); - } - ast::Stmt::ForOf(for_of) => { - if let ast::ForHead::VarDecl(var_decl) = &for_of.left { - collect_var_binding_names_from_var_decl(var_decl, out); - } - collect_var_binding_names_from_stmt(&for_of.body, out); - } - ast::Stmt::Labeled(labeled) => collect_var_binding_names_from_stmt(&labeled.body, out), - ast::Stmt::Switch(switch_stmt) => { - for case in &switch_stmt.cases { - for stmt in &case.cons { - collect_var_binding_names_from_stmt(stmt, out); - } - } - } - ast::Stmt::Try(try_stmt) => { - for stmt in &try_stmt.block.stmts { - collect_var_binding_names_from_stmt(stmt, out); - } - if let Some(handler) = &try_stmt.handler { - for stmt in &handler.body.stmts { - collect_var_binding_names_from_stmt(stmt, out); - } - } - if let Some(finalizer) = &try_stmt.finalizer { - for stmt in &finalizer.stmts { - collect_var_binding_names_from_stmt(stmt, out); - } - } - } - ast::Stmt::With(with_stmt) => collect_var_binding_names_from_stmt(&with_stmt.body, out), - _ => {} - } -} - -/// Collect the lexically-declared names (`let` / `const` / `class`) at the top -/// level of a statement list. A `var` or a `function` declaration is NOT -/// lexical and does not belong here. Used to build the Annex B "forbidden" set: -/// a block-level function declaration whose name collides with a lexical -/// binding in an enclosing scope would make the equivalent `var` an early -/// error, so B.3.3 skips creating the enclosing-scope `var`. -pub(crate) fn collect_lexical_decl_names( - stmts: &[ast::Stmt], - out: &mut std::collections::HashSet, -) { - for stmt in stmts { - match stmt { - ast::Stmt::Decl(ast::Decl::Var(var_decl)) if var_decl.kind != ast::VarDeclKind::Var => { - for decl in &var_decl.decls { - let mut names = Vec::new(); - collect_var_binding_names_from_pat(&decl.name, &mut names); - out.extend(names); - } - } - ast::Stmt::Decl(ast::Decl::Class(class_decl)) => { - out.insert(class_decl.ident.sym.to_string()); - } - _ => {} - } - } -} - -/// Annex B B.3.3 (#5297): collect the names of function declarations that -/// appear *inside a nested block* of a function/program body. In sloppy mode -/// such a legacy block-level function declaration ALSO creates a `var`-style -/// binding in the enclosing function/global scope (`f` is visible — as a `var` -/// initialised to `undefined` until the declaration runs — outside the block). -/// -/// `body_stmts` are the body's own top-level statements: a `function f(){}` -/// directly among them is an ordinary FunctionDeclaration (already function- -/// scoped) and is NOT collected; every function declaration reached by -/// descending through a block / `if` branch / loop body / `switch` case / -/// `try` part / labeled / `with` body IS. `forbidden` seeds the names for which -/// the legacy `var` must be skipped — the spec gates B.3.3 on "replacing the -/// FunctionDeclaration with a `var` produces no early error and the name is not -/// a parameter": callers pass the parameter names, the body's own top-level -/// lexical names, and `"arguments"`. As we descend, each block contributes its -/// own `let`/`const`/`class` names to the forbidden set for everything nested -/// within it (so `{ let f; { function f(){} } }` is correctly skipped). Nested -/// function and class bodies own their own var environment and are not entered. -/// One traversal yields two results: -/// - `all_out`: EVERY block-nested function declaration name. Every block-level -/// function declaration is block-scoped (gets its own binding), so -/// `lower_nested_fn_decl` gives these a fresh local rather than clobbering an -/// enclosing same-named parameter/binding. -/// - `var_out`: the subset that ALSO gets the legacy enclosing-scope `var` — -/// names not in `forbidden` and not shadowed by an enclosing block's -/// `let`/`const`/`class` (which would make `var f` an early error). -pub(crate) fn collect_annexb_block_fn_decl_names( - body_stmts: &[ast::Stmt], - forbidden: &std::collections::HashSet, - all_out: &mut Vec, - var_out: &mut Vec, -) { - for stmt in body_stmts { - // A direct top-level function declaration is already function-scoped. - if matches!(stmt, ast::Stmt::Decl(ast::Decl::Fn(_))) { - continue; - } - annexb_nested_stmt(stmt, forbidden, all_out, var_out); - } -} - -fn annexb_nested_stmt( - stmt: &ast::Stmt, - forbidden: &std::collections::HashSet, - all_out: &mut Vec, - var_out: &mut Vec, -) { - match stmt { - ast::Stmt::Decl(ast::Decl::Fn(fn_decl)) => { - let name = fn_decl.ident.sym.to_string(); - all_out.push(name.clone()); - if !forbidden.contains(&name) { - var_out.push(name); - } - } - // Nested function/class bodies have their own var environment. - ast::Stmt::Decl(ast::Decl::Class(_)) => {} - ast::Stmt::Block(block) => annexb_nested_block(&block.stmts, forbidden, all_out, var_out), - ast::Stmt::If(if_stmt) => { - annexb_nested_stmt(&if_stmt.cons, forbidden, all_out, var_out); - if let Some(alt) = &if_stmt.alt { - annexb_nested_stmt(alt, forbidden, all_out, var_out); - } - } - ast::Stmt::While(while_stmt) => { - annexb_nested_stmt(&while_stmt.body, forbidden, all_out, var_out) - } - ast::Stmt::DoWhile(do_while) => { - annexb_nested_stmt(&do_while.body, forbidden, all_out, var_out) - } - // A `for`/`for-in`/`for-of` lexical head (`for (let f; ...)`, - // `for (let f in/of ...)`) introduces a binding whose scope encloses - // the loop body; an equivalent `var f` in the body is an early error - // (14.7.4.1 / 14.7.5.1), so the AnnexB legacy `var` for a same-named - // block function in the body must be skipped. - ast::Stmt::For(for_stmt) => { - let names = match &for_stmt.init { - Some(ast::VarDeclOrExpr::VarDecl(vd)) => var_decl_lexical_names(vd), - _ => Vec::new(), - }; - annexb_nested_loop_body(&for_stmt.body, names, forbidden, all_out, var_out); - } - ast::Stmt::ForIn(for_in) => { - let names = for_head_lexical_names(&for_in.left); - annexb_nested_loop_body(&for_in.body, names, forbidden, all_out, var_out); - } - ast::Stmt::ForOf(for_of) => { - let names = for_head_lexical_names(&for_of.left); - annexb_nested_loop_body(&for_of.body, names, forbidden, all_out, var_out); - } - ast::Stmt::Labeled(labeled) => { - annexb_nested_stmt(&labeled.body, forbidden, all_out, var_out) - } - ast::Stmt::Switch(switch_stmt) => { - // All cases of a switch share one block scope, so their lexical - // names contribute together to the forbidden set. - let mut inner = forbidden.clone(); - for case in &switch_stmt.cases { - collect_lexical_decl_names(&case.cons, &mut inner); - } - for case in &switch_stmt.cases { - for stmt in &case.cons { - annexb_nested_stmt(stmt, &inner, all_out, var_out); - } - } - } - ast::Stmt::Try(try_stmt) => { - annexb_nested_block(&try_stmt.block.stmts, forbidden, all_out, var_out); - if let Some(handler) = &try_stmt.handler { - // B.3.5: a `var` whose name is also a bound name of a - // *destructuring* CatchParameter is an early error, so the - // equivalent AnnexB legacy `var` for a same-named block - // function in the handler body must be skipped. The B.3.5 - // exception only exempts a simple `catch (e)` BindingIdentifier - // (where the var IS allowed), so only pattern catch params - // (`catch ({ f })` / `catch ([f])`) contribute to `forbidden`. - let mut handler_forbidden; - let inner = match &handler.param { - Some(param) if !matches!(param, ast::Pat::Ident(_)) => { - handler_forbidden = forbidden.clone(); - let mut names = Vec::new(); - collect_var_binding_names_from_pat(param, &mut names); - handler_forbidden.extend(names); - &handler_forbidden - } - _ => forbidden, - }; - annexb_nested_block(&handler.body.stmts, inner, all_out, var_out); - } - if let Some(finalizer) = &try_stmt.finalizer { - annexb_nested_block(&finalizer.stmts, forbidden, all_out, var_out); - } - } - ast::Stmt::With(with_stmt) => { - annexb_nested_stmt(&with_stmt.body, forbidden, all_out, var_out) - } - _ => {} - } -} - -/// Lexical (`let`/`const`) binding names introduced by a `VarDecl`. A `var` -/// declaration introduces no lexical names and yields an empty list. -fn var_decl_lexical_names(vd: &ast::VarDecl) -> Vec { - if vd.kind == ast::VarDeclKind::Var { - return Vec::new(); - } - let mut names = Vec::new(); - for decl in &vd.decls { - collect_var_binding_names_from_pat(&decl.name, &mut names); - } - names -} - -/// Lexical binding names of a `for-in` / `for-of` head (`for (let f in …)`). -/// A `var` head or a bare assignment-target pattern introduces no lexical -/// binding here and yields an empty list. -fn for_head_lexical_names(head: &ast::ForHead) -> Vec { - match head { - ast::ForHead::VarDecl(vd) => var_decl_lexical_names(vd), - _ => Vec::new(), - } -} - -/// Descend into a loop body, adding the loop head's lexical binding names to -/// the forbidden set so a same-named block function in the body skips its -/// AnnexB legacy `var` (the equivalent `var` would be an early error). -fn annexb_nested_loop_body( - body: &ast::Stmt, - lexical_names: Vec, - forbidden: &std::collections::HashSet, - all_out: &mut Vec, - var_out: &mut Vec, -) { - if lexical_names.is_empty() { - annexb_nested_stmt(body, forbidden, all_out, var_out); - } else { - let mut inner = forbidden.clone(); - inner.extend(lexical_names); - annexb_nested_stmt(body, &inner, all_out, var_out); - } -} - -fn annexb_nested_block( - stmts: &[ast::Stmt], - forbidden: &std::collections::HashSet, - all_out: &mut Vec, - var_out: &mut Vec, -) { - let mut inner = forbidden.clone(); - collect_lexical_decl_names(stmts, &mut inner); - for stmt in stmts { - annexb_nested_stmt(stmt, &inner, all_out, var_out); - } -} - /// Returns the (name, id) pairs newly created here (i.e. names that did not /// already have a binding in the current scope, like a same-named param). /// The caller emits an undefined-initialised `Stmt::Let` for each at body diff --git a/crates/perry-hir/src/lower_decl/block/closure_ident_scan.rs b/crates/perry-hir/src/lower_decl/block/closure_ident_scan.rs new file mode 100644 index 0000000000..040bb46c88 --- /dev/null +++ b/crates/perry-hir/src/lower_decl/block/closure_ident_scan.rs @@ -0,0 +1,344 @@ +//! `cic_*` — the "collect identifiers referenced from inside a closure" +//! traversal used by `pre_register_forward_captured_lets`. +//! +//! Moved verbatim out of `lower_decl/block.rs` (file-size gate). + +use swc_ecma_ast as ast; + +pub(super) fn cic_stmt(s: &ast::Stmt, in_cl: bool, out: &mut std::collections::HashSet) { + use ast::Stmt::*; + match s { + Block(b) => b.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)), + Return(r) => { + if let Some(a) = &r.arg { + cic_expr(a, in_cl, out); + } + } + Expr(e) => cic_expr(&e.expr, in_cl, out), + If(i) => { + cic_expr(&i.test, in_cl, out); + cic_stmt(&i.cons, in_cl, out); + if let Some(a) = &i.alt { + cic_stmt(a, in_cl, out); + } + } + Throw(t) => cic_expr(&t.arg, in_cl, out), + While(w) => { + cic_expr(&w.test, in_cl, out); + cic_stmt(&w.body, in_cl, out); + } + DoWhile(w) => { + cic_expr(&w.test, in_cl, out); + cic_stmt(&w.body, in_cl, out); + } + For(f) => { + if let Some(init) = &f.init { + match init { + ast::VarDeclOrExpr::Expr(e) => cic_expr(e, in_cl, out), + ast::VarDeclOrExpr::VarDecl(vd) => vd.decls.iter().for_each(|d| { + if let Some(i) = &d.init { + cic_expr(i, in_cl, out); + } + }), + } + } + if let Some(t) = &f.test { + cic_expr(t, in_cl, out); + } + if let Some(u) = &f.update { + cic_expr(u, in_cl, out); + } + cic_stmt(&f.body, in_cl, out); + } + ForIn(f) => { + cic_expr(&f.right, in_cl, out); + cic_stmt(&f.body, in_cl, out); + } + ForOf(f) => { + cic_expr(&f.right, in_cl, out); + cic_stmt(&f.body, in_cl, out); + } + Try(t) => { + t.block.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)); + if let Some(h) = &t.handler { + h.body.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)); + } + if let Some(f) = &t.finalizer { + f.stmts.iter().for_each(|st| cic_stmt(st, in_cl, out)); + } + } + Switch(sw) => { + cic_expr(&sw.discriminant, in_cl, out); + for c in &sw.cases { + if let Some(t) = &c.test { + cic_expr(t, in_cl, out); + } + c.cons.iter().for_each(|st| cic_stmt(st, in_cl, out)); + } + } + Labeled(l) => cic_stmt(&l.body, in_cl, out), + With(w) => { + cic_expr(&w.obj, in_cl, out); + cic_stmt(&w.body, in_cl, out); + } + Decl(d) => cic_decl(d, in_cl, out), + _ => {} + } +} + +fn cic_decl(d: &ast::Decl, in_cl: bool, out: &mut std::collections::HashSet) { + match d { + ast::Decl::Var(vd) => vd.decls.iter().for_each(|de| { + if let Some(i) = &de.init { + cic_expr(i, in_cl, out); + } + }), + // A nested function declaration's body is a closure scope. + ast::Decl::Fn(f) => cic_function(&f.function, out), + ast::Decl::Class(c) => cic_class(&c.class, in_cl, out), + _ => {} + } +} + +/// Param patterns (defaults evaluate at CALL time) + body of a closure-scoped +/// `ast::Function` — nested fn declarations/expressions and class methods all +/// share this traversal. +fn cic_function(f: &ast::Function, out: &mut std::collections::HashSet) { + for p in &f.params { + cic_pat(&p.pat, true, out); + } + if let Some(b) = &f.body { + b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); + } +} + +fn cic_class(c: &ast::Class, in_cl: bool, out: &mut std::collections::HashSet) { + if let Some(sc) = &c.super_class { + cic_expr(sc, in_cl, out); + } + for m in &c.body { + match m { + ast::ClassMember::Method(mm) => cic_function(&mm.function, out), + ast::ClassMember::PrivateMethod(mm) => cic_function(&mm.function, out), + // #6523: the CONSTRUCTOR body runs at `new` time, not at class + // definition — a binding it references that is declared AFTER the + // class (`class C { constructor(){ a() } } const a = ...`) must be + // pre-registered as a forward-captured lexical exactly like a + // method-body reference. This arm was missing, so such refs never + // got a box: `collect_method_captures` dropped them (not in + // `ctx.locals` at the class decl) and the ref fell through to the + // global lookup — "a is not defined" at construction (bundled + // semver's Comparator debug/constant pattern). + ast::ClassMember::Constructor(ctor) => { + for p in &ctor.params { + match p { + ast::ParamOrTsParamProp::Param(p) => cic_pat(&p.pat, true, out), + ast::ParamOrTsParamProp::TsParamProp(tp) => { + if let ast::TsParamPropParam::Assign(a) = &tp.param { + cic_expr(&a.right, true, out); + } + } + } + } + if let Some(b) = &ctor.body { + b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); + } + } + ast::ClassMember::ClassProp(p) => { + if let Some(v) = &p.value { + cic_expr(v, true, out); + } + } + ast::ClassMember::PrivateProp(p) => { + if let Some(v) = &p.value { + cic_expr(v, true, out); + } + } + ast::ClassMember::StaticBlock(sb) => { + sb.body.stmts.iter().for_each(|st| cic_stmt(st, true, out)); + } + _ => {} + } + } +} + +pub(super) fn cic_expr(e: &ast::Expr, in_cl: bool, out: &mut std::collections::HashSet) { + use ast::Expr::*; + match e { + Ident(i) => { + if in_cl { + out.insert(i.sym.to_string()); + } + } + Arrow(a) => { + for p in &a.params { + cic_pat(p, true, out); + } + match &*a.body { + ast::BlockStmtOrExpr::BlockStmt(b) => { + b.stmts.iter().for_each(|st| cic_stmt(st, true, out)) + } + ast::BlockStmtOrExpr::Expr(ex) => cic_expr(ex, true, out), + } + } + Fn(f) => cic_function(&f.function, out), + Class(c) => cic_class(&c.class, in_cl, out), + Array(a) => a + .elems + .iter() + .flatten() + .for_each(|el| cic_expr(&el.expr, in_cl, out)), + Object(o) => { + for p in &o.props { + match p { + ast::PropOrSpread::Spread(s) => cic_expr(&s.expr, in_cl, out), + ast::PropOrSpread::Prop(pr) => cic_prop(pr, in_cl, out), + } + } + } + Unary(u) => cic_expr(&u.arg, in_cl, out), + Update(u) => cic_expr(&u.arg, in_cl, out), + Bin(b) => { + cic_expr(&b.left, in_cl, out); + cic_expr(&b.right, in_cl, out); + } + Assign(a) => { + cic_assign_target(&a.left, in_cl, out); + cic_expr(&a.right, in_cl, out); + } + Member(m) => { + cic_expr(&m.obj, in_cl, out); + if let ast::MemberProp::Computed(c) = &m.prop { + cic_expr(&c.expr, in_cl, out); + } + } + Cond(c) => { + cic_expr(&c.test, in_cl, out); + cic_expr(&c.cons, in_cl, out); + cic_expr(&c.alt, in_cl, out); + } + Call(c) => { + if let ast::Callee::Expr(e) = &c.callee { + cic_expr(e, in_cl, out); + } + c.args.iter().for_each(|a| cic_expr(&a.expr, in_cl, out)); + } + New(n) => { + cic_expr(&n.callee, in_cl, out); + if let Some(args) = &n.args { + args.iter().for_each(|a| cic_expr(&a.expr, in_cl, out)); + } + } + Seq(s) => s.exprs.iter().for_each(|e| cic_expr(e, in_cl, out)), + Tpl(t) => t.exprs.iter().for_each(|e| cic_expr(e, in_cl, out)), + TaggedTpl(t) => { + cic_expr(&t.tag, in_cl, out); + t.tpl.exprs.iter().for_each(|e| cic_expr(e, in_cl, out)); + } + Paren(p) => cic_expr(&p.expr, in_cl, out), + Await(a) => cic_expr(&a.arg, in_cl, out), + Yield(y) => { + if let Some(a) = &y.arg { + cic_expr(a, in_cl, out); + } + } + OptChain(o) => match &*o.base { + ast::OptChainBase::Member(m) => { + cic_expr(&m.obj, in_cl, out); + if let ast::MemberProp::Computed(c) = &m.prop { + cic_expr(&c.expr, in_cl, out); + } + } + ast::OptChainBase::Call(c) => { + cic_expr(&c.callee, in_cl, out); + c.args.iter().for_each(|a| cic_expr(&a.expr, in_cl, out)); + } + }, + _ => {} + } +} + +fn cic_pat(p: &ast::Pat, in_cl: bool, out: &mut std::collections::HashSet) { + match p { + ast::Pat::Assign(a) => { + cic_pat(&a.left, in_cl, out); + cic_expr(&a.right, in_cl, out); + } + ast::Pat::Array(arr) => arr + .elems + .iter() + .flatten() + .for_each(|el| cic_pat(el, in_cl, out)), + ast::Pat::Object(o) => { + for pp in &o.props { + match pp { + ast::ObjectPatProp::KeyValue(kv) => cic_pat(&kv.value, in_cl, out), + ast::ObjectPatProp::Assign(a) => { + if let Some(v) = &a.value { + cic_expr(v, in_cl, out); + } + } + ast::ObjectPatProp::Rest(r) => cic_pat(&r.arg, in_cl, out), + } + } + } + ast::Pat::Rest(r) => cic_pat(&r.arg, in_cl, out), + _ => {} + } +} + +fn cic_prop(p: &ast::Prop, in_cl: bool, out: &mut std::collections::HashSet) { + match p { + ast::Prop::Shorthand(i) => { + if in_cl { + out.insert(i.sym.to_string()); + } + } + ast::Prop::KeyValue(kv) => { + if let ast::PropName::Computed(c) = &kv.key { + cic_expr(&c.expr, in_cl, out); + } + cic_expr(&kv.value, in_cl, out); + } + ast::Prop::Getter(g) => { + if let Some(b) = &g.body { + b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); + } + } + ast::Prop::Setter(s) => { + if let Some(b) = &s.body { + b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); + } + } + ast::Prop::Method(m) => { + if let Some(b) = &m.function.body { + b.stmts.iter().for_each(|st| cic_stmt(st, true, out)); + } + } + ast::Prop::Assign(a) => cic_expr(&a.value, in_cl, out), + } +} + +fn cic_assign_target( + t: &ast::AssignTarget, + in_cl: bool, + out: &mut std::collections::HashSet, +) { + if let ast::AssignTarget::Simple(s) = t { + match s { + ast::SimpleAssignTarget::Ident(i) => { + if in_cl { + out.insert(i.id.sym.to_string()); + } + } + ast::SimpleAssignTarget::Member(m) => { + cic_expr(&m.obj, in_cl, out); + if let ast::MemberProp::Computed(c) = &m.prop { + cic_expr(&c.expr, in_cl, out); + } + } + ast::SimpleAssignTarget::Paren(p) => cic_expr(&p.expr, in_cl, out), + _ => {} + } + } +} diff --git a/crates/perry-hir/src/lower_decl/block/var_names.rs b/crates/perry-hir/src/lower_decl/block/var_names.rs new file mode 100644 index 0000000000..af257d1469 --- /dev/null +++ b/crates/perry-hir/src/lower_decl/block/var_names.rs @@ -0,0 +1,331 @@ +//! `var` / lexical binding-name collection and the Annex B B.3.3 scan. +//! +//! Moved verbatim out of `lower_decl/block.rs` (file-size gate); `block.rs` +//! re-exports the `pub(crate)` entry points so `crate::lower_decl::*` keeps +//! the exact same shape. + +use swc_ecma_ast as ast; + +pub(crate) fn collect_var_binding_names_from_pat(pat: &ast::Pat, out: &mut Vec) { + match pat { + ast::Pat::Ident(ident) => out.push(ident.id.sym.to_string()), + ast::Pat::Array(arr) => { + for elem in arr.elems.iter().flatten() { + collect_var_binding_names_from_pat(elem, out); + } + } + ast::Pat::Object(obj) => { + for prop in &obj.props { + match prop { + ast::ObjectPatProp::Assign(assign) => out.push(assign.key.sym.to_string()), + ast::ObjectPatProp::KeyValue(kv) => { + collect_var_binding_names_from_pat(&kv.value, out) + } + ast::ObjectPatProp::Rest(rest) => { + collect_var_binding_names_from_pat(&rest.arg, out) + } + } + } + } + ast::Pat::Assign(assign) => collect_var_binding_names_from_pat(&assign.left, out), + ast::Pat::Rest(rest) => collect_var_binding_names_from_pat(&rest.arg, out), + _ => {} + } +} + +fn collect_var_binding_names_from_var_decl(var_decl: &ast::VarDecl, out: &mut Vec) { + if var_decl.kind != ast::VarDeclKind::Var { + return; + } + for decl in &var_decl.decls { + collect_var_binding_names_from_pat(&decl.name, out); + } +} + +pub(crate) fn collect_var_binding_names_from_stmt(stmt: &ast::Stmt, out: &mut Vec) { + match stmt { + ast::Stmt::Block(block) => { + for stmt in &block.stmts { + collect_var_binding_names_from_stmt(stmt, out); + } + } + ast::Stmt::Decl(ast::Decl::Var(var_decl)) => { + collect_var_binding_names_from_var_decl(var_decl, out); + } + // Nested function/class bodies have their own var environments. + ast::Stmt::Decl(ast::Decl::Fn(_)) | ast::Stmt::Decl(ast::Decl::Class(_)) => {} + ast::Stmt::If(if_stmt) => { + collect_var_binding_names_from_stmt(&if_stmt.cons, out); + if let Some(alt) = &if_stmt.alt { + collect_var_binding_names_from_stmt(alt, out); + } + } + ast::Stmt::While(while_stmt) => collect_var_binding_names_from_stmt(&while_stmt.body, out), + ast::Stmt::DoWhile(do_while) => collect_var_binding_names_from_stmt(&do_while.body, out), + ast::Stmt::For(for_stmt) => { + if let Some(ast::VarDeclOrExpr::VarDecl(var_decl)) = &for_stmt.init { + collect_var_binding_names_from_var_decl(var_decl, out); + } + collect_var_binding_names_from_stmt(&for_stmt.body, out); + } + ast::Stmt::ForIn(for_in) => { + if let ast::ForHead::VarDecl(var_decl) = &for_in.left { + collect_var_binding_names_from_var_decl(var_decl, out); + } + collect_var_binding_names_from_stmt(&for_in.body, out); + } + ast::Stmt::ForOf(for_of) => { + if let ast::ForHead::VarDecl(var_decl) = &for_of.left { + collect_var_binding_names_from_var_decl(var_decl, out); + } + collect_var_binding_names_from_stmt(&for_of.body, out); + } + ast::Stmt::Labeled(labeled) => collect_var_binding_names_from_stmt(&labeled.body, out), + ast::Stmt::Switch(switch_stmt) => { + for case in &switch_stmt.cases { + for stmt in &case.cons { + collect_var_binding_names_from_stmt(stmt, out); + } + } + } + ast::Stmt::Try(try_stmt) => { + for stmt in &try_stmt.block.stmts { + collect_var_binding_names_from_stmt(stmt, out); + } + if let Some(handler) = &try_stmt.handler { + for stmt in &handler.body.stmts { + collect_var_binding_names_from_stmt(stmt, out); + } + } + if let Some(finalizer) = &try_stmt.finalizer { + for stmt in &finalizer.stmts { + collect_var_binding_names_from_stmt(stmt, out); + } + } + } + ast::Stmt::With(with_stmt) => collect_var_binding_names_from_stmt(&with_stmt.body, out), + _ => {} + } +} + +/// Collect the lexically-declared names (`let` / `const` / `class`) at the top +/// level of a statement list. A `var` or a `function` declaration is NOT +/// lexical and does not belong here. Used to build the Annex B "forbidden" set: +/// a block-level function declaration whose name collides with a lexical +/// binding in an enclosing scope would make the equivalent `var` an early +/// error, so B.3.3 skips creating the enclosing-scope `var`. +pub(crate) fn collect_lexical_decl_names( + stmts: &[ast::Stmt], + out: &mut std::collections::HashSet, +) { + for stmt in stmts { + match stmt { + ast::Stmt::Decl(ast::Decl::Var(var_decl)) if var_decl.kind != ast::VarDeclKind::Var => { + for decl in &var_decl.decls { + let mut names = Vec::new(); + collect_var_binding_names_from_pat(&decl.name, &mut names); + out.extend(names); + } + } + ast::Stmt::Decl(ast::Decl::Class(class_decl)) => { + out.insert(class_decl.ident.sym.to_string()); + } + _ => {} + } + } +} + +/// Annex B B.3.3 (#5297): collect the names of function declarations that +/// appear *inside a nested block* of a function/program body. In sloppy mode +/// such a legacy block-level function declaration ALSO creates a `var`-style +/// binding in the enclosing function/global scope (`f` is visible — as a `var` +/// initialised to `undefined` until the declaration runs — outside the block). +/// +/// `body_stmts` are the body's own top-level statements: a `function f(){}` +/// directly among them is an ordinary FunctionDeclaration (already function- +/// scoped) and is NOT collected; every function declaration reached by +/// descending through a block / `if` branch / loop body / `switch` case / +/// `try` part / labeled / `with` body IS. `forbidden` seeds the names for which +/// the legacy `var` must be skipped — the spec gates B.3.3 on "replacing the +/// FunctionDeclaration with a `var` produces no early error and the name is not +/// a parameter": callers pass the parameter names, the body's own top-level +/// lexical names, and `"arguments"`. As we descend, each block contributes its +/// own `let`/`const`/`class` names to the forbidden set for everything nested +/// within it (so `{ let f; { function f(){} } }` is correctly skipped). Nested +/// function and class bodies own their own var environment and are not entered. +/// One traversal yields two results: +/// - `all_out`: EVERY block-nested function declaration name. Every block-level +/// function declaration is block-scoped (gets its own binding), so +/// `lower_nested_fn_decl` gives these a fresh local rather than clobbering an +/// enclosing same-named parameter/binding. +/// - `var_out`: the subset that ALSO gets the legacy enclosing-scope `var` — +/// names not in `forbidden` and not shadowed by an enclosing block's +/// `let`/`const`/`class` (which would make `var f` an early error). +pub(crate) fn collect_annexb_block_fn_decl_names( + body_stmts: &[ast::Stmt], + forbidden: &std::collections::HashSet, + all_out: &mut Vec, + var_out: &mut Vec, +) { + for stmt in body_stmts { + // A direct top-level function declaration is already function-scoped. + if matches!(stmt, ast::Stmt::Decl(ast::Decl::Fn(_))) { + continue; + } + annexb_nested_stmt(stmt, forbidden, all_out, var_out); + } +} + +fn annexb_nested_stmt( + stmt: &ast::Stmt, + forbidden: &std::collections::HashSet, + all_out: &mut Vec, + var_out: &mut Vec, +) { + match stmt { + ast::Stmt::Decl(ast::Decl::Fn(fn_decl)) => { + let name = fn_decl.ident.sym.to_string(); + all_out.push(name.clone()); + if !forbidden.contains(&name) { + var_out.push(name); + } + } + // Nested function/class bodies have their own var environment. + ast::Stmt::Decl(ast::Decl::Class(_)) => {} + ast::Stmt::Block(block) => annexb_nested_block(&block.stmts, forbidden, all_out, var_out), + ast::Stmt::If(if_stmt) => { + annexb_nested_stmt(&if_stmt.cons, forbidden, all_out, var_out); + if let Some(alt) = &if_stmt.alt { + annexb_nested_stmt(alt, forbidden, all_out, var_out); + } + } + ast::Stmt::While(while_stmt) => { + annexb_nested_stmt(&while_stmt.body, forbidden, all_out, var_out) + } + ast::Stmt::DoWhile(do_while) => { + annexb_nested_stmt(&do_while.body, forbidden, all_out, var_out) + } + // A `for`/`for-in`/`for-of` lexical head (`for (let f; ...)`, + // `for (let f in/of ...)`) introduces a binding whose scope encloses + // the loop body; an equivalent `var f` in the body is an early error + // (14.7.4.1 / 14.7.5.1), so the AnnexB legacy `var` for a same-named + // block function in the body must be skipped. + ast::Stmt::For(for_stmt) => { + let names = match &for_stmt.init { + Some(ast::VarDeclOrExpr::VarDecl(vd)) => var_decl_lexical_names(vd), + _ => Vec::new(), + }; + annexb_nested_loop_body(&for_stmt.body, names, forbidden, all_out, var_out); + } + ast::Stmt::ForIn(for_in) => { + let names = for_head_lexical_names(&for_in.left); + annexb_nested_loop_body(&for_in.body, names, forbidden, all_out, var_out); + } + ast::Stmt::ForOf(for_of) => { + let names = for_head_lexical_names(&for_of.left); + annexb_nested_loop_body(&for_of.body, names, forbidden, all_out, var_out); + } + ast::Stmt::Labeled(labeled) => { + annexb_nested_stmt(&labeled.body, forbidden, all_out, var_out) + } + ast::Stmt::Switch(switch_stmt) => { + // All cases of a switch share one block scope, so their lexical + // names contribute together to the forbidden set. + let mut inner = forbidden.clone(); + for case in &switch_stmt.cases { + collect_lexical_decl_names(&case.cons, &mut inner); + } + for case in &switch_stmt.cases { + for stmt in &case.cons { + annexb_nested_stmt(stmt, &inner, all_out, var_out); + } + } + } + ast::Stmt::Try(try_stmt) => { + annexb_nested_block(&try_stmt.block.stmts, forbidden, all_out, var_out); + if let Some(handler) = &try_stmt.handler { + // B.3.5: a `var` whose name is also a bound name of a + // *destructuring* CatchParameter is an early error, so the + // equivalent AnnexB legacy `var` for a same-named block + // function in the handler body must be skipped. The B.3.5 + // exception only exempts a simple `catch (e)` BindingIdentifier + // (where the var IS allowed), so only pattern catch params + // (`catch ({ f })` / `catch ([f])`) contribute to `forbidden`. + let mut handler_forbidden; + let inner = match &handler.param { + Some(param) if !matches!(param, ast::Pat::Ident(_)) => { + handler_forbidden = forbidden.clone(); + let mut names = Vec::new(); + collect_var_binding_names_from_pat(param, &mut names); + handler_forbidden.extend(names); + &handler_forbidden + } + _ => forbidden, + }; + annexb_nested_block(&handler.body.stmts, inner, all_out, var_out); + } + if let Some(finalizer) = &try_stmt.finalizer { + annexb_nested_block(&finalizer.stmts, forbidden, all_out, var_out); + } + } + ast::Stmt::With(with_stmt) => { + annexb_nested_stmt(&with_stmt.body, forbidden, all_out, var_out) + } + _ => {} + } +} + +/// Lexical (`let`/`const`) binding names introduced by a `VarDecl`. A `var` +/// declaration introduces no lexical names and yields an empty list. +fn var_decl_lexical_names(vd: &ast::VarDecl) -> Vec { + if vd.kind == ast::VarDeclKind::Var { + return Vec::new(); + } + let mut names = Vec::new(); + for decl in &vd.decls { + collect_var_binding_names_from_pat(&decl.name, &mut names); + } + names +} + +/// Lexical binding names of a `for-in` / `for-of` head (`for (let f in …)`). +/// A `var` head or a bare assignment-target pattern introduces no lexical +/// binding here and yields an empty list. +fn for_head_lexical_names(head: &ast::ForHead) -> Vec { + match head { + ast::ForHead::VarDecl(vd) => var_decl_lexical_names(vd), + _ => Vec::new(), + } +} + +/// Descend into a loop body, adding the loop head's lexical binding names to +/// the forbidden set so a same-named block function in the body skips its +/// AnnexB legacy `var` (the equivalent `var` would be an early error). +fn annexb_nested_loop_body( + body: &ast::Stmt, + lexical_names: Vec, + forbidden: &std::collections::HashSet, + all_out: &mut Vec, + var_out: &mut Vec, +) { + if lexical_names.is_empty() { + annexb_nested_stmt(body, forbidden, all_out, var_out); + } else { + let mut inner = forbidden.clone(); + inner.extend(lexical_names); + annexb_nested_stmt(body, &inner, all_out, var_out); + } +} + +fn annexb_nested_block( + stmts: &[ast::Stmt], + forbidden: &std::collections::HashSet, + all_out: &mut Vec, + var_out: &mut Vec, +) { + let mut inner = forbidden.clone(); + collect_lexical_decl_names(stmts, &mut inner); + for stmt in stmts { + annexb_nested_stmt(stmt, &inner, all_out, var_out); + } +} diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 1c9aae9614..878fc6ddde 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -29,13 +29,17 @@ //! 3rd argument is `instanceof Number`/`Boolean`), //! * a symbol/bigint → no indexed properties → an empty array-like (length 0). +use super::generic_object::{ + arg_at, object_concat, object_pop, object_push, object_reverse, object_shift, object_sort, + object_splice, object_unshift, +}; use super::*; use crate::closure::{js_closure_call3, ClosureHeader}; use crate::value::{JSValue, TAG_HOLE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED}; use std::ptr; #[inline(always)] -fn undef() -> f64 { +pub(super) fn undef() -> f64 { f64::from_bits(TAG_UNDEFINED) } @@ -207,7 +211,7 @@ enum PtrKind { Exotic, } -fn proxy_string_key(k: i64) -> f64 { +pub(super) fn proxy_string_key(k: i64) -> f64 { let s = k.to_string(); let key = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); f64::from_bits(JSValue::string_ptr(key).bits()) @@ -521,7 +525,7 @@ fn object_get_property_chain(obj_ptr: usize, k: i64) -> f64 { } /// `HasProperty(ToObject(recv), k)`. -fn al_has(recv: f64, k: i64) -> bool { +pub(super) fn al_has(recv: f64, k: i64) -> bool { if k < 0 { return false; } @@ -1154,157 +1158,6 @@ static KEEP_ARRAYLIKE_JOIN: extern "C" fn(f64, f64) -> f64 = js_arraylike_join; #[used] static KEEP_ARRAYLIKE_SLICE: extern "C" fn(f64, f64, i32, f64, i32) -> f64 = js_arraylike_slice; -// --------------------------------------------------------------------------- -// Generic array-like MUTATORS over a plain-object receiver (#4742 follow-up). -// -// `Array.prototype.{pop,shift,push,unshift,reverse,splice}` are intentionally -// generic (ECMA-262 §23.1.3) — they operate on `O = ToObject(this)` with live -// `Get`/`Set`/`Delete`/`HasProperty` and a writable `length`. Perry's dense -// fast paths assume a real `ArrayHeader`; when the receiver is a plain object -// (a stored `obj.pop = Array.prototype.pop; obj.pop()` borrow, or -// `Array.prototype.pop.call(obj, …)`), the dense path read the object's -// `ObjectHeader` words as an `ArrayHeader` and corrupted/crashed -// (`TypeError: Cannot convert object to primitive value`). -// -// These helpers run the spec algorithm by mutating the *original* receiver -// object in place via the polymorphic index get/set/delete and a `length` -// property write. They are dispatched from `js_native_call_method` only when -// the receiver classifies as a plain `Object` (never a real array / typed -// array / buffer / primitive), so the hot real-array paths are untouched. -// --------------------------------------------------------------------------- - -/// `Set(O, ToString(k), v, true)` for an array-like object receiver. -fn al_set(recv: f64, k: i64, v: f64) { - if crate::proxy::js_proxy_is_proxy(recv) != 0 { - crate::proxy::js_proxy_set(recv, proxy_string_key(k), v); - return; - } - crate::object::js_object_set_index_polymorphic(recv.to_bits() as i64, k as f64, v); -} - -/// `DeletePropertyOrThrow(O, ToString(k))` for an array-like object receiver. -fn al_delete(recv: f64, k: i64) { - if crate::proxy::js_proxy_is_proxy(recv) != 0 { - crate::proxy::js_proxy_delete(recv, proxy_string_key(k)); - return; - } - let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut crate::object::ObjectHeader; - crate::object::js_object_delete_dynamic(raw, k as f64); -} - -/// `Set(O, "length", len, true)` for an array-like object receiver. An own -/// `length` ACCESSOR fires its setter — and throws TypeError when there is -/// none (getter-only `length`, test262 splice/S15.4.4.12_A6.1_T3), matching -/// `Set(..., true)` on a non-writable slot. -fn al_set_length(recv: f64, len: i64) { - let raw_addr = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if let Some(acc) = crate::object::get_accessor_descriptor(raw_addr, "length") { - if acc.set != 0 { - unsafe { crate::object::invoke_accessor_setter(acc.set, recv, len as f64) }; - return; - } - crate::collection_iter::throw_type_error( - "Cannot set property length of object which has only a getter", - ); - } - // An object-LITERAL `get length()` lives in the anon-shape class vtable, - // not the defineProperty descriptor table — a getter with no setter makes - // `Set(O, "length", ..., true)` throw (test262 splice/S15.4.4.12_A6.1_T3). - { - let raw = raw_addr as *const crate::object::ObjectHeader; - let class_id = crate::object::js_object_get_class_id(raw); - if class_id != 0 { - if let Some((getter, setter)) = - crate::object::class_own_accessor_ptrs(class_id, "length") - { - if setter == 0 && getter != 0 { - crate::collection_iter::throw_type_error( - "Cannot set property length of object which has only a getter", - ); - } - } - } - } - // `Set(O, "length", …, true)` (Throw=true) must throw a TypeError when the - // set fails: a frozen array, an array/object whose `length` is a - // non-writable data property (`defineProperty(o,"length",{writable:false})` - // or a function's intrinsic `length`), or a frozen plain object. The - // by-name setter below silently no-ops in those cases (PutValue's - // non-strict fall-through), so detect the failure up front and throw. - if al_length_write_would_fail(recv, raw_addr) { - crate::collection_iter::throw_type_error( - "Cannot assign to read only property 'length' of object", - ); - } - let raw = raw_addr as *mut crate::object::ObjectHeader; - let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - crate::object::js_object_set_field_by_name(raw, key, len as f64); -} - -/// True when `Set(recv, "length", …)` would fail (and so must throw under -/// `Throw=true`): a frozen array, a non-writable `length` data property, or a -/// callable receiver (a function's `length` is non-writable by spec). -fn al_length_write_would_fail(recv: f64, raw_addr: usize) -> bool { - // Frozen array: `length` is non-writable after `Object.freeze`. - let arr = as_real_array(recv); - if !arr.is_null() { - if crate::array::array_is_frozen(arr) { - return true; - } - return crate::object::get_property_attrs(raw_addr, "length") - .map(|a| !a.writable()) - .unwrap_or(false); - } - // Non-array receivers: a recorded non-writable `length` descriptor - // (`defineProperty(o,"length",{writable:false})`) or a callable receiver - // whose intrinsic `length` is non-writable. - if crate::object::get_property_attrs(raw_addr, "length") - .map(|a| !a.writable()) - .unwrap_or(false) - { - return true; - } - crate::closure::is_closure_ptr(raw_addr) -} - -/// `ToIntegerOrInfinity(v)` as an `f64` (NaN → 0; ±Infinity preserved). -fn to_integer_or_infinity(v: f64) -> f64 { - if v.is_nan() { - 0.0 - } else if v.is_infinite() { - v - } else { - v.trunc() - } -} - -/// Resolve a relative index argument (`splice` start) to an absolute, -/// clamped `[0, len]` index. -fn relative_index(v: f64, len: i64) -> i64 { - let n = to_integer_or_infinity(v); - if n < 0.0 { - let r = len as f64 + n; - if r < 0.0 { - 0 - } else { - r as i64 - } - } else if n > len as f64 { - len - } else { - n as i64 - } -} - -#[inline] -fn arg_at(args_ptr: *const f64, args_len: usize, i: usize) -> f64 { - if i < args_len && !args_ptr.is_null() { - unsafe { *args_ptr.add(i) } - } else { - undef() - } -} - /// A receiver that reached a dense `ArrayHeader` entry point but is actually /// a plain object/closure at runtime (a variable whose static type was /// inferred `Array` and later reassigned — `var x = [1, 0]; … x = {0:1,1:0}; @@ -1353,303 +1206,6 @@ pub fn plain_object_value(arr: *const ArrayHeader) -> Option { Some(recv) } -/// `Array.prototype.pop` over an array-like object receiver. -pub(crate) fn object_pop(recv: f64) -> f64 { - let len = al_length(recv); - if len <= 0 { - al_set_length(recv, 0); - return undef(); - } - let new_len = len - 1; - let element = al_get(recv, new_len); - al_delete(recv, new_len); - al_set_length(recv, new_len); - element -} - -/// `Array.prototype.shift` over an array-like object receiver. -pub(crate) fn object_shift(recv: f64) -> f64 { - let len = al_length(recv); - if len <= 0 { - al_set_length(recv, 0); - return undef(); - } - let first = al_get(recv, 0); - for k in 1..len { - if al_has(recv, k) { - al_set(recv, k - 1, al_get(recv, k)); - } else { - al_delete(recv, k - 1); - } - } - al_delete(recv, len - 1); - al_set_length(recv, len - 1); - first -} - -/// `Array.prototype.push` over an array-like object receiver. Returns the new -/// length. -fn object_push(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { - let len = al_length(recv); - for i in 0..args_len { - al_set(recv, len + i as i64, arg_at(args_ptr, args_len, i)); - } - let new_len = len + args_len as i64; - al_set_length(recv, new_len); - new_len as f64 -} - -/// `Array.prototype.unshift` over an array-like object receiver. Returns the -/// new length. -fn object_unshift(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { - let len = al_length(recv); - let count = args_len as i64; - if count > 0 { - // Move existing elements up by `count`, high index first so we don't - // clobber not-yet-moved slots. - let mut k = len; - while k > 0 { - let from = k - 1; - let to = from + count; - if al_has(recv, from) { - al_set(recv, to, al_get(recv, from)); - } else { - al_delete(recv, to); - } - k -= 1; - } - for j in 0..count { - al_set(recv, j, arg_at(args_ptr, args_len, j as usize)); - } - } - let new_len = len + count; - al_set_length(recv, new_len); - new_len as f64 -} - -/// `Array.prototype.reverse` over an array-like object receiver. Returns the -/// receiver. -fn object_reverse(recv: f64) -> f64 { - let len = al_length(recv); - let middle = len / 2; - let mut lower = 0; - while lower < middle { - let upper = len - 1 - lower; - let lower_exists = al_has(recv, lower); - let upper_exists = al_has(recv, upper); - let lower_val = al_get(recv, lower); - let upper_val = al_get(recv, upper); - match (lower_exists, upper_exists) { - (true, true) => { - al_set(recv, lower, upper_val); - al_set(recv, upper, lower_val); - } - (false, true) => { - al_set(recv, lower, upper_val); - al_delete(recv, upper); - } - (true, false) => { - al_delete(recv, lower); - al_set(recv, upper, lower_val); - } - (false, false) => {} - } - lower += 1; - } - recv -} - -/// `Array.prototype.splice` over an array-like object receiver. Returns a fresh -/// plain array of the removed elements (holes preserved). -pub(crate) fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { - let len = al_length(recv); - let actual_start = relative_index(arg_at(args_ptr, args_len, 0), len); - let delete_count = if args_len == 0 { - 0 - } else if args_len == 1 { - len - actual_start - } else { - let dc = to_integer_or_infinity(arg_at(args_ptr, args_len, 1)); - dc.max(0.0).min((len - actual_start) as f64) as i64 - }; - // Removed elements -> fresh plain array (holes preserved). ArrayCreate - // throws RangeError for a count ≥ 2^32 (splice/create-non-array-invalid-len). - if delete_count > u32::MAX as i64 { - crate::array::array_length_range_error(); - } - let removed = js_array_alloc_with_length(delete_count.max(0) as u32); - let removed_elems = - unsafe { (removed as *mut u8).add(std::mem::size_of::()) as *mut f64 }; - for k in 0..delete_count { - let from = actual_start + k; - if al_has(recv, from) { - let v = al_get(recv, from); - unsafe { - // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this slot with the barrier. - ptr::write(removed_elems.add(k as usize), v); - note_array_slot(removed, k as usize, v.to_bits()); - } - } - } - let item_count = args_len.saturating_sub(2) as i64; - if item_count < delete_count { - // Shift the tail down to close the gap. - let mut k = actual_start; - while k < len - delete_count { - let from = k + delete_count; - let to = k + item_count; - if al_has(recv, from) { - al_set(recv, to, al_get(recv, from)); - } else { - al_delete(recv, to); - } - k += 1; - } - // Delete the now-vacated trailing slots. - let mut k = len; - while k > len - delete_count + item_count { - al_delete(recv, k - 1); - k -= 1; - } - } else if item_count > delete_count { - // Open a gap by shifting the tail up. - let mut k = len - delete_count; - while k > actual_start { - let from = k + delete_count - 1; - let to = k + item_count - 1; - if al_has(recv, from) { - al_set(recv, to, al_get(recv, from)); - } else { - al_delete(recv, to); - } - k -= 1; - } - } - // Write the inserted items. - for j in 0..item_count { - al_set( - recv, - actual_start + j, - arg_at(args_ptr, args_len, 2 + j as usize), - ); - } - al_set_length(recv, len - delete_count + item_count); - nanbox_arr(removed) -} - -/// `Array.prototype.sort` over an array-like (non-real-array) receiver: -/// ECMA-262 SortIndexedProperties with holes skipped — collect via -/// `HasProperty`/`Get`, sort (undefined trailing, never compared), write back -/// via `Set` and `Delete` the trailing range. Returns the receiver. -/// `cmp_validated` is the already-validated comparator (null = default sort). -pub(crate) fn object_sort(recv: f64, cmp_validated: *const ClosureHeader) -> f64 { - let cmp = if cmp_validated.is_null() { - None - } else { - Some(super::sort::ComparatorCall::new(cmp_validated)) - }; - let len = al_length(recv); - unsafe { - // Root BOTH the receiver value and the collection temp for the whole - // protocol: `al_has`/`al_get`/`al_set` fire user accessors (and the - // comparator runs inside `sort_rooted_values`) — any of them can - // allocate and sweep or move either object, so every raw pointer is - // re-derived from its rooted handle after each such call. - let scope = crate::gc::RuntimeHandleScope::new(); - let recv_handle = scope.root_nanbox_f64(recv); - let temp = super::sort::RootedArrayElems::new( - &scope, - js_array_alloc_with_length(len.clamp(0, u32::MAX as i64) as u32), - ); - let mut count = 0usize; - let mut undef_count = 0usize; - for j in 0..len { - if al_has(recv_handle.get_nanbox_f64(), j) { - let v = al_get(recv_handle.get_nanbox_f64(), j); - if v.to_bits() == TAG_UNDEFINED { - undef_count += 1; - } else { - temp.set(count, v); - count += 1; - } - } - } - (*temp.arr()).length = count as u32; - rebuild_array_layout(temp.arr()); - let _ = super::sort::sort_rooted_values(temp.arr(), count, cmp); - for j in 0..count { - al_set(recv_handle.get_nanbox_f64(), j as i64, temp.get(j)); - } - for j in count..count + undef_count { - al_set(recv_handle.get_nanbox_f64(), j as i64, undef()); - } - for j in (count + undef_count) as i64..len { - al_delete(recv_handle.get_nanbox_f64(), j); - } - recv_handle.get_nanbox_f64() - } -} - -/// `Array.prototype.concat` over a non-real-array receiver: the receiver is -/// the first concat element (spread only when `@@isConcatSpreadable` says so — -/// a plain object/wrapper lands as a single element), then each argument is -/// appended with the usual spreadability rules. -fn object_concat(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { - let mut result = super::from_concat::append_concat_arg(js_array_alloc(0), recv); - for i in 0..args_len { - result = super::from_concat::append_concat_arg(result, arg_at(args_ptr, args_len, i)); - } - nanbox_arr(result) -} - -/// Generic `Array.prototype.sort.call(receiver, comparator?)` entry -/// (#4597 extension): ToObject + route real arrays to the dense/spec sort, -/// everything else through the array-like engine. Returns the receiver. -#[no_mangle] -pub extern "C" fn js_arraylike_sort(recv: f64, comparator: f64) -> f64 { - // Spec step 1: comparator must be undefined or callable — BEFORE ToObject. - let cmp = crate::array::js_validate_array_comparator(comparator) as *const ClosureHeader; - let o = to_object(recv); - let arr = as_real_array(o); - if !arr.is_null() { - let r = crate::array::js_array_sort_with_comparator(arr, cmp); - return nanbox_arr(r); - } - object_sort(o, cmp) -} - -/// Generic `Array.prototype.concat.call(receiver, ...items)` entry. -#[no_mangle] -pub extern "C" fn js_arraylike_concat(recv: f64, args_ptr: *const f64, count: i32) -> f64 { - let o = to_object(recv); - let arr = as_real_array(o); - if !arr.is_null() { - let r = crate::array::js_array_concat_variadic(arr, args_ptr, count.max(0)); - return nanbox_arr(r); - } - object_concat(o, args_ptr, count.max(0) as usize) -} - -/// Generic `Array.prototype.splice.call(receiver, start?, deleteCount?, ...items)`. -#[no_mangle] -pub extern "C" fn js_arraylike_splice(recv: f64, args_ptr: *const f64, count: i32) -> f64 { - let o = to_object(recv); - let count = count.max(0) as usize; - let arr = as_real_array(o); - if !arr.is_null() { - return unsafe { real_array_mutator(arr, "splice", args_ptr, count) }; - } - object_splice(o, args_ptr, count) -} - -#[cfg(feature = "keepalive-anchors")] -#[used] -static KEEP_ARRAYLIKE_SORT: extern "C" fn(f64, f64) -> f64 = js_arraylike_sort; -#[cfg(feature = "keepalive-anchors")] -#[used] -static KEEP_ARRAYLIKE_VARIADIC: [extern "C" fn(f64, *const f64, i32) -> f64; 2] = - [js_arraylike_concat, js_arraylike_splice]; - /// Walk the receiver's [[Prototype]] chain looking for a *real array* link — /// the `function foo() {}; foo.prototype = new Array(1, 2, 3); new foo()` /// shape (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*). Such a receiver diff --git a/crates/perry-runtime/src/array/generic_object.rs b/crates/perry-runtime/src/array/generic_object.rs new file mode 100644 index 0000000000..3601e0663f --- /dev/null +++ b/crates/perry-runtime/src/array/generic_object.rs @@ -0,0 +1,465 @@ +//! Generic `Array.prototype` MUTATORS over an *array-like object* receiver +//! (`pop` / `shift` / `push` / `unshift` / `reverse` / `splice` / `sort` / +//! `concat`), plus the `Set` / `Delete` / `length`-write helpers they need. +//! +//! Split verbatim out of the sibling [`super::generic`] module to keep that +//! file under the 2000-line gate; the read-only generic methods +//! (`forEach` / `map` / `filter` / …), the shared `ToObject` / `Get` / +//! `HasProperty` helpers, and the dynamic-dispatch entry points stay there. + +use super::generic::{ + al_get, al_has, al_length, as_real_array, nanbox_arr, proxy_string_key, real_array_mutator, + to_object, undef, +}; +use super::*; +use crate::closure::ClosureHeader; +use crate::value::TAG_UNDEFINED; +use std::ptr; + +// --------------------------------------------------------------------------- +// Generic array-like MUTATORS over a plain-object receiver (#4742 follow-up). +// +// `Array.prototype.{pop,shift,push,unshift,reverse,splice}` are intentionally +// generic (ECMA-262 §23.1.3) — they operate on `O = ToObject(this)` with live +// `Get`/`Set`/`Delete`/`HasProperty` and a writable `length`. Perry's dense +// fast paths assume a real `ArrayHeader`; when the receiver is a plain object +// (a stored `obj.pop = Array.prototype.pop; obj.pop()` borrow, or +// `Array.prototype.pop.call(obj, …)`), the dense path read the object's +// `ObjectHeader` words as an `ArrayHeader` and corrupted/crashed +// (`TypeError: Cannot convert object to primitive value`). +// +// These helpers run the spec algorithm by mutating the *original* receiver +// object in place via the polymorphic index get/set/delete and a `length` +// property write. They are dispatched from `js_native_call_method` only when +// the receiver classifies as a plain `Object` (never a real array / typed +// array / buffer / primitive), so the hot real-array paths are untouched. +// --------------------------------------------------------------------------- + +/// `Set(O, ToString(k), v, true)` for an array-like object receiver. +fn al_set(recv: f64, k: i64, v: f64) { + if crate::proxy::js_proxy_is_proxy(recv) != 0 { + crate::proxy::js_proxy_set(recv, proxy_string_key(k), v); + return; + } + crate::object::js_object_set_index_polymorphic(recv.to_bits() as i64, k as f64, v); +} + +/// `DeletePropertyOrThrow(O, ToString(k))` for an array-like object receiver. +fn al_delete(recv: f64, k: i64) { + if crate::proxy::js_proxy_is_proxy(recv) != 0 { + crate::proxy::js_proxy_delete(recv, proxy_string_key(k)); + return; + } + let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut crate::object::ObjectHeader; + crate::object::js_object_delete_dynamic(raw, k as f64); +} + +/// `Set(O, "length", len, true)` for an array-like object receiver. An own +/// `length` ACCESSOR fires its setter — and throws TypeError when there is +/// none (getter-only `length`, test262 splice/S15.4.4.12_A6.1_T3), matching +/// `Set(..., true)` on a non-writable slot. +fn al_set_length(recv: f64, len: i64) { + let raw_addr = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if let Some(acc) = crate::object::get_accessor_descriptor(raw_addr, "length") { + if acc.set != 0 { + unsafe { crate::object::invoke_accessor_setter(acc.set, recv, len as f64) }; + return; + } + crate::collection_iter::throw_type_error( + "Cannot set property length of object which has only a getter", + ); + } + // An object-LITERAL `get length()` lives in the anon-shape class vtable, + // not the defineProperty descriptor table — a getter with no setter makes + // `Set(O, "length", ..., true)` throw (test262 splice/S15.4.4.12_A6.1_T3). + { + let raw = raw_addr as *const crate::object::ObjectHeader; + let class_id = crate::object::js_object_get_class_id(raw); + if class_id != 0 { + if let Some((getter, setter)) = + crate::object::class_own_accessor_ptrs(class_id, "length") + { + if setter == 0 && getter != 0 { + crate::collection_iter::throw_type_error( + "Cannot set property length of object which has only a getter", + ); + } + } + } + } + // `Set(O, "length", …, true)` (Throw=true) must throw a TypeError when the + // set fails: a frozen array, an array/object whose `length` is a + // non-writable data property (`defineProperty(o,"length",{writable:false})` + // or a function's intrinsic `length`), or a frozen plain object. The + // by-name setter below silently no-ops in those cases (PutValue's + // non-strict fall-through), so detect the failure up front and throw. + if al_length_write_would_fail(recv, raw_addr) { + crate::collection_iter::throw_type_error( + "Cannot assign to read only property 'length' of object", + ); + } + let raw = raw_addr as *mut crate::object::ObjectHeader; + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + crate::object::js_object_set_field_by_name(raw, key, len as f64); +} + +/// True when `Set(recv, "length", …)` would fail (and so must throw under +/// `Throw=true`): a frozen array, a non-writable `length` data property, or a +/// callable receiver (a function's `length` is non-writable by spec). +fn al_length_write_would_fail(recv: f64, raw_addr: usize) -> bool { + // Frozen array: `length` is non-writable after `Object.freeze`. + let arr = as_real_array(recv); + if !arr.is_null() { + if crate::array::array_is_frozen(arr) { + return true; + } + return crate::object::get_property_attrs(raw_addr, "length") + .map(|a| !a.writable()) + .unwrap_or(false); + } + // Non-array receivers: a recorded non-writable `length` descriptor + // (`defineProperty(o,"length",{writable:false})`) or a callable receiver + // whose intrinsic `length` is non-writable. + if crate::object::get_property_attrs(raw_addr, "length") + .map(|a| !a.writable()) + .unwrap_or(false) + { + return true; + } + crate::closure::is_closure_ptr(raw_addr) +} + +/// `ToIntegerOrInfinity(v)` as an `f64` (NaN → 0; ±Infinity preserved). +fn to_integer_or_infinity(v: f64) -> f64 { + if v.is_nan() { + 0.0 + } else if v.is_infinite() { + v + } else { + v.trunc() + } +} + +/// Resolve a relative index argument (`splice` start) to an absolute, +/// clamped `[0, len]` index. +fn relative_index(v: f64, len: i64) -> i64 { + let n = to_integer_or_infinity(v); + if n < 0.0 { + let r = len as f64 + n; + if r < 0.0 { + 0 + } else { + r as i64 + } + } else if n > len as f64 { + len + } else { + n as i64 + } +} + +#[inline] +pub(super) fn arg_at(args_ptr: *const f64, args_len: usize, i: usize) -> f64 { + if i < args_len && !args_ptr.is_null() { + unsafe { *args_ptr.add(i) } + } else { + undef() + } +} + +/// `Array.prototype.pop` over an array-like object receiver. +pub(crate) fn object_pop(recv: f64) -> f64 { + let len = al_length(recv); + if len <= 0 { + al_set_length(recv, 0); + return undef(); + } + let new_len = len - 1; + let element = al_get(recv, new_len); + al_delete(recv, new_len); + al_set_length(recv, new_len); + element +} + +/// `Array.prototype.shift` over an array-like object receiver. +pub(crate) fn object_shift(recv: f64) -> f64 { + let len = al_length(recv); + if len <= 0 { + al_set_length(recv, 0); + return undef(); + } + let first = al_get(recv, 0); + for k in 1..len { + if al_has(recv, k) { + al_set(recv, k - 1, al_get(recv, k)); + } else { + al_delete(recv, k - 1); + } + } + al_delete(recv, len - 1); + al_set_length(recv, len - 1); + first +} + +/// `Array.prototype.push` over an array-like object receiver. Returns the new +/// length. +pub(super) fn object_push(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { + let len = al_length(recv); + for i in 0..args_len { + al_set(recv, len + i as i64, arg_at(args_ptr, args_len, i)); + } + let new_len = len + args_len as i64; + al_set_length(recv, new_len); + new_len as f64 +} + +/// `Array.prototype.unshift` over an array-like object receiver. Returns the +/// new length. +pub(super) fn object_unshift(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { + let len = al_length(recv); + let count = args_len as i64; + if count > 0 { + // Move existing elements up by `count`, high index first so we don't + // clobber not-yet-moved slots. + let mut k = len; + while k > 0 { + let from = k - 1; + let to = from + count; + if al_has(recv, from) { + al_set(recv, to, al_get(recv, from)); + } else { + al_delete(recv, to); + } + k -= 1; + } + for j in 0..count { + al_set(recv, j, arg_at(args_ptr, args_len, j as usize)); + } + } + let new_len = len + count; + al_set_length(recv, new_len); + new_len as f64 +} + +/// `Array.prototype.reverse` over an array-like object receiver. Returns the +/// receiver. +pub(super) fn object_reverse(recv: f64) -> f64 { + let len = al_length(recv); + let middle = len / 2; + let mut lower = 0; + while lower < middle { + let upper = len - 1 - lower; + let lower_exists = al_has(recv, lower); + let upper_exists = al_has(recv, upper); + let lower_val = al_get(recv, lower); + let upper_val = al_get(recv, upper); + match (lower_exists, upper_exists) { + (true, true) => { + al_set(recv, lower, upper_val); + al_set(recv, upper, lower_val); + } + (false, true) => { + al_set(recv, lower, upper_val); + al_delete(recv, upper); + } + (true, false) => { + al_delete(recv, lower); + al_set(recv, upper, lower_val); + } + (false, false) => {} + } + lower += 1; + } + recv +} + +/// `Array.prototype.splice` over an array-like object receiver. Returns a fresh +/// plain array of the removed elements (holes preserved). +pub(crate) fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { + let len = al_length(recv); + let actual_start = relative_index(arg_at(args_ptr, args_len, 0), len); + let delete_count = if args_len == 0 { + 0 + } else if args_len == 1 { + len - actual_start + } else { + let dc = to_integer_or_infinity(arg_at(args_ptr, args_len, 1)); + dc.max(0.0).min((len - actual_start) as f64) as i64 + }; + // Removed elements -> fresh plain array (holes preserved). ArrayCreate + // throws RangeError for a count ≥ 2^32 (splice/create-non-array-invalid-len). + if delete_count > u32::MAX as i64 { + crate::array::array_length_range_error(); + } + let removed = js_array_alloc_with_length(delete_count.max(0) as u32); + let removed_elems = + unsafe { (removed as *mut u8).add(std::mem::size_of::()) as *mut f64 }; + for k in 0..delete_count { + let from = actual_start + k; + if al_has(recv, from) { + let v = al_get(recv, from); + unsafe { + // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this slot with the barrier. + ptr::write(removed_elems.add(k as usize), v); + note_array_slot(removed, k as usize, v.to_bits()); + } + } + } + let item_count = args_len.saturating_sub(2) as i64; + if item_count < delete_count { + // Shift the tail down to close the gap. + let mut k = actual_start; + while k < len - delete_count { + let from = k + delete_count; + let to = k + item_count; + if al_has(recv, from) { + al_set(recv, to, al_get(recv, from)); + } else { + al_delete(recv, to); + } + k += 1; + } + // Delete the now-vacated trailing slots. + let mut k = len; + while k > len - delete_count + item_count { + al_delete(recv, k - 1); + k -= 1; + } + } else if item_count > delete_count { + // Open a gap by shifting the tail up. + let mut k = len - delete_count; + while k > actual_start { + let from = k + delete_count - 1; + let to = k + item_count - 1; + if al_has(recv, from) { + al_set(recv, to, al_get(recv, from)); + } else { + al_delete(recv, to); + } + k -= 1; + } + } + // Write the inserted items. + for j in 0..item_count { + al_set( + recv, + actual_start + j, + arg_at(args_ptr, args_len, 2 + j as usize), + ); + } + al_set_length(recv, len - delete_count + item_count); + nanbox_arr(removed) +} + +/// `Array.prototype.sort` over an array-like (non-real-array) receiver: +/// ECMA-262 SortIndexedProperties with holes skipped — collect via +/// `HasProperty`/`Get`, sort (undefined trailing, never compared), write back +/// via `Set` and `Delete` the trailing range. Returns the receiver. +/// `cmp_validated` is the already-validated comparator (null = default sort). +pub(crate) fn object_sort(recv: f64, cmp_validated: *const ClosureHeader) -> f64 { + let cmp = if cmp_validated.is_null() { + None + } else { + Some(super::sort::ComparatorCall::new(cmp_validated)) + }; + let len = al_length(recv); + unsafe { + // Root BOTH the receiver value and the collection temp for the whole + // protocol: `al_has`/`al_get`/`al_set` fire user accessors (and the + // comparator runs inside `sort_rooted_values`) — any of them can + // allocate and sweep or move either object, so every raw pointer is + // re-derived from its rooted handle after each such call. + let scope = crate::gc::RuntimeHandleScope::new(); + let recv_handle = scope.root_nanbox_f64(recv); + let temp = super::sort::RootedArrayElems::new( + &scope, + js_array_alloc_with_length(len.clamp(0, u32::MAX as i64) as u32), + ); + let mut count = 0usize; + let mut undef_count = 0usize; + for j in 0..len { + if al_has(recv_handle.get_nanbox_f64(), j) { + let v = al_get(recv_handle.get_nanbox_f64(), j); + if v.to_bits() == TAG_UNDEFINED { + undef_count += 1; + } else { + temp.set(count, v); + count += 1; + } + } + } + (*temp.arr()).length = count as u32; + rebuild_array_layout(temp.arr()); + let _ = super::sort::sort_rooted_values(temp.arr(), count, cmp); + for j in 0..count { + al_set(recv_handle.get_nanbox_f64(), j as i64, temp.get(j)); + } + for j in count..count + undef_count { + al_set(recv_handle.get_nanbox_f64(), j as i64, undef()); + } + for j in (count + undef_count) as i64..len { + al_delete(recv_handle.get_nanbox_f64(), j); + } + recv_handle.get_nanbox_f64() + } +} + +/// `Array.prototype.concat` over a non-real-array receiver: the receiver is +/// the first concat element (spread only when `@@isConcatSpreadable` says so — +/// a plain object/wrapper lands as a single element), then each argument is +/// appended with the usual spreadability rules. +pub(super) fn object_concat(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { + let mut result = super::from_concat::append_concat_arg(js_array_alloc(0), recv); + for i in 0..args_len { + result = super::from_concat::append_concat_arg(result, arg_at(args_ptr, args_len, i)); + } + nanbox_arr(result) +} + +/// Generic `Array.prototype.sort.call(receiver, comparator?)` entry +/// (#4597 extension): ToObject + route real arrays to the dense/spec sort, +/// everything else through the array-like engine. Returns the receiver. +#[no_mangle] +pub extern "C" fn js_arraylike_sort(recv: f64, comparator: f64) -> f64 { + // Spec step 1: comparator must be undefined or callable — BEFORE ToObject. + let cmp = crate::array::js_validate_array_comparator(comparator) as *const ClosureHeader; + let o = to_object(recv); + let arr = as_real_array(o); + if !arr.is_null() { + let r = crate::array::js_array_sort_with_comparator(arr, cmp); + return nanbox_arr(r); + } + object_sort(o, cmp) +} + +/// Generic `Array.prototype.concat.call(receiver, ...items)` entry. +#[no_mangle] +pub extern "C" fn js_arraylike_concat(recv: f64, args_ptr: *const f64, count: i32) -> f64 { + let o = to_object(recv); + let arr = as_real_array(o); + if !arr.is_null() { + let r = crate::array::js_array_concat_variadic(arr, args_ptr, count.max(0)); + return nanbox_arr(r); + } + object_concat(o, args_ptr, count.max(0) as usize) +} + +/// Generic `Array.prototype.splice.call(receiver, start?, deleteCount?, ...items)`. +#[no_mangle] +pub extern "C" fn js_arraylike_splice(recv: f64, args_ptr: *const f64, count: i32) -> f64 { + let o = to_object(recv); + let count = count.max(0) as usize; + let arr = as_real_array(o); + if !arr.is_null() { + return unsafe { real_array_mutator(arr, "splice", args_ptr, count) }; + } + object_splice(o, args_ptr, count) +} + +#[cfg(feature = "keepalive-anchors")] +#[used] +static KEEP_ARRAYLIKE_SORT: extern "C" fn(f64, f64) -> f64 = js_arraylike_sort; +#[cfg(feature = "keepalive-anchors")] +#[used] +static KEEP_ARRAYLIKE_VARIADIC: [extern "C" fn(f64, *const f64, i32) -> f64; 2] = + [js_arraylike_concat, js_arraylike_splice]; diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 2e9dc01552..07d7e87b5e 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -5,6 +5,7 @@ mod flat_clone; mod from_concat; mod generic; mod generic_mutators; +mod generic_object; mod header; mod immutable; mod indexing; @@ -44,20 +45,24 @@ pub use self::from_concat::{ }; pub use self::generic::array_proto_mutator; pub use self::generic::{ - dispatch_arraylike_read_method, js_arraylike_at, js_arraylike_concat, js_arraylike_every, - js_arraylike_filter, js_arraylike_find, js_arraylike_findIndex, js_arraylike_findLast, - js_arraylike_findLastIndex, js_arraylike_forEach, js_arraylike_includes, js_arraylike_indexOf, - js_arraylike_join, js_arraylike_lastIndexOf, js_arraylike_map, js_arraylike_reduce, - js_arraylike_reduceRight, js_arraylike_slice, js_arraylike_some, js_arraylike_sort, - js_arraylike_splice, try_array_proto_chain_method, try_object_arraylike_mutator, + dispatch_arraylike_read_method, js_arraylike_at, js_arraylike_every, js_arraylike_filter, + js_arraylike_find, js_arraylike_findIndex, js_arraylike_findLast, js_arraylike_findLastIndex, + js_arraylike_forEach, js_arraylike_includes, js_arraylike_indexOf, js_arraylike_join, + js_arraylike_lastIndexOf, js_arraylike_map, js_arraylike_reduce, js_arraylike_reduceRight, + js_arraylike_slice, js_arraylike_some, try_array_proto_chain_method, + try_object_arraylike_mutator, }; pub(crate) use self::generic::{ - non_array_object_receiver, object_owns_user_method, object_pop as generic_object_pop, - object_shift as generic_object_shift, object_sort, object_splice, plain_object_value, + non_array_object_receiver, object_owns_user_method, plain_object_value, }; pub use self::generic_mutators::{ js_arraylike_pop, js_arraylike_push, js_arraylike_shift, js_arraylike_unshift, }; +pub use self::generic_object::{js_arraylike_concat, js_arraylike_sort, js_arraylike_splice}; +pub(crate) use self::generic_object::{ + object_pop as generic_object_pop, object_shift as generic_object_shift, object_sort, + object_splice, +}; pub(crate) use self::header::{ array_has_arguments_object_flag, mark_array_as_arguments_object, prune_dead_array_named_property_owners, rebuild_array_numeric_raw_f64_allow_holes, diff --git a/crates/perry-runtime/src/gc/tests/layout_trace.rs b/crates/perry-runtime/src/gc/tests/layout_trace.rs index f4288f36bc..02d286c88f 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace.rs @@ -1,5 +1,9 @@ use super::super::*; use super::support::*; +mod array_layout; +mod object_closure_slots; +mod typed_shape; +mod unboxed_object; #[test] fn test_trace_array_marks_child() { @@ -419,1734 +423,3 @@ fn test_layout_mask_object_and_closure_slots() { clear_marks(); clear_mark_seeds(); } - -#[test] -fn test_typed_shape_descriptor_preserves_pointer_slots_after_non_pointer_overwrite() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 2); - let mask = [0b10u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - std::ptr::null(), - 0, - mask.as_ptr(), - mask.len() as u32, - ); - - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); - assert_eq!(test_heap_child_slot_count(obj as *mut u8), 1); - - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(7.0)); - - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); - assert_eq!(test_heap_child_slot_count(obj as *mut u8), 1); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_pointer_write_to_non_pointer_slot_falls_back() { - clear_marks(); - clear_mark_seeds(); - - let child = crate::string::js_string_from_bytes(b"typed-child".as_ptr(), 11); - let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; - let obj = crate::object::js_object_alloc(0, 2); - let mask = [0b10u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - std::ptr::null(), - 0, - mask.as_ptr(), - mask.len() as u32, - ); - - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); - - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), None); - assert_eq!(test_heap_child_slot_count(obj as *mut u8), 2); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - trace_marked_objects(&valid_ptrs); - unsafe { - assert_ne!( - (*child_header).gc_flags & GC_FLAG_MARKED, - 0, - "fallback all-field tracing should mark a pointer written to a numeric slot" - ); - } - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_tracks_raw_numeric_slots() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.5); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(2.5)); - let raw_mask = [0b01u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - - assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); - assert!(!layout_typed_raw_f64_slot_for_user(obj as usize, 1)); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); - - let child = crate::string::js_string_from_bytes(b"raw-child".as_ptr(), 9); - let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); - - assert!( - !layout_typed_raw_f64_slot_for_user(obj as usize, 0), - "non-number writes must clear the exact raw-f64 descriptor" - ); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), None); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - trace_marked_objects(&valid_ptrs); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -/// #6957 regression guard: the typed descriptor of a **shape-keyed** object must -/// be visible to the layout query helpers. -/// -/// #6893 keys the canonical descriptor by the shared `keys_array` (`SHAPE_LAYOUTS`) -/// and deletes the per-object `TYPED_LAYOUTS` entry — so every class instance -/// (the only objects that carry a keys_array) moved to the shared map. Every -/// other test in this file allocates with `js_object_alloc` (class 0, no -/// keys_array), which still takes the per-object path; that is precisely why the -/// query helpers could go blind on real class instances with the whole layout -/// suite green. -#[test] -fn test_typed_shape_descriptor_visible_for_shape_keyed_objects() { - clear_marks(); - clear_mark_seeds(); - - let packed = b"x\0y\0"; - let keys = crate::object::js_build_class_keys_array( - 0x6957_01, - 2, - packed.as_ptr(), - packed.len() as u32, - ); - let first = crate::object::js_object_alloc_class_inline_keys(0x6957_01, 0, 2, keys); - let second = crate::object::js_object_alloc_class_inline_keys(0x6957_01, 0, 2, keys); - unsafe { - assert_eq!( - (*first).keys_array, - (*second).keys_array, - "same-shape objects must share one canonical keys array" - ); - } - - let raw_mask = [0b01u64]; - for object in [first, second] { - crate::object::js_object_set_unboxed_f64_field(object, 0, 1.5); - crate::object::js_object_set_field(object, 1, crate::value::JSValue::number(2.5)); - js_gc_init_typed_shape_layout( - object as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - } - - for object in [first, second] { - let user = object as usize; - assert!( - layout_typed_intact_for_user(user), - "the shared shape install must set the intact bit" - ); - assert!( - layout_typed_raw_f64_slot_for_user(user, 0), - "slot 0 is raw-f64 in the shape descriptor" - ); - assert!(!layout_typed_raw_f64_slot_for_user(user, 1)); - assert!( - layout_slot_is_raw_f64_typed(user, 0), - "the store fast path must agree with layout_note_slot's own resolution" - ); - assert!( - layout_typed_accepts_finite_number_slot_for_user(user, 1), - "an ordinary JSValue slot of an intact descriptor accepts finite numbers" - ); - } - - // A contradicting store downgrades ONLY the object that made it. The shared - // entry cannot be removed (it still describes every sibling), so the intact - // bit is what separates the two — assert both halves. - let payload = crate::string::js_string_from_bytes(b"boxed".as_ptr(), 5); - crate::object::js_object_set_field(first, 0, crate::value::JSValue::string_ptr(payload)); - - assert!( - !layout_typed_raw_f64_slot_for_user(first as usize, 0), - "a boxed store into a raw-f64 slot must evict this object's descriptor" - ); - assert!(!layout_slot_is_raw_f64_typed(first as usize, 0)); - assert!( - !layout_typed_accepts_finite_number_slot_for_user(first as usize, 0), - "a downgraded object must not keep reading its shape's stale descriptor" - ); - assert!( - layout_typed_raw_f64_slot_for_user(second as usize, 0), - "the sibling never diverged and must keep the shared shape descriptor" - ); - assert!(layout_slot_is_raw_f64_typed(second as usize, 0)); - - clear_marks(); - clear_mark_seeds(); -} - -/// #6964: `layout_transfer` resolved the moved object's typed descriptor only -/// through the per-object `TYPED_LAYOUTS` map. #6893 moved the canonical -/// descriptor of every object carrying a `keys_array` (i.e. every class -/// instance) into the shape-keyed `SHAPE_LAYOUTS` map and DELETED the per-object -/// entry, so that lookup missed and the relocated copy had a still-valid -/// `GC_OBJ_TYPED_LAYOUT_INTACT` bit cleared. -/// -/// Deliberately a *shape-keyed* object: every pre-existing `layout_transfer` -/// test allocates with `js_object_alloc` (class 0, no keys_array), which keeps -/// its per-object entry and therefore takes the surviving path. That gap is why -/// #6893 merged green. -#[test] -fn test_shape_keyed_typed_layout_survives_layout_transfer() { - clear_marks(); - clear_mark_seeds(); - - let packed = b"x\0y\0"; - let keys = crate::object::js_build_class_keys_array( - 0x6964_01, - 2, - packed.as_ptr(), - packed.len() as u32, - ); - let src = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); - crate::object::js_object_set_unboxed_f64_field(src, 0, 1.5); - crate::object::js_object_set_field(src, 1, crate::value::JSValue::number(2.5)); - let raw_mask = [0b01u64]; - js_gc_init_typed_shape_layout( - src as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - assert!(layout_typed_intact_for_user(src as usize)); - assert!(layout_typed_raw_f64_slot_for_user(src as usize, 0)); - - // Model an evacuation copy the way every caller performs it: a destination - // of the same shape, payload copied verbatim, `_reserved` propagated, then - // `layout_transfer`. - let dst = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); - unsafe { - let header_size = std::mem::size_of::(); - std::ptr::copy_nonoverlapping( - src as *const u8, - dst as *mut u8, - header_size + 2 * std::mem::size_of::(), - ); - let src_header = header_from_user_ptr(src as *const u8); - let dst_header = header_from_user_ptr(dst as *const u8); - (*(dst_header as *mut GcHeader))._reserved = (*src_header)._reserved; - layout_transfer(src as *mut u8, dst as *mut u8); - } - - assert!( - layout_typed_intact_for_user(dst as usize), - "a relocated shape-keyed object must keep GC_OBJ_TYPED_LAYOUT_INTACT — its \ - SHAPE_LAYOUTS descriptor is keyed by the shared keys_array, which the copy carries" - ); - assert!( - layout_typed_raw_f64_slot_for_user(dst as usize, 0), - "slot 0 is still raw-f64 after relocation" - ); - assert!(!layout_typed_raw_f64_slot_for_user(dst as usize, 1)); - assert!( - layout_slot_is_raw_f64_typed(dst as usize, 0), - "the store fast path must agree with the descriptor after relocation" - ); - - // The source is downgraded on transfer (it is dead / a forwarding stub), and - // that must NOT take the shared entry with it: an untouched sibling still - // reads the shape descriptor. - let sibling = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); - crate::object::js_object_set_unboxed_f64_field(sibling, 0, 7.5); - crate::object::js_object_set_field(sibling, 1, crate::value::JSValue::number(8.5)); - js_gc_init_typed_shape_layout( - sibling as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - assert!(layout_typed_raw_f64_slot_for_user(sibling as usize, 0)); - - clear_marks(); - clear_mark_seeds(); -} - -/// #6964, but driven through the real evacuation path (`gc/copying.rs`'s -/// `layout_transfer` call site) instead of calling the helper directly. -#[test] -fn test_shape_keyed_typed_layout_survives_copying_minor() { - let _guard = CopyingNurseryTestGuard::new(1); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - - let packed = b"x\0y\0"; - let keys = crate::object::js_build_class_keys_array( - 0x6964_02, - 2, - packed.as_ptr(), - packed.len() as u32, - ); - let obj = crate::object::js_object_alloc_class_inline_keys(0x6964_02, 0, 2, keys); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 10.5); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(-3.25)); - let raw_mask = [0b01u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - assert!(layout_typed_intact_for_user(obj as usize)); - assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); - js_shadow_slot_set(0, ptr_bits(obj as usize)); - - let trace = collect_minor_trace(GcTriggerKind::Direct); - assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); - - let after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; - assert_ne!( - after, obj as usize, - "the copying minor must actually relocate the instance — an inert arm proves nothing" - ); - - let fields = unsafe { - (after as *const u8).add(std::mem::size_of::()) as *const u64 - }; - assert_eq!(f64::from_bits(unsafe { *fields.add(0) }), 10.5); - - assert!( - layout_typed_intact_for_user(after), - "#6964: the relocated class instance must keep its shape-keyed typed layout" - ); - assert!( - layout_typed_raw_f64_slot_for_user(after, 0), - "#6964: the shape descriptor still describes slot 0 as raw-f64 after relocation" - ); - assert!(layout_slot_is_raw_f64_typed(after, 0)); -} - -#[test] -fn test_typed_shape_raw_numeric_slots_accept_pointer_like_f64_bits() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 2); - let pointer_like_number = f64::from_bits(0x1000); - crate::object::js_object_set_unboxed_f64_field(obj, 0, pointer_like_number); - let child = crate::string::js_string_from_bytes(b"mixed-child".as_ptr(), 11); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::string_ptr(child)); - - let raw_mask = [0b01u64]; - let pointer_mask = [0b10u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - pointer_mask.as_ptr(), - pointer_mask.len() as u32, - ); - - assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); - - let next_pointer_like_number = f64::from_bits(0x2000); - crate::object::js_object_set_unboxed_f64_field(obj, 0, next_pointer_like_number); - assert!( - layout_typed_raw_f64_slot_for_user(obj as usize, 0), - "raw f64 slots must not be downgraded by numeric payload bits that resemble raw pointers" - ); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_rejects_nanbox_non_number_tags() { - clear_marks(); - clear_mark_seeds(); - - let raw_mask = [0b1u64]; - let obj = crate::object::js_object_alloc(0, 1); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.5); - js_gc_init_typed_shape_layout( - obj as u64, - 1, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); - - let short = crate::value::JSValue::try_short_string(b"abc").unwrap(); - crate::object::js_object_set_field(obj, 0, short); - assert!( - !layout_typed_raw_f64_slot_for_user(obj as usize, 0), - "SSO string tags must downgrade raw-f64 descriptors" - ); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 1), None); - - let handle_obj = crate::object::js_object_alloc(0, 1); - crate::object::js_object_set_unboxed_f64_field(handle_obj, 0, 2.5); - js_gc_init_typed_shape_layout( - handle_obj as u64, - 1, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - assert!(layout_typed_raw_f64_slot_for_user(handle_obj as usize, 0)); - - let handle = crate::value::JSValue::from_bits(crate::value::JS_HANDLE_TAG | 0x1234); - crate::object::js_object_set_field(handle_obj, 0, handle); - assert!( - !layout_typed_raw_f64_slot_for_user(handle_obj as usize, 0), - "JS handle tags must downgrade raw-f64 descriptors" - ); - assert_eq!(test_layout_pointer_slot_count(handle_obj as usize, 1), None); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_growing_new_field_falls_back() { - clear_marks(); - clear_mark_seeds(); - - let packed_keys = b"stable\0"; - let keys = crate::object::js_build_class_keys_array( - 65_001, - 1, - packed_keys.as_ptr(), - packed_keys.len() as u32, - ); - let obj = crate::object::js_object_alloc_class_inline_keys(65_001, 0, 1, keys); - js_gc_init_typed_shape_layout(obj as u64, 1, std::ptr::null(), 0, std::ptr::null(), 0); - - let extra_key = crate::string::js_string_from_bytes(b"extra".as_ptr(), 5); - crate::object::js_object_set_field_by_name(obj, extra_key, 42.0); - - unsafe { - assert_eq!((*obj).field_count, 2); - } - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), None); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_transfers_on_object_move() { - clear_marks(); - clear_mark_seeds(); - - let src = crate::object::js_object_alloc(0, 2); - let dst = crate::object::js_object_alloc(0, 2); - let mask = [0b10u64]; - js_gc_init_typed_shape_layout( - src as u64, - 2, - std::ptr::null(), - 0, - mask.as_ptr(), - mask.len() as u32, - ); - - unsafe { - layout_transfer(src as *mut u8, dst as *mut u8); - } - - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(1)); - crate::object::js_object_set_field(dst, 1, crate::value::JSValue::number(9.0)); - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(1)); - - let child = crate::string::js_string_from_bytes(b"moved-child".as_ptr(), 11); - crate::object::js_object_set_field(dst, 0, crate::value::JSValue::string_ptr(child)); - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), None); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_all_pointer_layout_transfers_on_array_move() { - clear_marks(); - clear_mark_seeds(); - - let src = crate::array::js_array_alloc_pointer_elements(2); - let dst = crate::array::js_array_alloc(2); - unsafe { - layout_transfer(src as *mut u8, dst as *mut u8); - } - - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(2)); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_unboxed_object_layout_scans_zero_raw_numeric_fields() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.25); - crate::object::js_object_set_unboxed_f64_field(obj, 1, -2.5); - js_gc_init_unboxed_object_layout(obj as u64, 2, 0b11, 0); - - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); - assert_eq!(test_heap_child_slot_count(obj as *mut u8), 0); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 0); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_layout_scan_trace_counts_raw_numeric_object_fields() { - clear_marks(); - clear_mark_seeds(); - - let trace = GcCycleTrace::new( - GcCollectionKind::Minor, - GcTriggerSnapshot { - kind: GcTriggerKind::Direct, - steps_before: Some(GcStepSnapshot::current()), - }, - ) - .expect("test requested GC trace capture"); - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.25); - crate::object::js_object_set_unboxed_f64_field(obj, 1, -2.5); - let raw_mask = [0b11u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - trace_marked_objects(&valid_ptrs); - - let event = trace.into_json(GcStepSnapshot::current()); - let layout_scans = &event["layout_scans"]; - assert_eq!( - layout_scans["raw_numeric_object_field_ranges_skipped"].as_u64(), - Some(1) - ); - assert_eq!( - layout_scans["raw_numeric_object_field_slots_skipped"].as_u64(), - Some(2) - ); - assert_eq!( - layout_scans["raw_numeric_object_field_payload_bytes_skipped"].as_u64(), - Some(16) - ); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_layout_scan_trace_counts_mixed_raw_numeric_object_fields() { - clear_marks(); - clear_mark_seeds(); - - let trace = GcCycleTrace::new( - GcCollectionKind::Minor, - GcTriggerSnapshot { - kind: GcTriggerKind::Direct, - steps_before: Some(GcStepSnapshot::current()), - }, - ) - .expect("test requested GC trace capture"); - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_unboxed_f64_field(obj, 0, f64::from_bits(0x1000)); - let child = crate::string::js_string_from_bytes(b"mixed-child".as_ptr(), 11); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::string_ptr(child)); - let raw_mask = [0b01u64]; - let pointer_mask = [0b10u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - pointer_mask.as_ptr(), - pointer_mask.len() as u32, - ); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - trace_marked_objects(&valid_ptrs); - - let event = trace.into_json(GcStepSnapshot::current()); - let layout_scans = &event["layout_scans"]; - assert_eq!(layout_scans["masked_pointer_slots_read"].as_u64(), Some(1)); - assert_eq!( - layout_scans["raw_numeric_object_field_ranges_skipped"].as_u64(), - Some(1) - ); - assert_eq!( - layout_scans["raw_numeric_object_field_slots_skipped"].as_u64(), - Some(1) - ); - assert_eq!( - layout_scans["raw_numeric_object_field_payload_bytes_skipped"].as_u64(), - Some(8) - ); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_unboxed_object_pointer_write_to_raw_slot_falls_back_and_traces() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.0); - crate::object::js_object_set_unboxed_f64_field(obj, 1, 2.0); - js_gc_init_unboxed_object_layout(obj as u64, 2, 0b11, 0); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); - - let child = crate::string::js_string_from_bytes(b"unboxed-child".as_ptr(), 13); - let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); - - assert_eq!( - test_layout_pointer_slot_count(obj as usize, 2), - None, - "non-number writes to raw f64 slots must deopt to full scanning" - ); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 2); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_unboxed_object_descriptor_transfers_on_object_move() { - clear_marks(); - clear_mark_seeds(); - - let src = crate::object::js_object_alloc(0, 2); - let dst = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_unboxed_f64_field(src, 0, 3.0); - crate::object::js_object_set_unboxed_f64_field(src, 1, 4.0); - js_gc_init_unboxed_object_layout(src as u64, 2, 0b11, 0); - - unsafe { - layout_transfer(src as *mut u8, dst as *mut u8); - } - - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(0)); - crate::object::js_object_set_unboxed_f64_field(dst, 1, 5.0); - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(0)); - - let child = crate::string::js_string_from_bytes(b"moved-child".as_ptr(), 11); - crate::object::js_object_set_field(dst, 1, crate::value::JSValue::string_ptr(child)); - assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), None); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_raw_numeric_object_descriptor_transfers_on_copying_minor_and_skips_raw_slots() { - let _guard = CopyingNurseryTestGuard::new(1); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - - let child = young_leaf(); - let obj = crate::object::js_object_alloc(0, 3); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 10.5); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::from_bits(ptr_bits(child))); - crate::object::js_object_set_unboxed_f64_field(obj, 2, -3.25); - let raw_mask = [0b101u64]; - let pointer_mask = [0b010u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 3, - raw_mask.as_ptr(), - raw_mask.len() as u32, - pointer_mask.as_ptr(), - pointer_mask.len() as u32, - ); - assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); - assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 2)); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 3), Some(1)); - js_shadow_slot_set(0, ptr_bits(obj as usize)); - - let trace = collect_minor_trace(GcTriggerKind::Direct); - let after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; - let fields = unsafe { - (after as *const u8).add(std::mem::size_of::()) as *const u64 - }; - let first = f64::from_bits(unsafe { *fields.add(0) }); - let child_after = unsafe { (*fields.add(1) & POINTER_MASK) as usize }; - let third = f64::from_bits(unsafe { *fields.add(2) }); - - assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); - assert_ne!(after, obj as usize); - assert_ne!(child_after, child); - assert!(crate::arena::pointer_in_nursery(after)); - assert!(crate::arena::pointer_in_nursery(child_after)); - assert_eq!(first, 10.5); - assert_eq!(third, -3.25); - assert!(layout_typed_raw_f64_slot_for_user(after, 0)); - assert!(layout_typed_raw_f64_slot_for_user(after, 2)); - assert_eq!(test_layout_pointer_slot_count(after, 3), Some(1)); - assert_eq!(test_heap_child_slot_count(after as *mut u8), 1); - assert!( - trace.layout_scans.masked_pointer_slots_read >= 1, - "pointer slot should still be scanned: {:?}", - trace.layout_scans - ); - assert!( - trace.layout_scans.raw_numeric_object_field_slots_skipped >= 2, - "raw numeric object slots should be skipped: {:?}", - trace.layout_scans - ); -} - -fn unboxed_point_for_shape_change_test(shape_id: u32) -> *mut crate::object::ObjectHeader { - let packed_keys = b"x\0y\0"; - let obj = crate::object::js_object_alloc_with_shape( - shape_id, - 2, - packed_keys.as_ptr(), - packed_keys.len() as u32, - ); - crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.0); - crate::object::js_object_set_unboxed_f64_field(obj, 1, 2.0); - js_gc_init_unboxed_object_layout(obj as u64, 2, 0b11, 0); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); - obj -} - -fn descriptor_object_with_single_field( - shape_id: u32, - key: &[u8], - value: crate::value::JSValue, -) -> *mut crate::object::ObjectHeader { - let mut packed_key = Vec::with_capacity(key.len() + 1); - packed_key.extend_from_slice(key); - packed_key.push(0); - let desc = crate::object::js_object_alloc_with_shape( - shape_id, - 1, - packed_key.as_ptr(), - packed_key.len() as u32, - ); - crate::object::js_object_set_field(desc, 0, value); - desc -} - -#[test] -fn test_unboxed_object_dynamic_added_property_falls_back() { - clear_marks(); - clear_mark_seeds(); - - let obj = unboxed_point_for_shape_change_test(86_101); - let z_key = crate::string::js_string_from_bytes(b"z".as_ptr(), 1); - crate::object::js_object_set_field_by_name(obj, z_key, 3.0); - - assert_eq!( - test_layout_pointer_slot_count(obj as usize, 3), - None, - "adding a dynamic property must invalidate the exact unboxed shape" - ); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_unboxed_object_delete_falls_back() { - clear_marks(); - clear_mark_seeds(); - - let obj = unboxed_point_for_shape_change_test(86_102); - let x_key = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); - assert_eq!(crate::object::js_object_delete_field(obj, x_key), 1); - - assert_eq!( - test_layout_pointer_slot_count(obj as usize, 1), - None, - "delete shifts keys/fields and must invalidate the exact unboxed shape" - ); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_unboxed_object_define_property_falls_back() { - clear_marks(); - clear_mark_seeds(); - - let obj = unboxed_point_for_shape_change_test(86_103); - let x_key = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); - let desc = - descriptor_object_with_single_field(86_104, b"value", crate::value::JSValue::number(9.0)); - - crate::object::js_object_define_property( - crate::value::js_nanbox_pointer(obj as i64), - f64::from_bits(crate::value::JSValue::string_ptr(x_key).bits()), - crate::value::js_nanbox_pointer(desc as i64), - ); - - assert_eq!( - test_layout_pointer_slot_count(obj as usize, 2), - None, - "Object.defineProperty must invalidate the exact unboxed shape even for existing keys" - ); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_unboxed_object_accessor_define_property_falls_back() { - clear_marks(); - clear_mark_seeds(); - - let obj = unboxed_point_for_shape_change_test(86_105); - let x_key = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); - // #2817: an accessor descriptor's `get` must be callable — a non-function - // value now throws. Use a real (capture-less) closure as the getter so we - // still exercise the accessor shape-invalidation path under test. - let getter = crate::closure::js_closure_alloc(std::ptr::null(), 0); - let desc = descriptor_object_with_single_field( - 86_106, - b"get", - crate::value::JSValue::pointer(getter as *const u8), - ); - - crate::object::js_object_define_property( - crate::value::js_nanbox_pointer(obj as i64), - f64::from_bits(crate::value::JSValue::string_ptr(x_key).bits()), - crate::value::js_nanbox_pointer(desc as i64), - ); - - assert_eq!( - test_layout_pointer_slot_count(obj as usize, 2), - None, - "accessor descriptors must invalidate the exact unboxed shape" - ); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_heap_child_iterator_pointer_free_object_yields_no_child_slots() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 3); - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(1.0)); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(2.0)); - crate::object::js_object_set_field(obj, 2, crate::value::JSValue::bool(false)); - - assert_eq!(test_layout_pointer_slot_count(obj as usize, 3), Some(0)); - assert_eq!(test_heap_child_slot_count(obj as *mut u8), 0); - - let valid_ptrs = build_valid_pointer_set(); - let mut worklist = Vec::new(); - test_reset_trace_slot_reads(); - unsafe { - trace_object(obj as *mut u8, &valid_ptrs, &mut worklist); - } - assert_eq!(test_trace_slot_reads(), 0); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_layout_mask_overflow_fields_and_array_grow_transfer() { - clear_marks(); - clear_mark_seeds(); - // #6812 spill: overflow writes now allocate GC memory (meta record + - // spill buffer), so an automatic minor GC mid-build could move `obj` - // out from under this test's raw pointers. The test asserts layout and - // tracing, not move-resilience — pin the heap while building. - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - - let child = crate::string::js_string_from_bytes(b"overflow-child".as_ptr(), 14) as *mut u8; - let child_header = unsafe { header_from_user_ptr(child) }; - let obj = crate::object::js_object_alloc(0, 0); - for i in 0..9 { - let name = format!("k{i}"); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = if i == 8 { - f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)) - } else { - i as f64 - }; - crate::object::js_object_set_field_by_name(obj, key, value); - } - - // #6812 spill: the k8 pointer lives in the object-owned spill buffer - // (owner inline slots hold only k0..k3 numerics), so the pointer-slot - // count moves from the owner's mask to the buffer's. Legacy mode keeps - // the original owner-mask expectation. - if crate::object::test_object_spill_enabled() { - let spill = crate::object::test_spill_buffer_addr(obj as usize); - assert_ne!(spill, 0, "overflow write must have created a spill buffer"); - assert_eq!(test_layout_pointer_slot_count(spill, 9), Some(1)); - } else { - assert_eq!(test_layout_pointer_slot_count(obj as usize, 9), Some(1)); - } - let valid_ptrs = build_valid_pointer_set(); - let mut worklist = Vec::new(); - unsafe { - trace_object(obj as *mut u8, &valid_ptrs, &mut worklist); - // #6812 spill: the overflow value is no longer owner-adjacent — the - // chain is obj → meta record → spill buffer → child, so drain the - // worklist exactly like production marking does instead of relying - // on a single hop. - while let Some(queued) = worklist.pop() { - let user = (queued as *mut u8).add(crate::gc::GC_HEADER_SIZE); - trace_object(user, &valid_ptrs, &mut worklist); - } - } - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - let arr = crate::array::js_array_alloc_with_length(1); - crate::array::js_array_set_f64( - arr, - 0, - f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), - ); - let grown = crate::array::js_array_grow(arr, 128); - assert_eq!(test_layout_pointer_slot_count(grown as usize, 1), Some(1)); - - let moved = crate::array::js_array_alloc_with_length(1); - unsafe { - layout_transfer(grown as *mut u8, moved as *mut u8); - } - assert_eq!(test_layout_pointer_slot_count(moved as usize, 1), Some(1)); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_trace_array_uses_pointer_layout_mask() { - clear_marks(); - clear_mark_seeds(); - - let numeric = crate::array::js_array_alloc_with_length(3); - crate::array::js_array_set_f64(numeric, 0, 1.0); - crate::array::js_array_set_f64(numeric, 1, 2.0); - crate::array::js_array_set_f64(numeric, 2, 3.0); - assert_eq!(test_layout_pointer_slot_count(numeric as usize, 3), Some(0)); - assert_eq!(test_heap_child_slot_count(numeric as *mut u8), 0); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (numeric as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 0); - clear_marks(); - clear_mark_seeds(); - - let child = crate::string::js_string_from_bytes(b"array-child".as_ptr(), 11) as *mut u8; - let child_header = unsafe { header_from_user_ptr(child) }; - let mixed = crate::array::js_array_alloc_with_length(3); - crate::array::js_array_set_f64(mixed, 0, 1.0); - crate::array::js_array_set_f64( - mixed, - 1, - f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), - ); - crate::array::js_array_set_f64(mixed, 2, 3.0); - assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (mixed as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -fn assert_array_root_trace_reads(arr: *mut crate::array::ArrayHeader, expected_reads: usize) { - clear_marks(); - clear_mark_seeds(); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (arr as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), expected_reads); -} - -fn assert_numeric_array_trace_free(arr: *mut crate::array::ArrayHeader, len: usize) { - assert_eq!(test_layout_pointer_slot_count(arr as usize, len), Some(0)); - assert_eq!(test_heap_child_slot_count(arr as *mut u8), 0); - assert_array_root_trace_reads(arr, 0); -} - -#[test] -fn test_array_numeric_producers_stay_pointer_free() { - clear_marks(); - clear_mark_seeds(); - - let values = [1.0, 2.5, 3.0, 4.25]; - let from_f64 = crate::array::js_array_from_f64(values.as_ptr(), values.len() as u32); - assert_numeric_array_trace_free(from_f64, values.len()); - - let keys_src = crate::array::js_array_alloc_with_length(4); - for i in 0..4 { - crate::array::js_array_set_f64(keys_src, i, (i + 10) as f64); - } - let keys = crate::array::js_array_keys(keys_src); - assert_numeric_array_trace_free(keys, 4); - - let filled = crate::array::js_array_alloc_with_length(4); - crate::array::js_array_fill(filled, 42.0); - assert_numeric_array_trace_free(filled, 4); - - let cloned = crate::array::js_array_clone(filled); - assert_numeric_array_trace_free(cloned, 4); - - let concat_dest = crate::array::js_array_alloc(0); - let concatenated = crate::array::js_array_concat(concat_dest, filled); - assert_numeric_array_trace_free(concatenated, 4); - - crate::array::js_array_copy_within(concatenated, 1.0, 0.0, 0, 0.0); - assert_numeric_array_trace_free(concatenated, 4); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_array_mixed_bulk_producers_preserve_pointer_layout() { - clear_marks(); - clear_mark_seeds(); - - let child = crate::string::js_string_from_bytes(b"bulk-child".as_ptr(), 10) as *mut u8; - let child_header = unsafe { header_from_user_ptr(child) }; - let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); - - let src = crate::array::js_array_alloc_with_length(2); - crate::array::js_array_set_f64(src, 0, 1.0); - crate::array::js_array_set_f64(src, 1, child_box); - - let cloned = crate::array::js_array_clone(src); - assert_eq!(test_layout_pointer_slot_count(cloned as usize, 2), Some(1)); - assert_array_root_trace_reads(cloned, 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - clear_marks(); - clear_mark_seeds(); - - let concatenated = crate::array::js_array_concat(crate::array::js_array_alloc(0), src); - assert_eq!( - test_layout_pointer_slot_count(concatenated as usize, 2), - Some(1) - ); - assert_array_root_trace_reads(concatenated, 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - clear_marks(); - clear_mark_seeds(); - - let set = crate::set::js_set_alloc(4); - let set = crate::set::js_set_add(set, child_box); - let set_arr = crate::set::js_set_to_array(set); - assert_eq!(test_layout_pointer_slot_count(set_arr as usize, 1), Some(1)); - assert_array_root_trace_reads(set_arr, 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - clear_marks(); - clear_mark_seeds(); - - let map = crate::map::js_map_alloc(4); - let map = crate::map::js_map_set(map, 7.0, child_box); - let entries = crate::map::js_map_entries(map); - assert_eq!(test_layout_pointer_slot_count(entries as usize, 1), Some(1)); - let pair_box = crate::array::js_array_get_f64(entries, 0); - let pair = (pair_box.to_bits() & POINTER_MASK) as *mut crate::array::ArrayHeader; - assert_eq!(test_layout_pointer_slot_count(pair as usize, 2), Some(1)); - assert_array_root_trace_reads(entries, 2); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - clear_marks(); - clear_mark_seeds(); - - let overwritten = crate::array::js_array_alloc_with_length(1); - crate::array::js_array_set_f64(overwritten, 0, child_box); - assert_eq!( - test_layout_pointer_slot_count(overwritten as usize, 1), - Some(1) - ); - crate::array::js_array_set_f64(overwritten, 0, 99.0); - assert_numeric_array_trace_free(overwritten, 1); - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_numeric_array_push_heap_value_transitions_and_traces() { - clear_marks(); - clear_mark_seeds(); - - let mut arr = crate::array::js_array_alloc(4); - arr = crate::array::js_array_push_f64(arr, 1.0); - arr = crate::array::js_array_push_f64(arr, 2.0); - assert_eq!(test_layout_pointer_slot_count(arr as usize, 2), Some(0)); - - let child = crate::string::js_string_from_bytes(b"pushed-child".as_ptr(), 12) as *mut u8; - let child_header = unsafe { header_from_user_ptr(child) }; - let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); - let pushed = crate::array::js_array_push_f64(arr, child_box); - - assert_eq!(pushed, arr, "fixture should exercise the no-grow push path"); - assert_eq!( - test_layout_pointer_slot_count(pushed as usize, 3), - Some(1), - "heap writes into a numeric array must transition to a pointer-bearing layout" - ); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (pushed as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_numeric_array_layout_metadata_matches_gc_scan_state() { - clear_marks(); - clear_mark_seeds(); - - let mut arr = crate::array::js_array_alloc(4); - arr = crate::array::js_array_push_f64(arr, 1.0); - arr = crate::array::js_array_push_f64(arr, 2.0); - - assert_eq!(crate::array::js_array_is_numeric_f64_layout(arr), 1); - assert_numeric_array_trace_free(arr, 2); - - let child = crate::string::js_string_from_bytes(b"layout-child".as_ptr(), 12) as *mut u8; - let child_header = unsafe { header_from_user_ptr(child) }; - let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); - arr = crate::array::js_array_push_f64(arr, child_box); - - assert_eq!(crate::array::js_array_is_numeric_f64_layout(arr), 0); - assert_eq!(test_layout_pointer_slot_count(arr as usize, 3), Some(1)); - - clear_marks(); - clear_mark_seeds(); - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (arr as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_trace_object_uses_pointer_layout_mask() { - clear_marks(); - clear_mark_seeds(); - - let numeric = crate::object::js_object_alloc(0, 3); - crate::object::js_object_set_field(numeric, 0, crate::value::JSValue::number(1.0)); - crate::object::js_object_set_field(numeric, 1, crate::value::JSValue::number(2.0)); - crate::object::js_object_set_field(numeric, 2, crate::value::JSValue::bool(false)); - assert_eq!(test_layout_pointer_slot_count(numeric as usize, 3), Some(0)); - assert_eq!(test_heap_child_slot_count(numeric as *mut u8), 0); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (numeric as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 0); - clear_marks(); - clear_mark_seeds(); - - let child = crate::string::js_string_from_bytes(b"object-child".as_ptr(), 12); - let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; - let mixed = crate::object::js_object_alloc(0, 3); - crate::object::js_object_set_field(mixed, 0, crate::value::JSValue::number(1.0)); - crate::object::js_object_set_field(mixed, 1, crate::value::JSValue::string_ptr(child)); - crate::object::js_object_set_field(mixed, 2, crate::value::JSValue::number(3.0)); - assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (mixed as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_scans_only_declared_pointer_slots() { - clear_marks(); - clear_mark_seeds(); - - let child = crate::string::js_string_from_bytes(b"typed-child".as_ptr(), 11); - let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; - let obj = crate::object::js_object_alloc(0, 3); - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(1.0)); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::string_ptr(child)); - crate::object::js_object_set_field(obj, 2, crate::value::JSValue::number(3.0)); - - let mask = [1u64 << 1]; - js_gc_init_typed_shape_layout( - obj as u64, - 3, - std::ptr::null(), - 0, - mask.as_ptr(), - mask.len() as u32, - ); - - assert_eq!(test_layout_pointer_slot_count(obj as usize, 3), Some(1)); - assert_eq!(test_heap_child_slot_count(obj as *mut u8), 1); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_typed_shape_descriptor_dynamic_pointer_mutation_falls_back_to_unknown_layout() { - clear_marks(); - clear_mark_seeds(); - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(1.0)); - crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(2.0)); - js_gc_init_typed_shape_layout(obj as u64, 2, std::ptr::null(), 0, std::ptr::null(), 0); - assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); - - let child = crate::string::js_string_from_bytes(b"fallback-child".as_ptr(), 14); - let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; - crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); - - assert_eq!( - test_layout_pointer_slot_count(obj as usize, 2), - None, - "storing a pointer into a non-pointer typed slot must drop to safe full scanning" - ); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (obj as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 2); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -extern "C" fn layout_mask_test_closure(_closure: *const crate::closure::ClosureHeader) -> f64 { - 0.0 -} - -#[test] -fn test_trace_closure_uses_pointer_layout_mask() { - clear_marks(); - clear_mark_seeds(); - - let numeric = crate::closure::js_closure_alloc(layout_mask_test_closure as *const u8, 3); - crate::closure::js_closure_set_capture_f64(numeric, 0, 1.0); - crate::closure::js_closure_set_capture_f64(numeric, 1, 2.0); - crate::closure::js_closure_set_capture_ptr(numeric, 2, 7); - assert_eq!(test_layout_pointer_slot_count(numeric as usize, 3), Some(0)); - assert_eq!(test_heap_child_slot_count(numeric as *mut u8), 0); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (numeric as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 0); - clear_marks(); - clear_mark_seeds(); - - let child = crate::string::js_string_from_bytes(b"closure-child".as_ptr(), 13) as *mut u8; - let child_header = unsafe { header_from_user_ptr(child) }; - let mixed = crate::closure::js_closure_alloc(layout_mask_test_closure as *const u8, 3); - crate::closure::js_closure_set_capture_f64(mixed, 0, 1.0); - crate::closure::js_closure_set_capture_f64( - mixed, - 1, - f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), - ); - crate::closure::js_closure_set_capture_ptr(mixed, 2, 7); - assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); - - let valid_ptrs = build_valid_pointer_set(); - assert!(try_mark_value( - POINTER_TAG | (mixed as u64 & POINTER_MASK), - &valid_ptrs - )); - test_reset_trace_slot_reads(); - trace_marked_objects(&valid_ptrs); - assert_eq!(test_trace_slot_reads(), 1); - unsafe { - assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); - } - - clear_marks(); - clear_mark_seeds(); -} - -// Repsel Phase 4b.2 — an INT32-boxed numeric value reaching a raw-f64-masked -// object slot through the runtime store choke point must be canonicalized to -// raw f64 instead of permanently poisoning the object's typed layout. -// -// `layout_note_slot` treats any non-raw-f64 bit pattern landing in a raw-f64 -// slot as a representation change and calls `layout_set_typed_unknown`, which -// evicts the `TypedLayoutDescriptor` one-way, per object. INT32 boxes genuinely -// reach object fields from FFI / native modules (sqlite row columns, `v8` -// deserialization), so one FFI integer used to cost that object its typed fast -// path forever. Codegen's guarded class-field store already canonicalized -// inline behind its plain-finite check; `runtime_store_jsvalue_slot` wrote the -// bits verbatim. - -/// Install a two-slot typed descriptor: slot 0 raw-f64, slot 1 pointer. -unsafe fn typed_two_slot_object() -> (*mut crate::object::ObjectHeader, *mut u64) { - let (obj, fields) = alloc_old_test_object(2); - *fields = 0.0f64.to_bits(); - *fields.add(1) = crate::value::TAG_UNDEFINED; - let raw_mask = [0b01u64]; - let ptr_mask = [0b10u64]; - js_gc_init_typed_shape_layout( - obj as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - ptr_mask.as_ptr(), - ptr_mask.len() as u32, - ); - assert!( - layout_has_typed_descriptor(obj as usize), - "test setup: the typed descriptor must install" - ); - (obj, fields) -} - -#[test] -fn test_int32_store_into_raw_f64_slot_keeps_typed_descriptor() { - let _guard = GcTestIsolationGuard::new(); - let (obj, fields) = unsafe { typed_two_slot_object() }; - - // An INT32-boxed 42, exactly as an FFI / native module hands one over. - let int32_bits = crate::value::INT32_TAG | 42u64; - runtime_store_jsvalue_slot(obj as usize, fields as usize, 0, int32_bits); - - assert!( - layout_has_typed_descriptor(obj as usize), - "an INT32-boxed integer stored into a raw-f64 slot must not evict the typed descriptor" - ); - let stored = unsafe { std::ptr::read(fields as *const u64) }; - assert_eq!( - stored, - 42.0f64.to_bits(), - "the slot holds canonical raw f64 bits, not the INT32 box" - ); - assert_eq!( - f64::from_bits(stored), - 42.0, - "and reads back byte-exact as the same number" - ); -} - -#[test] -fn test_int32_store_into_pointer_slot_is_left_verbatim() { - let _guard = GcTestIsolationGuard::new(); - let (obj, fields) = unsafe { typed_two_slot_object() }; - - // Slot 1 is pointer-masked, not raw-f64-masked: there is no raw-f64 - // contract to uphold and the note is already a no-op there, so the stored - // bits must survive untouched. - let int32_bits = crate::value::INT32_TAG | 7u64; - let slot1 = unsafe { fields.add(1) }; - runtime_store_jsvalue_slot(obj as usize, slot1 as usize, 1, int32_bits); - - assert!( - layout_has_typed_descriptor(obj as usize), - "a non-pointer value in a pointer-masked slot leaves the descriptor intact" - ); - assert_eq!( - unsafe { std::ptr::read(slot1 as *const u64) }, - int32_bits, - "canonicalization is scoped to raw-f64-masked slots" - ); -} - -#[test] -fn test_non_numeric_store_into_raw_f64_slot_still_evicts_descriptor() { - let _guard = GcTestIsolationGuard::new(); - let (obj, fields) = unsafe { typed_two_slot_object() }; - - // The negative control for the fix: a string IS a genuine representation - // change for a raw-f64 slot and must still downgrade the object — the scan - // skips raw-f64 slots, so a mask left claiming "number here" over a live - // string pointer would strand it. - let payload = crate::string::js_string_from_bytes(b"not-a-number".as_ptr(), 12); - let payload_bits = STRING_TAG | (payload as u64 & POINTER_MASK); - runtime_store_jsvalue_slot(obj as usize, fields as usize, 0, payload_bits); - - assert!( - !layout_has_typed_descriptor(obj as usize), - "a non-numeric store into a raw-f64 slot must still evict the typed descriptor" - ); - assert_eq!( - unsafe { std::ptr::read(fields as *const u64) }, - payload_bits, - "and the stored value itself is untouched" - ); -} - -#[test] -fn test_int32_store_without_typed_descriptor_is_left_verbatim() { - let _guard = GcTestIsolationGuard::new(); - let (obj, fields) = unsafe { alloc_old_test_object(1) }; - unsafe { - *fields = 0.0f64.to_bits(); - } - assert!( - !layout_has_typed_descriptor(obj as usize), - "test setup: no descriptor installed" - ); - - let int32_bits = crate::value::INT32_TAG | 5u64; - runtime_store_jsvalue_slot(obj as usize, fields as usize, 0, int32_bits); - - assert_eq!( - unsafe { std::ptr::read(fields as *const u64) }, - int32_bits, - "with no intact descriptor there is no raw-f64 contract to uphold — bits stay verbatim" - ); -} - -/// #6921 — the `lower_new_impl` standalone-constructor exit returns a freshly -/// allocated class instance on which NO constructor has run, so the -/// `js_gc_init_typed_shape_layout` that exit now emits sees an object whose -/// every field is still `undefined`. That is the premise the fix rests on, so -/// pin it here rather than reasoning about it. -/// -/// Three properties, in the order they matter: -/// -/// 1. A fresh instance really is left at `GC_LAYOUT_POINTER_FREE` with no -/// descriptor — the one state in which the per-store `layout_note_slot` -/// call is load-bearing for GC correctness rather than a precision hint. -/// That is why an exit which skips the layout init blocks the note elision. -/// 2. A raw-f64 mask CANNOT be honoured over `undefined` fields, and -/// `init_typed_shape_layout` must downgrade to `GC_LAYOUT_UNKNOWN` — the -/// conservative state — never install a mask that fails to describe the -/// live words. This is what makes emitting the init on that exit SAFE. -/// 3. A pointer-only mask IS installed over `undefined` fields, so a later -/// pointer store into that slot is traced even though `layout_note_slot` -/// never ran. This is what makes emitting it USEFUL. -#[test] -fn typed_shape_layout_init_on_unconstructed_instance_is_conservative() { - let _guard = GcTestIsolationGuard::new(); - // `GcTestIsolationGuard` takes the thread's mutable-root scanner registry, - // which includes the runtime-handle scanner. Put it back BEFORE opening the - // scope below, or the handles are decorative and every object here is an - // unrooted raw pointer held across a GC-capable allocation — the same bug - // class (#6655) this test exists to be free of. - register_runtime_handle_root_scanner_for_tests(); - let scope = RuntimeHandleScope::new(); - clear_marks(); - clear_mark_seeds(); - - // Every allocation below is a GC point, and evacuation MOVES arena objects, - // so nothing may be held as a raw pointer across one. Each instance is - // rooted in `scope` the moment it is created and re-read through - // `handle_user` after any later allocation. - fn handle_user(handle: &RuntimeHandle<'_>) -> usize { - (handle.get_nanbox_u64() & POINTER_MASK) as usize - } - let layout_state = |user: usize| unsafe { - (*header_from_user_ptr(user as *const u8))._reserved & GC_LAYOUT_STATE_MASK - }; - let alloc_instance = - || crate::object::js_object_alloc_class_inline_keys(0, 0, 2, std::ptr::null_mut()) as usize; - - // (1) The unconstructed instance, exactly as `js_object_alloc_class_*` - // hands it to the standalone-ctor exit. - let fresh = scope.root_nanbox_u64(ptr_bits(alloc_instance())); - let fresh_user = handle_user(&fresh); - assert_eq!( - layout_state(fresh_user), - GC_LAYOUT_POINTER_FREE, - "a fresh class instance is POINTER_FREE — the collector scans zero \ - slots on it until something publishes a pointer bit" - ); - assert!( - !layout_has_typed_descriptor(fresh_user), - "and carries no typed descriptor" - ); - - // (2) Raw-f64 mask over all-`undefined` fields must land in UNKNOWN. - let raw_only = scope.root_nanbox_u64(ptr_bits(alloc_instance())); - let raw_only_user = handle_user(&raw_only); - let raw_mask = [0b01u64]; - js_gc_init_typed_shape_layout( - raw_only_user as u64, - 2, - raw_mask.as_ptr(), - raw_mask.len() as u32, - std::ptr::null(), - 0, - ); - assert_eq!( - layout_state(raw_only_user), - GC_LAYOUT_UNKNOWN, - "`undefined` is not raw-f64 bits, so the descriptor must be refused \ - and the object downgraded to the conservative state — never left \ - POINTER_FREE, never given a mask that misdescribes it" - ); - assert!( - !layout_has_typed_descriptor(raw_only_user), - "a refused descriptor must not be installed" - ); - - // (3) Pointer-only mask over all-`undefined` fields IS installed, and the - // slot is traced on a later store without any `layout_note_slot` call. - let ptr_only = scope.root_nanbox_u64(ptr_bits(alloc_instance())); - let ptr_only_user = handle_user(&ptr_only); - let ptr_mask = [0b01u64]; - js_gc_init_typed_shape_layout( - ptr_only_user as u64, - 2, - std::ptr::null(), - 0, - ptr_mask.as_ptr(), - ptr_mask.len() as u32, - ); - assert_eq!( - layout_state(ptr_only_user), - GC_LAYOUT_SIDE_MASK, - "a pointer mask is compatible with `undefined` fields and is installed" - ); - assert_eq!( - test_layout_pointer_slot_count(ptr_only_user, 2), - Some(1), - "slot 0 is published as pointer-bearing" - ); - - // The child is reachable ONLY through that slot; write it with a raw store - // so no `layout_note_slot` runs, then prove tracing still finds it. - let child = scope - .root_nanbox_u64(string_bits( - crate::string::js_string_from_bytes(b"6921-child".as_ptr(), 10) as usize, - )); - // Allocating the child was a GC point: re-read the instance rather than - // reusing `ptr_only_user`, which may now name from-space. - let ptr_only_user = handle_user(&ptr_only); - let fields = unsafe { - (ptr_only_user as *mut u8).add(std::mem::size_of::()) - as *mut u64 - }; - unsafe { - std::ptr::write(fields, child.get_nanbox_u64()); - } - - // Force a real collection here, with the conservative native-stack scan - // pinned OFF. That makes the `RuntimeHandleScope` above LOAD-BEARING rather - // than decorative: the raw Rust locals are no longer a safety net, so the - // scope is the only thing keeping these objects alive. Drop the scanner - // registration at the top of this test and this collection reclaims them. - { - let _scan = ConservativeScanDisabledGuard::new(); - let _ = collect_minor_trace(GcTriggerKind::Direct); - } - // Re-read everything through the handles — a copied minor relocates - // nursery objects and rewrites the rooted slots. - let ptr_only_user = handle_user(&ptr_only); - assert_eq!( - test_layout_pointer_slot_count(ptr_only_user, 2), - Some(1), - "the typed descriptor must survive the collection (and any relocation) \ - — the note-elision premise depends on it staying intact, not just on \ - it being installed once" - ); - - // A collection may have left objects marked; start the hand-driven mark - // from a known-clean state so the assertions below mean what they say. - clear_marks(); - clear_mark_seeds(); - let valid_ptrs = build_valid_pointer_set(); - assert!( - try_mark_value(ptr_bits(handle_user(&ptr_only)), &valid_ptrs), - "test setup: the instance marks as a root" - ); - trace_marked_objects(&valid_ptrs); - unsafe { - let child_header = header_from_user_ptr(handle_user(&child) as *const u8); - assert_ne!( - (*child_header).gc_flags & GC_FLAG_MARKED, - 0, - "the typed descriptor alone must make the pointer slot traceable — \ - this is the liveness `layout_note_slot` would otherwise have had \ - to establish store-by-store" - ); - } - - clear_marks(); - clear_mark_seeds(); -} diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs b/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs new file mode 100644 index 0000000000..e39db14c38 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs @@ -0,0 +1,325 @@ +use super::*; + +#[test] +fn test_layout_mask_overflow_fields_and_array_grow_transfer() { + clear_marks(); + clear_mark_seeds(); + // #6812 spill: overflow writes now allocate GC memory (meta record + + // spill buffer), so an automatic minor GC mid-build could move `obj` + // out from under this test's raw pointers. The test asserts layout and + // tracing, not move-resilience — pin the heap while building. + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = crate::string::js_string_from_bytes(b"overflow-child".as_ptr(), 14) as *mut u8; + let child_header = unsafe { header_from_user_ptr(child) }; + let obj = crate::object::js_object_alloc(0, 0); + for i in 0..9 { + let name = format!("k{i}"); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = if i == 8 { + f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)) + } else { + i as f64 + }; + crate::object::js_object_set_field_by_name(obj, key, value); + } + + // #6812 spill: the k8 pointer lives in the object-owned spill buffer + // (owner inline slots hold only k0..k3 numerics), so the pointer-slot + // count moves from the owner's mask to the buffer's. Legacy mode keeps + // the original owner-mask expectation. + if crate::object::test_object_spill_enabled() { + let spill = crate::object::test_spill_buffer_addr(obj as usize); + assert_ne!(spill, 0, "overflow write must have created a spill buffer"); + assert_eq!(test_layout_pointer_slot_count(spill, 9), Some(1)); + } else { + assert_eq!(test_layout_pointer_slot_count(obj as usize, 9), Some(1)); + } + let valid_ptrs = build_valid_pointer_set(); + let mut worklist = Vec::new(); + unsafe { + trace_object(obj as *mut u8, &valid_ptrs, &mut worklist); + // #6812 spill: the overflow value is no longer owner-adjacent — the + // chain is obj → meta record → spill buffer → child, so drain the + // worklist exactly like production marking does instead of relying + // on a single hop. + while let Some(queued) = worklist.pop() { + let user = (queued as *mut u8).add(crate::gc::GC_HEADER_SIZE); + trace_object(user, &valid_ptrs, &mut worklist); + } + } + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + let arr = crate::array::js_array_alloc_with_length(1); + crate::array::js_array_set_f64( + arr, + 0, + f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), + ); + let grown = crate::array::js_array_grow(arr, 128); + assert_eq!(test_layout_pointer_slot_count(grown as usize, 1), Some(1)); + + let moved = crate::array::js_array_alloc_with_length(1); + unsafe { + layout_transfer(grown as *mut u8, moved as *mut u8); + } + assert_eq!(test_layout_pointer_slot_count(moved as usize, 1), Some(1)); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_trace_array_uses_pointer_layout_mask() { + clear_marks(); + clear_mark_seeds(); + + let numeric = crate::array::js_array_alloc_with_length(3); + crate::array::js_array_set_f64(numeric, 0, 1.0); + crate::array::js_array_set_f64(numeric, 1, 2.0); + crate::array::js_array_set_f64(numeric, 2, 3.0); + assert_eq!(test_layout_pointer_slot_count(numeric as usize, 3), Some(0)); + assert_eq!(test_heap_child_slot_count(numeric as *mut u8), 0); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (numeric as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 0); + clear_marks(); + clear_mark_seeds(); + + let child = crate::string::js_string_from_bytes(b"array-child".as_ptr(), 11) as *mut u8; + let child_header = unsafe { header_from_user_ptr(child) }; + let mixed = crate::array::js_array_alloc_with_length(3); + crate::array::js_array_set_f64(mixed, 0, 1.0); + crate::array::js_array_set_f64( + mixed, + 1, + f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), + ); + crate::array::js_array_set_f64(mixed, 2, 3.0); + assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (mixed as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +fn assert_array_root_trace_reads(arr: *mut crate::array::ArrayHeader, expected_reads: usize) { + clear_marks(); + clear_mark_seeds(); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (arr as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), expected_reads); +} + +fn assert_numeric_array_trace_free(arr: *mut crate::array::ArrayHeader, len: usize) { + assert_eq!(test_layout_pointer_slot_count(arr as usize, len), Some(0)); + assert_eq!(test_heap_child_slot_count(arr as *mut u8), 0); + assert_array_root_trace_reads(arr, 0); +} + +#[test] +fn test_array_numeric_producers_stay_pointer_free() { + clear_marks(); + clear_mark_seeds(); + + let values = [1.0, 2.5, 3.0, 4.25]; + let from_f64 = crate::array::js_array_from_f64(values.as_ptr(), values.len() as u32); + assert_numeric_array_trace_free(from_f64, values.len()); + + let keys_src = crate::array::js_array_alloc_with_length(4); + for i in 0..4 { + crate::array::js_array_set_f64(keys_src, i, (i + 10) as f64); + } + let keys = crate::array::js_array_keys(keys_src); + assert_numeric_array_trace_free(keys, 4); + + let filled = crate::array::js_array_alloc_with_length(4); + crate::array::js_array_fill(filled, 42.0); + assert_numeric_array_trace_free(filled, 4); + + let cloned = crate::array::js_array_clone(filled); + assert_numeric_array_trace_free(cloned, 4); + + let concat_dest = crate::array::js_array_alloc(0); + let concatenated = crate::array::js_array_concat(concat_dest, filled); + assert_numeric_array_trace_free(concatenated, 4); + + crate::array::js_array_copy_within(concatenated, 1.0, 0.0, 0, 0.0); + assert_numeric_array_trace_free(concatenated, 4); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_array_mixed_bulk_producers_preserve_pointer_layout() { + clear_marks(); + clear_mark_seeds(); + + let child = crate::string::js_string_from_bytes(b"bulk-child".as_ptr(), 10) as *mut u8; + let child_header = unsafe { header_from_user_ptr(child) }; + let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); + + let src = crate::array::js_array_alloc_with_length(2); + crate::array::js_array_set_f64(src, 0, 1.0); + crate::array::js_array_set_f64(src, 1, child_box); + + let cloned = crate::array::js_array_clone(src); + assert_eq!(test_layout_pointer_slot_count(cloned as usize, 2), Some(1)); + assert_array_root_trace_reads(cloned, 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + clear_marks(); + clear_mark_seeds(); + + let concatenated = crate::array::js_array_concat(crate::array::js_array_alloc(0), src); + assert_eq!( + test_layout_pointer_slot_count(concatenated as usize, 2), + Some(1) + ); + assert_array_root_trace_reads(concatenated, 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + clear_marks(); + clear_mark_seeds(); + + let set = crate::set::js_set_alloc(4); + let set = crate::set::js_set_add(set, child_box); + let set_arr = crate::set::js_set_to_array(set); + assert_eq!(test_layout_pointer_slot_count(set_arr as usize, 1), Some(1)); + assert_array_root_trace_reads(set_arr, 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + clear_marks(); + clear_mark_seeds(); + + let map = crate::map::js_map_alloc(4); + let map = crate::map::js_map_set(map, 7.0, child_box); + let entries = crate::map::js_map_entries(map); + assert_eq!(test_layout_pointer_slot_count(entries as usize, 1), Some(1)); + let pair_box = crate::array::js_array_get_f64(entries, 0); + let pair = (pair_box.to_bits() & POINTER_MASK) as *mut crate::array::ArrayHeader; + assert_eq!(test_layout_pointer_slot_count(pair as usize, 2), Some(1)); + assert_array_root_trace_reads(entries, 2); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + clear_marks(); + clear_mark_seeds(); + + let overwritten = crate::array::js_array_alloc_with_length(1); + crate::array::js_array_set_f64(overwritten, 0, child_box); + assert_eq!( + test_layout_pointer_slot_count(overwritten as usize, 1), + Some(1) + ); + crate::array::js_array_set_f64(overwritten, 0, 99.0); + assert_numeric_array_trace_free(overwritten, 1); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_numeric_array_push_heap_value_transitions_and_traces() { + clear_marks(); + clear_mark_seeds(); + + let mut arr = crate::array::js_array_alloc(4); + arr = crate::array::js_array_push_f64(arr, 1.0); + arr = crate::array::js_array_push_f64(arr, 2.0); + assert_eq!(test_layout_pointer_slot_count(arr as usize, 2), Some(0)); + + let child = crate::string::js_string_from_bytes(b"pushed-child".as_ptr(), 12) as *mut u8; + let child_header = unsafe { header_from_user_ptr(child) }; + let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); + let pushed = crate::array::js_array_push_f64(arr, child_box); + + assert_eq!(pushed, arr, "fixture should exercise the no-grow push path"); + assert_eq!( + test_layout_pointer_slot_count(pushed as usize, 3), + Some(1), + "heap writes into a numeric array must transition to a pointer-bearing layout" + ); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (pushed as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_numeric_array_layout_metadata_matches_gc_scan_state() { + clear_marks(); + clear_mark_seeds(); + + let mut arr = crate::array::js_array_alloc(4); + arr = crate::array::js_array_push_f64(arr, 1.0); + arr = crate::array::js_array_push_f64(arr, 2.0); + + assert_eq!(crate::array::js_array_is_numeric_f64_layout(arr), 1); + assert_numeric_array_trace_free(arr, 2); + + let child = crate::string::js_string_from_bytes(b"layout-child".as_ptr(), 12) as *mut u8; + let child_header = unsafe { header_from_user_ptr(child) }; + let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); + arr = crate::array::js_array_push_f64(arr, child_box); + + assert_eq!(crate::array::js_array_is_numeric_f64_layout(arr), 0); + assert_eq!(test_layout_pointer_slot_count(arr as usize, 3), Some(1)); + + clear_marks(); + clear_mark_seeds(); + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (arr as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs b/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs new file mode 100644 index 0000000000..e1a750d632 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs @@ -0,0 +1,481 @@ +use super::*; + +#[test] +fn test_trace_object_uses_pointer_layout_mask() { + clear_marks(); + clear_mark_seeds(); + + let numeric = crate::object::js_object_alloc(0, 3); + crate::object::js_object_set_field(numeric, 0, crate::value::JSValue::number(1.0)); + crate::object::js_object_set_field(numeric, 1, crate::value::JSValue::number(2.0)); + crate::object::js_object_set_field(numeric, 2, crate::value::JSValue::bool(false)); + assert_eq!(test_layout_pointer_slot_count(numeric as usize, 3), Some(0)); + assert_eq!(test_heap_child_slot_count(numeric as *mut u8), 0); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (numeric as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 0); + clear_marks(); + clear_mark_seeds(); + + let child = crate::string::js_string_from_bytes(b"object-child".as_ptr(), 12); + let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; + let mixed = crate::object::js_object_alloc(0, 3); + crate::object::js_object_set_field(mixed, 0, crate::value::JSValue::number(1.0)); + crate::object::js_object_set_field(mixed, 1, crate::value::JSValue::string_ptr(child)); + crate::object::js_object_set_field(mixed, 2, crate::value::JSValue::number(3.0)); + assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (mixed as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_scans_only_declared_pointer_slots() { + clear_marks(); + clear_mark_seeds(); + + let child = crate::string::js_string_from_bytes(b"typed-child".as_ptr(), 11); + let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; + let obj = crate::object::js_object_alloc(0, 3); + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(1.0)); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::string_ptr(child)); + crate::object::js_object_set_field(obj, 2, crate::value::JSValue::number(3.0)); + + let mask = [1u64 << 1]; + js_gc_init_typed_shape_layout( + obj as u64, + 3, + std::ptr::null(), + 0, + mask.as_ptr(), + mask.len() as u32, + ); + + assert_eq!(test_layout_pointer_slot_count(obj as usize, 3), Some(1)); + assert_eq!(test_heap_child_slot_count(obj as *mut u8), 1); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_dynamic_pointer_mutation_falls_back_to_unknown_layout() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(1.0)); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(2.0)); + js_gc_init_typed_shape_layout(obj as u64, 2, std::ptr::null(), 0, std::ptr::null(), 0); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); + + let child = crate::string::js_string_from_bytes(b"fallback-child".as_ptr(), 14); + let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); + + assert_eq!( + test_layout_pointer_slot_count(obj as usize, 2), + None, + "storing a pointer into a non-pointer typed slot must drop to safe full scanning" + ); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 2); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +extern "C" fn layout_mask_test_closure(_closure: *const crate::closure::ClosureHeader) -> f64 { + 0.0 +} + +#[test] +fn test_trace_closure_uses_pointer_layout_mask() { + clear_marks(); + clear_mark_seeds(); + + let numeric = crate::closure::js_closure_alloc(layout_mask_test_closure as *const u8, 3); + crate::closure::js_closure_set_capture_f64(numeric, 0, 1.0); + crate::closure::js_closure_set_capture_f64(numeric, 1, 2.0); + crate::closure::js_closure_set_capture_ptr(numeric, 2, 7); + assert_eq!(test_layout_pointer_slot_count(numeric as usize, 3), Some(0)); + assert_eq!(test_heap_child_slot_count(numeric as *mut u8), 0); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (numeric as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 0); + clear_marks(); + clear_mark_seeds(); + + let child = crate::string::js_string_from_bytes(b"closure-child".as_ptr(), 13) as *mut u8; + let child_header = unsafe { header_from_user_ptr(child) }; + let mixed = crate::closure::js_closure_alloc(layout_mask_test_closure as *const u8, 3); + crate::closure::js_closure_set_capture_f64(mixed, 0, 1.0); + crate::closure::js_closure_set_capture_f64( + mixed, + 1, + f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), + ); + crate::closure::js_closure_set_capture_ptr(mixed, 2, 7); + assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (mixed as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 1); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +// Repsel Phase 4b.2 — an INT32-boxed numeric value reaching a raw-f64-masked +// object slot through the runtime store choke point must be canonicalized to +// raw f64 instead of permanently poisoning the object's typed layout. +// +// `layout_note_slot` treats any non-raw-f64 bit pattern landing in a raw-f64 +// slot as a representation change and calls `layout_set_typed_unknown`, which +// evicts the `TypedLayoutDescriptor` one-way, per object. INT32 boxes genuinely +// reach object fields from FFI / native modules (sqlite row columns, `v8` +// deserialization), so one FFI integer used to cost that object its typed fast +// path forever. Codegen's guarded class-field store already canonicalized +// inline behind its plain-finite check; `runtime_store_jsvalue_slot` wrote the +// bits verbatim. + +/// Install a two-slot typed descriptor: slot 0 raw-f64, slot 1 pointer. +unsafe fn typed_two_slot_object() -> (*mut crate::object::ObjectHeader, *mut u64) { + let (obj, fields) = alloc_old_test_object(2); + *fields = 0.0f64.to_bits(); + *fields.add(1) = crate::value::TAG_UNDEFINED; + let raw_mask = [0b01u64]; + let ptr_mask = [0b10u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + ptr_mask.as_ptr(), + ptr_mask.len() as u32, + ); + assert!( + layout_has_typed_descriptor(obj as usize), + "test setup: the typed descriptor must install" + ); + (obj, fields) +} + +#[test] +fn test_int32_store_into_raw_f64_slot_keeps_typed_descriptor() { + let _guard = GcTestIsolationGuard::new(); + let (obj, fields) = unsafe { typed_two_slot_object() }; + + // An INT32-boxed 42, exactly as an FFI / native module hands one over. + let int32_bits = crate::value::INT32_TAG | 42u64; + runtime_store_jsvalue_slot(obj as usize, fields as usize, 0, int32_bits); + + assert!( + layout_has_typed_descriptor(obj as usize), + "an INT32-boxed integer stored into a raw-f64 slot must not evict the typed descriptor" + ); + let stored = unsafe { std::ptr::read(fields as *const u64) }; + assert_eq!( + stored, + 42.0f64.to_bits(), + "the slot holds canonical raw f64 bits, not the INT32 box" + ); + assert_eq!( + f64::from_bits(stored), + 42.0, + "and reads back byte-exact as the same number" + ); +} + +#[test] +fn test_int32_store_into_pointer_slot_is_left_verbatim() { + let _guard = GcTestIsolationGuard::new(); + let (obj, fields) = unsafe { typed_two_slot_object() }; + + // Slot 1 is pointer-masked, not raw-f64-masked: there is no raw-f64 + // contract to uphold and the note is already a no-op there, so the stored + // bits must survive untouched. + let int32_bits = crate::value::INT32_TAG | 7u64; + let slot1 = unsafe { fields.add(1) }; + runtime_store_jsvalue_slot(obj as usize, slot1 as usize, 1, int32_bits); + + assert!( + layout_has_typed_descriptor(obj as usize), + "a non-pointer value in a pointer-masked slot leaves the descriptor intact" + ); + assert_eq!( + unsafe { std::ptr::read(slot1 as *const u64) }, + int32_bits, + "canonicalization is scoped to raw-f64-masked slots" + ); +} + +#[test] +fn test_non_numeric_store_into_raw_f64_slot_still_evicts_descriptor() { + let _guard = GcTestIsolationGuard::new(); + let (obj, fields) = unsafe { typed_two_slot_object() }; + + // The negative control for the fix: a string IS a genuine representation + // change for a raw-f64 slot and must still downgrade the object — the scan + // skips raw-f64 slots, so a mask left claiming "number here" over a live + // string pointer would strand it. + let payload = crate::string::js_string_from_bytes(b"not-a-number".as_ptr(), 12); + let payload_bits = STRING_TAG | (payload as u64 & POINTER_MASK); + runtime_store_jsvalue_slot(obj as usize, fields as usize, 0, payload_bits); + + assert!( + !layout_has_typed_descriptor(obj as usize), + "a non-numeric store into a raw-f64 slot must still evict the typed descriptor" + ); + assert_eq!( + unsafe { std::ptr::read(fields as *const u64) }, + payload_bits, + "and the stored value itself is untouched" + ); +} + +#[test] +fn test_int32_store_without_typed_descriptor_is_left_verbatim() { + let _guard = GcTestIsolationGuard::new(); + let (obj, fields) = unsafe { alloc_old_test_object(1) }; + unsafe { + *fields = 0.0f64.to_bits(); + } + assert!( + !layout_has_typed_descriptor(obj as usize), + "test setup: no descriptor installed" + ); + + let int32_bits = crate::value::INT32_TAG | 5u64; + runtime_store_jsvalue_slot(obj as usize, fields as usize, 0, int32_bits); + + assert_eq!( + unsafe { std::ptr::read(fields as *const u64) }, + int32_bits, + "with no intact descriptor there is no raw-f64 contract to uphold — bits stay verbatim" + ); +} + +/// #6921 — the `lower_new_impl` standalone-constructor exit returns a freshly +/// allocated class instance on which NO constructor has run, so the +/// `js_gc_init_typed_shape_layout` that exit now emits sees an object whose +/// every field is still `undefined`. That is the premise the fix rests on, so +/// pin it here rather than reasoning about it. +/// +/// Three properties, in the order they matter: +/// +/// 1. A fresh instance really is left at `GC_LAYOUT_POINTER_FREE` with no +/// descriptor — the one state in which the per-store `layout_note_slot` +/// call is load-bearing for GC correctness rather than a precision hint. +/// That is why an exit which skips the layout init blocks the note elision. +/// 2. A raw-f64 mask CANNOT be honoured over `undefined` fields, and +/// `init_typed_shape_layout` must downgrade to `GC_LAYOUT_UNKNOWN` — the +/// conservative state — never install a mask that fails to describe the +/// live words. This is what makes emitting the init on that exit SAFE. +/// 3. A pointer-only mask IS installed over `undefined` fields, so a later +/// pointer store into that slot is traced even though `layout_note_slot` +/// never ran. This is what makes emitting it USEFUL. +#[test] +fn typed_shape_layout_init_on_unconstructed_instance_is_conservative() { + let _guard = GcTestIsolationGuard::new(); + // `GcTestIsolationGuard` takes the thread's mutable-root scanner registry, + // which includes the runtime-handle scanner. Put it back BEFORE opening the + // scope below, or the handles are decorative and every object here is an + // unrooted raw pointer held across a GC-capable allocation — the same bug + // class (#6655) this test exists to be free of. + register_runtime_handle_root_scanner_for_tests(); + let scope = RuntimeHandleScope::new(); + clear_marks(); + clear_mark_seeds(); + + // Every allocation below is a GC point, and evacuation MOVES arena objects, + // so nothing may be held as a raw pointer across one. Each instance is + // rooted in `scope` the moment it is created and re-read through + // `handle_user` after any later allocation. + fn handle_user(handle: &RuntimeHandle<'_>) -> usize { + (handle.get_nanbox_u64() & POINTER_MASK) as usize + } + let layout_state = |user: usize| unsafe { + (*header_from_user_ptr(user as *const u8))._reserved & GC_LAYOUT_STATE_MASK + }; + let alloc_instance = + || crate::object::js_object_alloc_class_inline_keys(0, 0, 2, std::ptr::null_mut()) as usize; + + // (1) The unconstructed instance, exactly as `js_object_alloc_class_*` + // hands it to the standalone-ctor exit. + let fresh = scope.root_nanbox_u64(ptr_bits(alloc_instance())); + let fresh_user = handle_user(&fresh); + assert_eq!( + layout_state(fresh_user), + GC_LAYOUT_POINTER_FREE, + "a fresh class instance is POINTER_FREE — the collector scans zero \ + slots on it until something publishes a pointer bit" + ); + assert!( + !layout_has_typed_descriptor(fresh_user), + "and carries no typed descriptor" + ); + + // (2) Raw-f64 mask over all-`undefined` fields must land in UNKNOWN. + let raw_only = scope.root_nanbox_u64(ptr_bits(alloc_instance())); + let raw_only_user = handle_user(&raw_only); + let raw_mask = [0b01u64]; + js_gc_init_typed_shape_layout( + raw_only_user as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + assert_eq!( + layout_state(raw_only_user), + GC_LAYOUT_UNKNOWN, + "`undefined` is not raw-f64 bits, so the descriptor must be refused \ + and the object downgraded to the conservative state — never left \ + POINTER_FREE, never given a mask that misdescribes it" + ); + assert!( + !layout_has_typed_descriptor(raw_only_user), + "a refused descriptor must not be installed" + ); + + // (3) Pointer-only mask over all-`undefined` fields IS installed, and the + // slot is traced on a later store without any `layout_note_slot` call. + let ptr_only = scope.root_nanbox_u64(ptr_bits(alloc_instance())); + let ptr_only_user = handle_user(&ptr_only); + let ptr_mask = [0b01u64]; + js_gc_init_typed_shape_layout( + ptr_only_user as u64, + 2, + std::ptr::null(), + 0, + ptr_mask.as_ptr(), + ptr_mask.len() as u32, + ); + assert_eq!( + layout_state(ptr_only_user), + GC_LAYOUT_SIDE_MASK, + "a pointer mask is compatible with `undefined` fields and is installed" + ); + assert_eq!( + test_layout_pointer_slot_count(ptr_only_user, 2), + Some(1), + "slot 0 is published as pointer-bearing" + ); + + // The child is reachable ONLY through that slot; write it with a raw store + // so no `layout_note_slot` runs, then prove tracing still finds it. + let child = scope + .root_nanbox_u64(string_bits( + crate::string::js_string_from_bytes(b"6921-child".as_ptr(), 10) as usize, + )); + // Allocating the child was a GC point: re-read the instance rather than + // reusing `ptr_only_user`, which may now name from-space. + let ptr_only_user = handle_user(&ptr_only); + let fields = unsafe { + (ptr_only_user as *mut u8).add(std::mem::size_of::()) + as *mut u64 + }; + unsafe { + std::ptr::write(fields, child.get_nanbox_u64()); + } + + // Force a real collection here, with the conservative native-stack scan + // pinned OFF. That makes the `RuntimeHandleScope` above LOAD-BEARING rather + // than decorative: the raw Rust locals are no longer a safety net, so the + // scope is the only thing keeping these objects alive. Drop the scanner + // registration at the top of this test and this collection reclaims them. + { + let _scan = ConservativeScanDisabledGuard::new(); + let _ = collect_minor_trace(GcTriggerKind::Direct); + } + // Re-read everything through the handles — a copied minor relocates + // nursery objects and rewrites the rooted slots. + let ptr_only_user = handle_user(&ptr_only); + assert_eq!( + test_layout_pointer_slot_count(ptr_only_user, 2), + Some(1), + "the typed descriptor must survive the collection (and any relocation) \ + — the note-elision premise depends on it staying intact, not just on \ + it being installed once" + ); + + // A collection may have left objects marked; start the hand-driven mark + // from a known-clean state so the assertions below mean what they say. + clear_marks(); + clear_mark_seeds(); + let valid_ptrs = build_valid_pointer_set(); + assert!( + try_mark_value(ptr_bits(handle_user(&ptr_only)), &valid_ptrs), + "test setup: the instance marks as a root" + ); + trace_marked_objects(&valid_ptrs); + unsafe { + let child_header = header_from_user_ptr(handle_user(&child) as *const u8); + assert_ne!( + (*child_header).gc_flags & GC_FLAG_MARKED, + 0, + "the typed descriptor alone must make the pointer slot traceable — \ + this is the liveness `layout_note_slot` would otherwise have had \ + to establish store-by-store" + ); + } + + clear_marks(); + clear_mark_seeds(); +} diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs b/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs new file mode 100644 index 0000000000..0f12a03f41 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs @@ -0,0 +1,517 @@ +use super::*; + +#[test] +fn test_typed_shape_descriptor_preserves_pointer_slots_after_non_pointer_overwrite() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 2); + let mask = [0b10u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + std::ptr::null(), + 0, + mask.as_ptr(), + mask.len() as u32, + ); + + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); + assert_eq!(test_heap_child_slot_count(obj as *mut u8), 1); + + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(7.0)); + + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); + assert_eq!(test_heap_child_slot_count(obj as *mut u8), 1); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_pointer_write_to_non_pointer_slot_falls_back() { + clear_marks(); + clear_mark_seeds(); + + let child = crate::string::js_string_from_bytes(b"typed-child".as_ptr(), 11); + let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; + let obj = crate::object::js_object_alloc(0, 2); + let mask = [0b10u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + std::ptr::null(), + 0, + mask.as_ptr(), + mask.len() as u32, + ); + + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); + + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), None); + assert_eq!(test_heap_child_slot_count(obj as *mut u8), 2); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + trace_marked_objects(&valid_ptrs); + unsafe { + assert_ne!( + (*child_header).gc_flags & GC_FLAG_MARKED, + 0, + "fallback all-field tracing should mark a pointer written to a numeric slot" + ); + } + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_tracks_raw_numeric_slots() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.5); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(2.5)); + let raw_mask = [0b01u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); + assert!(!layout_typed_raw_f64_slot_for_user(obj as usize, 1)); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); + + let child = crate::string::js_string_from_bytes(b"raw-child".as_ptr(), 9); + let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); + + assert!( + !layout_typed_raw_f64_slot_for_user(obj as usize, 0), + "non-number writes must clear the exact raw-f64 descriptor" + ); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), None); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + trace_marked_objects(&valid_ptrs); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +/// #6957 regression guard: the typed descriptor of a **shape-keyed** object must +/// be visible to the layout query helpers. +/// +/// #6893 keys the canonical descriptor by the shared `keys_array` (`SHAPE_LAYOUTS`) +/// and deletes the per-object `TYPED_LAYOUTS` entry — so every class instance +/// (the only objects that carry a keys_array) moved to the shared map. Every +/// other test in this file allocates with `js_object_alloc` (class 0, no +/// keys_array), which still takes the per-object path; that is precisely why the +/// query helpers could go blind on real class instances with the whole layout +/// suite green. +#[test] +fn test_typed_shape_descriptor_visible_for_shape_keyed_objects() { + clear_marks(); + clear_mark_seeds(); + + let packed = b"x\0y\0"; + let keys = crate::object::js_build_class_keys_array( + 0x6957_01, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + let first = crate::object::js_object_alloc_class_inline_keys(0x6957_01, 0, 2, keys); + let second = crate::object::js_object_alloc_class_inline_keys(0x6957_01, 0, 2, keys); + unsafe { + assert_eq!( + (*first).keys_array, + (*second).keys_array, + "same-shape objects must share one canonical keys array" + ); + } + + let raw_mask = [0b01u64]; + for object in [first, second] { + crate::object::js_object_set_unboxed_f64_field(object, 0, 1.5); + crate::object::js_object_set_field(object, 1, crate::value::JSValue::number(2.5)); + js_gc_init_typed_shape_layout( + object as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + } + + for object in [first, second] { + let user = object as usize; + assert!( + layout_typed_intact_for_user(user), + "the shared shape install must set the intact bit" + ); + assert!( + layout_typed_raw_f64_slot_for_user(user, 0), + "slot 0 is raw-f64 in the shape descriptor" + ); + assert!(!layout_typed_raw_f64_slot_for_user(user, 1)); + assert!( + layout_slot_is_raw_f64_typed(user, 0), + "the store fast path must agree with layout_note_slot's own resolution" + ); + assert!( + layout_typed_accepts_finite_number_slot_for_user(user, 1), + "an ordinary JSValue slot of an intact descriptor accepts finite numbers" + ); + } + + // A contradicting store downgrades ONLY the object that made it. The shared + // entry cannot be removed (it still describes every sibling), so the intact + // bit is what separates the two — assert both halves. + let payload = crate::string::js_string_from_bytes(b"boxed".as_ptr(), 5); + crate::object::js_object_set_field(first, 0, crate::value::JSValue::string_ptr(payload)); + + assert!( + !layout_typed_raw_f64_slot_for_user(first as usize, 0), + "a boxed store into a raw-f64 slot must evict this object's descriptor" + ); + assert!(!layout_slot_is_raw_f64_typed(first as usize, 0)); + assert!( + !layout_typed_accepts_finite_number_slot_for_user(first as usize, 0), + "a downgraded object must not keep reading its shape's stale descriptor" + ); + assert!( + layout_typed_raw_f64_slot_for_user(second as usize, 0), + "the sibling never diverged and must keep the shared shape descriptor" + ); + assert!(layout_slot_is_raw_f64_typed(second as usize, 0)); + + clear_marks(); + clear_mark_seeds(); +} + +/// #6964: `layout_transfer` resolved the moved object's typed descriptor only +/// through the per-object `TYPED_LAYOUTS` map. #6893 moved the canonical +/// descriptor of every object carrying a `keys_array` (i.e. every class +/// instance) into the shape-keyed `SHAPE_LAYOUTS` map and DELETED the per-object +/// entry, so that lookup missed and the relocated copy had a still-valid +/// `GC_OBJ_TYPED_LAYOUT_INTACT` bit cleared. +/// +/// Deliberately a *shape-keyed* object: every pre-existing `layout_transfer` +/// test allocates with `js_object_alloc` (class 0, no keys_array), which keeps +/// its per-object entry and therefore takes the surviving path. That gap is why +/// #6893 merged green. +#[test] +fn test_shape_keyed_typed_layout_survives_layout_transfer() { + clear_marks(); + clear_mark_seeds(); + + let packed = b"x\0y\0"; + let keys = crate::object::js_build_class_keys_array( + 0x6964_01, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + let src = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); + crate::object::js_object_set_unboxed_f64_field(src, 0, 1.5); + crate::object::js_object_set_field(src, 1, crate::value::JSValue::number(2.5)); + let raw_mask = [0b01u64]; + js_gc_init_typed_shape_layout( + src as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + assert!(layout_typed_intact_for_user(src as usize)); + assert!(layout_typed_raw_f64_slot_for_user(src as usize, 0)); + + // Model an evacuation copy the way every caller performs it: a destination + // of the same shape, payload copied verbatim, `_reserved` propagated, then + // `layout_transfer`. + let dst = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); + unsafe { + let header_size = std::mem::size_of::(); + std::ptr::copy_nonoverlapping( + src as *const u8, + dst as *mut u8, + header_size + 2 * std::mem::size_of::(), + ); + let src_header = header_from_user_ptr(src as *const u8); + let dst_header = header_from_user_ptr(dst as *const u8); + (*(dst_header as *mut GcHeader))._reserved = (*src_header)._reserved; + layout_transfer(src as *mut u8, dst as *mut u8); + } + + assert!( + layout_typed_intact_for_user(dst as usize), + "a relocated shape-keyed object must keep GC_OBJ_TYPED_LAYOUT_INTACT — its \ + SHAPE_LAYOUTS descriptor is keyed by the shared keys_array, which the copy carries" + ); + assert!( + layout_typed_raw_f64_slot_for_user(dst as usize, 0), + "slot 0 is still raw-f64 after relocation" + ); + assert!(!layout_typed_raw_f64_slot_for_user(dst as usize, 1)); + assert!( + layout_slot_is_raw_f64_typed(dst as usize, 0), + "the store fast path must agree with the descriptor after relocation" + ); + + // The source is downgraded on transfer (it is dead / a forwarding stub), and + // that must NOT take the shared entry with it: an untouched sibling still + // reads the shape descriptor. + let sibling = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); + crate::object::js_object_set_unboxed_f64_field(sibling, 0, 7.5); + crate::object::js_object_set_field(sibling, 1, crate::value::JSValue::number(8.5)); + js_gc_init_typed_shape_layout( + sibling as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + assert!(layout_typed_raw_f64_slot_for_user(sibling as usize, 0)); + + clear_marks(); + clear_mark_seeds(); +} + +/// #6964, but driven through the real evacuation path (`gc/copying.rs`'s +/// `layout_transfer` call site) instead of calling the helper directly. +#[test] +fn test_shape_keyed_typed_layout_survives_copying_minor() { + let _guard = CopyingNurseryTestGuard::new(1); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let packed = b"x\0y\0"; + let keys = crate::object::js_build_class_keys_array( + 0x6964_02, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + let obj = crate::object::js_object_alloc_class_inline_keys(0x6964_02, 0, 2, keys); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 10.5); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(-3.25)); + let raw_mask = [0b01u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + assert!(layout_typed_intact_for_user(obj as usize)); + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); + js_shadow_slot_set(0, ptr_bits(obj as usize)); + + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); + + let after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; + assert_ne!( + after, obj as usize, + "the copying minor must actually relocate the instance — an inert arm proves nothing" + ); + + let fields = unsafe { + (after as *const u8).add(std::mem::size_of::()) as *const u64 + }; + assert_eq!(f64::from_bits(unsafe { *fields.add(0) }), 10.5); + + assert!( + layout_typed_intact_for_user(after), + "#6964: the relocated class instance must keep its shape-keyed typed layout" + ); + assert!( + layout_typed_raw_f64_slot_for_user(after, 0), + "#6964: the shape descriptor still describes slot 0 as raw-f64 after relocation" + ); + assert!(layout_slot_is_raw_f64_typed(after, 0)); +} + +#[test] +fn test_typed_shape_raw_numeric_slots_accept_pointer_like_f64_bits() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 2); + let pointer_like_number = f64::from_bits(0x1000); + crate::object::js_object_set_unboxed_f64_field(obj, 0, pointer_like_number); + let child = crate::string::js_string_from_bytes(b"mixed-child".as_ptr(), 11); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::string_ptr(child)); + + let raw_mask = [0b01u64]; + let pointer_mask = [0b10u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + pointer_mask.as_ptr(), + pointer_mask.len() as u32, + ); + + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); + + let next_pointer_like_number = f64::from_bits(0x2000); + crate::object::js_object_set_unboxed_f64_field(obj, 0, next_pointer_like_number); + assert!( + layout_typed_raw_f64_slot_for_user(obj as usize, 0), + "raw f64 slots must not be downgraded by numeric payload bits that resemble raw pointers" + ); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_rejects_nanbox_non_number_tags() { + clear_marks(); + clear_mark_seeds(); + + let raw_mask = [0b1u64]; + let obj = crate::object::js_object_alloc(0, 1); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.5); + js_gc_init_typed_shape_layout( + obj as u64, + 1, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); + + let short = crate::value::JSValue::try_short_string(b"abc").unwrap(); + crate::object::js_object_set_field(obj, 0, short); + assert!( + !layout_typed_raw_f64_slot_for_user(obj as usize, 0), + "SSO string tags must downgrade raw-f64 descriptors" + ); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 1), None); + + let handle_obj = crate::object::js_object_alloc(0, 1); + crate::object::js_object_set_unboxed_f64_field(handle_obj, 0, 2.5); + js_gc_init_typed_shape_layout( + handle_obj as u64, + 1, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + assert!(layout_typed_raw_f64_slot_for_user(handle_obj as usize, 0)); + + let handle = crate::value::JSValue::from_bits(crate::value::JS_HANDLE_TAG | 0x1234); + crate::object::js_object_set_field(handle_obj, 0, handle); + assert!( + !layout_typed_raw_f64_slot_for_user(handle_obj as usize, 0), + "JS handle tags must downgrade raw-f64 descriptors" + ); + assert_eq!(test_layout_pointer_slot_count(handle_obj as usize, 1), None); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_growing_new_field_falls_back() { + clear_marks(); + clear_mark_seeds(); + + let packed_keys = b"stable\0"; + let keys = crate::object::js_build_class_keys_array( + 65_001, + 1, + packed_keys.as_ptr(), + packed_keys.len() as u32, + ); + let obj = crate::object::js_object_alloc_class_inline_keys(65_001, 0, 1, keys); + js_gc_init_typed_shape_layout(obj as u64, 1, std::ptr::null(), 0, std::ptr::null(), 0); + + let extra_key = crate::string::js_string_from_bytes(b"extra".as_ptr(), 5); + crate::object::js_object_set_field_by_name(obj, extra_key, 42.0); + + unsafe { + assert_eq!((*obj).field_count, 2); + } + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), None); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_typed_shape_descriptor_transfers_on_object_move() { + clear_marks(); + clear_mark_seeds(); + + let src = crate::object::js_object_alloc(0, 2); + let dst = crate::object::js_object_alloc(0, 2); + let mask = [0b10u64]; + js_gc_init_typed_shape_layout( + src as u64, + 2, + std::ptr::null(), + 0, + mask.as_ptr(), + mask.len() as u32, + ); + + unsafe { + layout_transfer(src as *mut u8, dst as *mut u8); + } + + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(1)); + crate::object::js_object_set_field(dst, 1, crate::value::JSValue::number(9.0)); + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(1)); + + let child = crate::string::js_string_from_bytes(b"moved-child".as_ptr(), 11); + crate::object::js_object_set_field(dst, 0, crate::value::JSValue::string_ptr(child)); + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), None); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_all_pointer_layout_transfers_on_array_move() { + clear_marks(); + clear_mark_seeds(); + + let src = crate::array::js_array_alloc_pointer_elements(2); + let dst = crate::array::js_array_alloc(2); + unsafe { + layout_transfer(src as *mut u8, dst as *mut u8); + } + + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(2)); + + clear_marks(); + clear_mark_seeds(); +} diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/unboxed_object.rs b/crates/perry-runtime/src/gc/tests/layout_trace/unboxed_object.rs new file mode 100644 index 0000000000..5ae281d95c --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/layout_trace/unboxed_object.rs @@ -0,0 +1,412 @@ +use super::*; + +#[test] +fn test_unboxed_object_layout_scans_zero_raw_numeric_fields() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.25); + crate::object::js_object_set_unboxed_f64_field(obj, 1, -2.5); + js_gc_init_unboxed_object_layout(obj as u64, 2, 0b11, 0); + + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); + assert_eq!(test_heap_child_slot_count(obj as *mut u8), 0); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 0); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_layout_scan_trace_counts_raw_numeric_object_fields() { + clear_marks(); + clear_mark_seeds(); + + let trace = GcCycleTrace::new( + GcCollectionKind::Minor, + GcTriggerSnapshot { + kind: GcTriggerKind::Direct, + steps_before: Some(GcStepSnapshot::current()), + }, + ) + .expect("test requested GC trace capture"); + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.25); + crate::object::js_object_set_unboxed_f64_field(obj, 1, -2.5); + let raw_mask = [0b11u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + std::ptr::null(), + 0, + ); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + trace_marked_objects(&valid_ptrs); + + let event = trace.into_json(GcStepSnapshot::current()); + let layout_scans = &event["layout_scans"]; + assert_eq!( + layout_scans["raw_numeric_object_field_ranges_skipped"].as_u64(), + Some(1) + ); + assert_eq!( + layout_scans["raw_numeric_object_field_slots_skipped"].as_u64(), + Some(2) + ); + assert_eq!( + layout_scans["raw_numeric_object_field_payload_bytes_skipped"].as_u64(), + Some(16) + ); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_layout_scan_trace_counts_mixed_raw_numeric_object_fields() { + clear_marks(); + clear_mark_seeds(); + + let trace = GcCycleTrace::new( + GcCollectionKind::Minor, + GcTriggerSnapshot { + kind: GcTriggerKind::Direct, + steps_before: Some(GcStepSnapshot::current()), + }, + ) + .expect("test requested GC trace capture"); + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_unboxed_f64_field(obj, 0, f64::from_bits(0x1000)); + let child = crate::string::js_string_from_bytes(b"mixed-child".as_ptr(), 11); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::string_ptr(child)); + let raw_mask = [0b01u64]; + let pointer_mask = [0b10u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 2, + raw_mask.as_ptr(), + raw_mask.len() as u32, + pointer_mask.as_ptr(), + pointer_mask.len() as u32, + ); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + trace_marked_objects(&valid_ptrs); + + let event = trace.into_json(GcStepSnapshot::current()); + let layout_scans = &event["layout_scans"]; + assert_eq!(layout_scans["masked_pointer_slots_read"].as_u64(), Some(1)); + assert_eq!( + layout_scans["raw_numeric_object_field_ranges_skipped"].as_u64(), + Some(1) + ); + assert_eq!( + layout_scans["raw_numeric_object_field_slots_skipped"].as_u64(), + Some(1) + ); + assert_eq!( + layout_scans["raw_numeric_object_field_payload_bytes_skipped"].as_u64(), + Some(8) + ); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_unboxed_object_pointer_write_to_raw_slot_falls_back_and_traces() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.0); + crate::object::js_object_set_unboxed_f64_field(obj, 1, 2.0); + js_gc_init_unboxed_object_layout(obj as u64, 2, 0b11, 0); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); + + let child = crate::string::js_string_from_bytes(b"unboxed-child".as_ptr(), 13); + let child_header = unsafe { header_from_user_ptr(child as *mut u8) }; + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::string_ptr(child)); + + assert_eq!( + test_layout_pointer_slot_count(obj as usize, 2), + None, + "non-number writes to raw f64 slots must deopt to full scanning" + ); + + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + test_reset_trace_slot_reads(); + trace_marked_objects(&valid_ptrs); + assert_eq!(test_trace_slot_reads(), 2); + unsafe { + assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); + } + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_unboxed_object_descriptor_transfers_on_object_move() { + clear_marks(); + clear_mark_seeds(); + + let src = crate::object::js_object_alloc(0, 2); + let dst = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_unboxed_f64_field(src, 0, 3.0); + crate::object::js_object_set_unboxed_f64_field(src, 1, 4.0); + js_gc_init_unboxed_object_layout(src as u64, 2, 0b11, 0); + + unsafe { + layout_transfer(src as *mut u8, dst as *mut u8); + } + + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(0)); + crate::object::js_object_set_unboxed_f64_field(dst, 1, 5.0); + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), Some(0)); + + let child = crate::string::js_string_from_bytes(b"moved-child".as_ptr(), 11); + crate::object::js_object_set_field(dst, 1, crate::value::JSValue::string_ptr(child)); + assert_eq!(test_layout_pointer_slot_count(dst as usize, 2), None); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_raw_numeric_object_descriptor_transfers_on_copying_minor_and_skips_raw_slots() { + let _guard = CopyingNurseryTestGuard::new(1); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = young_leaf(); + let obj = crate::object::js_object_alloc(0, 3); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 10.5); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::from_bits(ptr_bits(child))); + crate::object::js_object_set_unboxed_f64_field(obj, 2, -3.25); + let raw_mask = [0b101u64]; + let pointer_mask = [0b010u64]; + js_gc_init_typed_shape_layout( + obj as u64, + 3, + raw_mask.as_ptr(), + raw_mask.len() as u32, + pointer_mask.as_ptr(), + pointer_mask.len() as u32, + ); + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 0)); + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 2)); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 3), Some(1)); + js_shadow_slot_set(0, ptr_bits(obj as usize)); + + let trace = collect_minor_trace(GcTriggerKind::Direct); + let after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; + let fields = unsafe { + (after as *const u8).add(std::mem::size_of::()) as *const u64 + }; + let first = f64::from_bits(unsafe { *fields.add(0) }); + let child_after = unsafe { (*fields.add(1) & POINTER_MASK) as usize }; + let third = f64::from_bits(unsafe { *fields.add(2) }); + + assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); + assert_ne!(after, obj as usize); + assert_ne!(child_after, child); + assert!(crate::arena::pointer_in_nursery(after)); + assert!(crate::arena::pointer_in_nursery(child_after)); + assert_eq!(first, 10.5); + assert_eq!(third, -3.25); + assert!(layout_typed_raw_f64_slot_for_user(after, 0)); + assert!(layout_typed_raw_f64_slot_for_user(after, 2)); + assert_eq!(test_layout_pointer_slot_count(after, 3), Some(1)); + assert_eq!(test_heap_child_slot_count(after as *mut u8), 1); + assert!( + trace.layout_scans.masked_pointer_slots_read >= 1, + "pointer slot should still be scanned: {:?}", + trace.layout_scans + ); + assert!( + trace.layout_scans.raw_numeric_object_field_slots_skipped >= 2, + "raw numeric object slots should be skipped: {:?}", + trace.layout_scans + ); +} + +fn unboxed_point_for_shape_change_test(shape_id: u32) -> *mut crate::object::ObjectHeader { + let packed_keys = b"x\0y\0"; + let obj = crate::object::js_object_alloc_with_shape( + shape_id, + 2, + packed_keys.as_ptr(), + packed_keys.len() as u32, + ); + crate::object::js_object_set_unboxed_f64_field(obj, 0, 1.0); + crate::object::js_object_set_unboxed_f64_field(obj, 1, 2.0); + js_gc_init_unboxed_object_layout(obj as u64, 2, 0b11, 0); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(0)); + obj +} + +fn descriptor_object_with_single_field( + shape_id: u32, + key: &[u8], + value: crate::value::JSValue, +) -> *mut crate::object::ObjectHeader { + let mut packed_key = Vec::with_capacity(key.len() + 1); + packed_key.extend_from_slice(key); + packed_key.push(0); + let desc = crate::object::js_object_alloc_with_shape( + shape_id, + 1, + packed_key.as_ptr(), + packed_key.len() as u32, + ); + crate::object::js_object_set_field(desc, 0, value); + desc +} + +#[test] +fn test_unboxed_object_dynamic_added_property_falls_back() { + clear_marks(); + clear_mark_seeds(); + + let obj = unboxed_point_for_shape_change_test(86_101); + let z_key = crate::string::js_string_from_bytes(b"z".as_ptr(), 1); + crate::object::js_object_set_field_by_name(obj, z_key, 3.0); + + assert_eq!( + test_layout_pointer_slot_count(obj as usize, 3), + None, + "adding a dynamic property must invalidate the exact unboxed shape" + ); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_unboxed_object_delete_falls_back() { + clear_marks(); + clear_mark_seeds(); + + let obj = unboxed_point_for_shape_change_test(86_102); + let x_key = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); + assert_eq!(crate::object::js_object_delete_field(obj, x_key), 1); + + assert_eq!( + test_layout_pointer_slot_count(obj as usize, 1), + None, + "delete shifts keys/fields and must invalidate the exact unboxed shape" + ); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_unboxed_object_define_property_falls_back() { + clear_marks(); + clear_mark_seeds(); + + let obj = unboxed_point_for_shape_change_test(86_103); + let x_key = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); + let desc = + descriptor_object_with_single_field(86_104, b"value", crate::value::JSValue::number(9.0)); + + crate::object::js_object_define_property( + crate::value::js_nanbox_pointer(obj as i64), + f64::from_bits(crate::value::JSValue::string_ptr(x_key).bits()), + crate::value::js_nanbox_pointer(desc as i64), + ); + + assert_eq!( + test_layout_pointer_slot_count(obj as usize, 2), + None, + "Object.defineProperty must invalidate the exact unboxed shape even for existing keys" + ); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_unboxed_object_accessor_define_property_falls_back() { + clear_marks(); + clear_mark_seeds(); + + let obj = unboxed_point_for_shape_change_test(86_105); + let x_key = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); + // #2817: an accessor descriptor's `get` must be callable — a non-function + // value now throws. Use a real (capture-less) closure as the getter so we + // still exercise the accessor shape-invalidation path under test. + let getter = crate::closure::js_closure_alloc(std::ptr::null(), 0); + let desc = descriptor_object_with_single_field( + 86_106, + b"get", + crate::value::JSValue::pointer(getter as *const u8), + ); + + crate::object::js_object_define_property( + crate::value::js_nanbox_pointer(obj as i64), + f64::from_bits(crate::value::JSValue::string_ptr(x_key).bits()), + crate::value::js_nanbox_pointer(desc as i64), + ); + + assert_eq!( + test_layout_pointer_slot_count(obj as usize, 2), + None, + "accessor descriptors must invalidate the exact unboxed shape" + ); + + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_heap_child_iterator_pointer_free_object_yields_no_child_slots() { + clear_marks(); + clear_mark_seeds(); + + let obj = crate::object::js_object_alloc(0, 3); + crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(1.0)); + crate::object::js_object_set_field(obj, 1, crate::value::JSValue::number(2.0)); + crate::object::js_object_set_field(obj, 2, crate::value::JSValue::bool(false)); + + assert_eq!(test_layout_pointer_slot_count(obj as usize, 3), Some(0)); + assert_eq!(test_heap_child_slot_count(obj as *mut u8), 0); + + let valid_ptrs = build_valid_pointer_set(); + let mut worklist = Vec::new(); + test_reset_trace_slot_reads(); + unsafe { + trace_object(obj as *mut u8, &valid_ptrs, &mut worklist); + } + assert_eq!(test_trace_slot_reads(), 0); + + clear_marks(); + clear_mark_seeds(); +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots.rs b/crates/perry-runtime/src/gc/tests/runtime_roots.rs index 893b9fcd26..14f67214eb 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots.rs @@ -2,8 +2,11 @@ use super::super::*; use super::support::*; use std::cell::Cell; mod callback_scanners; +mod hook_dispatch_handles; mod prototype_addr_cache; +mod side_table_scanners; mod string_slice; +mod transient_handles; fn assert_panics_with(expected: &str, f: impl FnOnce()) { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); @@ -375,1034 +378,6 @@ fn test_prototype_resolution_stack_scanner_is_registered() { ); } -#[test] -fn test_implicit_this_root_scanner_marks_and_rewrites() { - // Regression for #1813. The implicit-`this` cell holds the NaN-boxed - // receiver across a dynamically-dispatched non-arrow method body. Under a - // moving GC triggered from inside that body (the @perryts/mysql - // Pool.acquire → handshake → nativeScramble path under concurrent load) - // the receiver relocates. The scanner must (a) MARK it so it is not swept - // when the cell is its only root, and (b) REWRITE the cell to the moved - // copy so the body's next `this`-derived dispatch derefs live memory - // instead of the stale slot (the reported SIGSEGV in js_native_call_method). - clear_marks(); - clear_mark_seeds(); - let prev_this = crate::object::js_implicit_this_get(); - - let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let nursery_hdr = unsafe { header_from_user_ptr(nursery_user) as *mut GcHeader }; - - crate::object::js_implicit_this_set(f64::from_bits(ptr_bits(nursery_user as usize))); - - // Mark phase: the live receiver must be discovered as a root. - crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); - unsafe { - assert_ne!( - (*nursery_hdr).gc_flags & GC_FLAG_MARKED, - 0, - "IMPLICIT_THIS scanner must mark the receiver so GC does not sweep `this`" - ); - } - - // Rewrite phase: the cell must follow the forwarding pointer. - unsafe { - set_forwarding_address(nursery_hdr, old_user); - } - crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - ptr_bits(old_user as usize), - "IMPLICIT_THIS must be rewritten to the receiver's relocated copy (#1813)" - ); - - // Idle / undefined cell must be a no-op (the default state between calls). - crate::object::js_implicit_this_set(f64::from_bits(crate::value::TAG_UNDEFINED)); - crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - crate::value::TAG_UNDEFINED, - "scanning the idle implicit-`this` cell must leave TAG_UNDEFINED untouched" - ); - - crate::object::js_implicit_this_set(prev_this); - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_class_side_table_scanner_marks_values_but_not_function_keys() { - let _guard = GcTestIsolationGuard::new(); - clear_marks(); - clear_mark_seeds(); - crate::object::test_clear_class_side_table_roots(); - - let dynamic_value = young_leaf(); - let prototype_value = young_leaf(); - let cached_value = young_leaf(); - let prototype_object = crate::object::js_object_alloc(0, 0) as usize; - let parent_closure = crate::arena::arena_alloc_gc( - std::mem::size_of::(), - std::mem::align_of::(), - GC_TYPE_CLOSURE, - ) as usize; - let function_key = crate::arena::arena_alloc_gc( - std::mem::size_of::(), - std::mem::align_of::(), - GC_TYPE_CLOSURE, - ) as usize; - unsafe { - init_test_closure(parent_closure as *mut u8); - init_test_closure(function_key as *mut u8); - } - - crate::object::test_seed_class_dynamic_prop_root(0x5201, "dyn", string_bits(dynamic_value)); - crate::object::test_seed_class_prototype_method_root( - 0x5201, - "proto", - string_bits(prototype_value), - ); - crate::object::test_seed_class_prototype_method_value_root( - 0x5201, - "bound", - string_bits(cached_value), - ); - crate::object::test_seed_class_prototype_object_root(0x5201, prototype_object); - crate::object::test_seed_class_parent_closure_root(0x5201, parent_closure); - crate::object::test_seed_function_class_id_key(ptr_bits(function_key), 0x8200_5201); - - let valid_ptrs = build_valid_pointer_set(); - crate::object::scan_class_side_table_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); - - assert_marked_user_ptr(dynamic_value, "dynamic class property value"); - assert_marked_user_ptr(prototype_value, "prototype method value"); - assert_marked_user_ptr(cached_value, "cached bound prototype method value"); - assert_marked_user_ptr(prototype_object, "prototype-object side-table value"); - assert_marked_user_ptr(parent_closure, "parent-closure side-table value"); - assert_unmarked_user_ptr(function_key, "function-to-class metadata key"); - - crate::object::test_clear_class_side_table_roots(); - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_registered_class_side_table_scanner_rewrites_values_and_function_keys() { - let _guard = GcTestIsolationGuard::new(); - crate::object::test_clear_class_side_table_roots(); - gc_register_mutable_root_scanner(crate::object::scan_class_side_table_roots_mut); - - let value_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let key_user = crate::arena::arena_alloc_gc( - std::mem::size_of::(), - std::mem::align_of::(), - GC_TYPE_CLOSURE, - ); - unsafe { - init_test_closure(key_user); - } - let valid_ptrs = build_valid_pointer_set(); - let value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let key_old = crate::arena::arena_alloc_gc_old( - std::mem::size_of::(), - std::mem::align_of::(), - GC_TYPE_CLOSURE, - ); - unsafe { - init_test_closure(key_old); - set_forwarding_address(header_from_user_ptr(value_user) as *mut GcHeader, value_old); - set_forwarding_address(header_from_user_ptr(key_user) as *mut GcHeader, key_old); - } - - let value_bits = ptr_bits(value_user as usize); - let value_old_bits = ptr_bits(value_old as usize); - let key_bits = ptr_bits(key_user as usize); - let key_old_bits = ptr_bits(key_old as usize); - crate::object::test_seed_class_dynamic_prop_root(0x5202, "dyn", value_bits); - crate::object::test_seed_class_prototype_method_root(0x5202, "proto", value_bits); - crate::object::test_seed_class_prototype_method_value_root(0x5202, "bound", value_bits); - crate::object::test_seed_class_prototype_object_root(0x5202, value_user as usize); - crate::object::test_seed_class_parent_closure_root(0x5202, key_user as usize); - crate::object::test_seed_function_class_id_key(key_bits, 0x8200_5202); - - rewrite_mutable_registered_roots(&valid_ptrs); - - assert_eq!( - crate::object::test_class_dynamic_prop_root_bits(0x5202, "dyn"), - value_old_bits - ); - assert_eq!( - crate::object::test_class_prototype_method_root_bits(0x5202, "proto"), - value_old_bits - ); - assert_eq!( - crate::object::test_class_prototype_method_value_root_bits(0x5202, "bound"), - value_old_bits - ); - assert_eq!( - crate::object::test_class_prototype_object_root_addr(0x5202), - value_old as usize - ); - assert_eq!( - crate::object::test_class_parent_closure_root_addr(0x5202), - key_old as usize - ); - assert_eq!( - crate::object::function_class_id(f64::from_bits(key_old_bits)), - 0x8200_5202 - ); - assert_eq!( - crate::object::test_function_class_id_key_for_class(0x8200_5202), - key_old_bits - ); - assert_eq!( - crate::object::function_class_id(f64::from_bits(key_bits)), - 0 - ); - - crate::object::test_clear_class_side_table_roots(); -} - -#[test] -fn test_symbol_side_table_scanner_marks_keys_and_values_without_marking_owner() { - let _guard = GcTestIsolationGuard::new(); - clear_marks(); - clear_mark_seeds(); - crate::symbol::test_clear_symbol_side_table_roots(); - - let owner = crate::object::js_object_alloc(0, 0) as usize; - let sym_key = unsafe { alloc_nursery_test_symbol() }; - let value = young_leaf(); - let static_sym_key = unsafe { alloc_nursery_test_symbol() }; - let static_value = young_leaf(); - - crate::symbol::test_seed_symbol_property_root(owner, sym_key, string_bits(value)); - crate::symbol::test_seed_class_static_symbol_root( - 0x5301, - static_sym_key, - string_bits(static_value), - ); - - let valid_ptrs = build_valid_pointer_set(); - crate::symbol::scan_symbol_side_table_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); - - assert_unmarked_user_ptr(owner, "symbol side-table owner metadata key"); - assert_marked_user_ptr(sym_key, "symbol property key"); - assert_marked_user_ptr(value, "symbol property value"); - assert_marked_user_ptr(static_sym_key, "class static symbol key"); - assert_marked_user_ptr(static_value, "class static symbol value"); - - crate::symbol::test_clear_symbol_side_table_roots(); - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_symbol_side_table_registered_scanner_rewrites_roots_and_metadata() { - let _guard = GcTestIsolationGuard::new(); - crate::symbol::test_clear_symbol_side_table_roots(); - gc_register_mutable_root_scanner(crate::symbol::scan_symbol_side_table_roots_mut); - - let owner = crate::object::js_object_alloc(0, 0) as usize; - let sym_key = unsafe { alloc_nursery_test_symbol() }; - let value = young_leaf(); - let static_sym_key = unsafe { alloc_nursery_test_symbol() }; - let static_value = young_leaf(); - - let valid_ptrs = build_valid_pointer_set(); - let owner_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT) as usize; - let sym_key_old = unsafe { alloc_old_test_symbol() }; - let value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING) as usize; - let static_sym_key_old = unsafe { alloc_old_test_symbol() }; - let static_value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING) as usize; - unsafe { - set_forwarding_address( - header_from_user_ptr(owner as *const u8) as *mut GcHeader, - owner_old as *mut u8, - ); - set_forwarding_address( - header_from_user_ptr(sym_key as *const u8) as *mut GcHeader, - sym_key_old as *mut u8, - ); - set_forwarding_address( - header_from_user_ptr(value as *const u8) as *mut GcHeader, - value_old as *mut u8, - ); - set_forwarding_address( - header_from_user_ptr(static_sym_key as *const u8) as *mut GcHeader, - static_sym_key_old as *mut u8, - ); - set_forwarding_address( - header_from_user_ptr(static_value as *const u8) as *mut GcHeader, - static_value_old as *mut u8, - ); - } - - crate::symbol::test_seed_symbol_pointer_root(sym_key); - crate::symbol::test_seed_symbol_pointer_root(static_sym_key); - crate::symbol::test_seed_symbol_property_root(owner, sym_key, string_bits(value)); - crate::symbol::test_seed_class_static_symbol_root( - 0x5302, - static_sym_key, - string_bits(static_value), - ); - - rewrite_mutable_registered_roots(&valid_ptrs); - - assert!( - !crate::symbol::test_symbol_property_owner_exists(owner), - "symbol side table should remove the stale owner key" - ); - assert_eq!( - crate::symbol::test_symbol_property_root_bits(owner_old, sym_key_old), - Some(string_bits(value_old)) - ); - assert_eq!( - crate::symbol::test_class_static_symbol_root_bits(0x5302, static_sym_key_old), - Some(string_bits(static_value_old)) - ); - assert_eq!( - crate::symbol::test_class_static_symbol_root_bits(0x5302, static_sym_key), - None - ); - assert!(crate::symbol::test_symbol_pointer_root_contains( - sym_key_old - )); - assert!(crate::symbol::test_symbol_pointer_root_contains( - static_sym_key_old - )); - assert!(!crate::symbol::test_symbol_pointer_root_contains(sym_key)); - assert!(!crate::symbol::test_symbol_pointer_root_contains( - static_sym_key - )); - - crate::symbol::test_clear_symbol_side_table_roots(); -} - -#[test] -fn test_runtime_root_visitor_rewrites_raw_pointer_slots() { - let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - unsafe { - set_forwarding_address( - header_from_user_ptr(nursery_user) as *mut GcHeader, - old_user, - ); - } - - let mut mut_ptr = nursery_user; - let mut const_ptr = nursery_user as *const u8; - let mut usize_slot = nursery_user as usize; - let mut i64_slot = nursery_user as i64; - - let mut visitor = RuntimeRootVisitor::for_rewrite(&valid_ptrs); - visitor.visit_raw_mut_ptr_slot(&mut mut_ptr); - visitor.visit_raw_const_ptr_slot(&mut const_ptr); - visitor.visit_usize_slot(&mut usize_slot); - visitor.visit_i64_slot(&mut i64_slot); - - assert_eq!(mut_ptr, old_user); - assert_eq!(const_ptr, old_user as *const u8); - assert_eq!(usize_slot, old_user as usize); - assert_eq!(i64_slot, old_user as i64); -} - -/// Issue #1790: the class static-inheritance side-tables -/// (`CLASS_PROTOTYPE_OBJECTS`, `CLASS_PARENT_CLOSURES`) store the heap parent -/// as a raw `usize`. `scan_class_inheritance_roots_mut` must (a) MARK each -/// stored parent as a live root so it survives a collection that can't -/// otherwise reach it, and (b) REWRITE the stored address after the parent is -/// evacuated, so the static-inheritance walk (`Sub.ast` / inherited static -/// methods) resolves to the moved object rather than a freed/stale one. -#[test] -fn test_class_inheritance_side_table_roots_mark_and_rewrite() { - use crate::object::{ - scan_class_inheritance_roots_mut, test_class_parent_closure_root, - test_class_prototype_object_root, test_clear_class_inheritance_roots, - test_decl_class_prototype_root, test_seed_class_inheritance_roots, - test_seed_class_parent_closure_root, test_seed_decl_class_prototype_root, - }; - - const PROTO_CID: u32 = 0xDEAD_0001; - const CLOSURE_CID: u32 = 0xDEAD_0002; - - clear_marks(); - clear_mark_seeds(); - - // Allocate the side-table objects in the nursery before snapshotting the - // valid-pointer set so the mark phase recognizes them as roots. - let proto_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let decl_proto_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let closure_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - let proto_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let decl_proto_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let closure_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let proto_hdr = unsafe { header_from_user_ptr(proto_user) as *mut GcHeader }; - let decl_proto_hdr = unsafe { header_from_user_ptr(decl_proto_user) as *mut GcHeader }; - let closure_hdr = unsafe { header_from_user_ptr(closure_user) as *mut GcHeader }; - - test_seed_class_inheritance_roots(PROTO_CID, proto_user as usize); - test_seed_decl_class_prototype_root(PROTO_CID, decl_proto_user as usize); - test_seed_class_parent_closure_root(CLOSURE_CID, closure_user as usize); - - // Mark phase: all side-table parents become live roots. - scan_class_inheritance_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); - unsafe { - assert_ne!( - (*proto_hdr).gc_flags & GC_FLAG_MARKED, - 0, - "CLASS_PROTOTYPE_OBJECTS parent must be marked as a root" - ); - assert_ne!( - (*decl_proto_hdr).gc_flags & GC_FLAG_MARKED, - 0, - "CLASS_DECL_PROTOTYPE_OBJECTS prototype must be marked as a root" - ); - assert_ne!( - (*closure_hdr).gc_flags & GC_FLAG_MARKED, - 0, - "CLASS_PARENT_CLOSURES parent must be marked as a root" - ); - } - - // Simulate evacuation, then run the rewrite phase: the stored raw pointers - // follow the forwarding address. - unsafe { - set_forwarding_address(proto_hdr, proto_old); - set_forwarding_address(decl_proto_hdr, decl_proto_old); - set_forwarding_address(closure_hdr, closure_old); - } - scan_class_inheritance_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); - - assert_eq!( - test_class_prototype_object_root(PROTO_CID), - proto_old as usize, - "CLASS_PROTOTYPE_OBJECTS parent must be rewritten to the evacuated address" - ); - assert_eq!( - test_decl_class_prototype_root(PROTO_CID), - decl_proto_old as usize, - "CLASS_DECL_PROTOTYPE_OBJECTS prototype must be rewritten to the evacuated address" - ); - assert_eq!( - test_class_parent_closure_root(CLOSURE_CID), - closure_old as usize, - "CLASS_PARENT_CLOSURES parent must be rewritten to the evacuated address" - ); - - // A verify pass must not panic now that the slots point at the live - // (non-forwarded) evacuated objects. - scan_class_inheritance_roots_mut(&mut RuntimeRootVisitor::for_verify( - &valid_ptrs, - "class inheritance side-table roots (test)", - )); - - test_clear_class_inheritance_roots(PROTO_CID, CLOSURE_CID); - clear_marks(); - clear_mark_seeds(); -} - -#[test] -fn test_runtime_root_visitor_rewrites_cell_and_atomic_slots() { - let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - unsafe { - set_forwarding_address( - header_from_user_ptr(nursery_user) as *mut GcHeader, - old_user, - ); - } - - let cell = Cell::new(f64::from_bits( - POINTER_TAG | (nursery_user as u64 & POINTER_MASK), - )); - let atomic = std::sync::atomic::AtomicPtr::new(nursery_user); - let atomic_i64 = std::sync::atomic::AtomicI64::new(nursery_user as i64); - let atomic_nanbox_u64 = - std::sync::atomic::AtomicU64::new(POINTER_TAG | (nursery_user as u64 & POINTER_MASK)); - - let mut visitor = RuntimeRootVisitor::for_rewrite(&valid_ptrs); - visitor.visit_cell_f64_slot(&cell); - visitor.visit_atomic_nanbox_u64_slot( - &atomic_nanbox_u64, - std::sync::atomic::Ordering::Acquire, - std::sync::atomic::Ordering::Release, - ); - visitor.visit_atomic_raw_mut_ptr_slot( - &atomic, - std::sync::atomic::Ordering::Acquire, - std::sync::atomic::Ordering::Release, - ); - visitor.visit_atomic_i64_slot( - &atomic_i64, - std::sync::atomic::Ordering::Acquire, - std::sync::atomic::Ordering::Release, - ); - - assert_eq!( - cell.get().to_bits(), - POINTER_TAG | (old_user as u64 & POINTER_MASK) - ); - assert_eq!( - atomic_nanbox_u64.load(std::sync::atomic::Ordering::Acquire), - POINTER_TAG | (old_user as u64 & POINTER_MASK) - ); - assert_eq!(atomic.load(std::sync::atomic::Ordering::Acquire), old_user); - assert_eq!( - atomic_i64.load(std::sync::atomic::Ordering::Acquire), - old_user as i64 - ); -} - -#[test] -fn test_runtime_root_visitor_rewrites_metadata_without_marking() { - let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let nursery_hdr = unsafe { header_from_user_ptr(nursery_user) as *mut GcHeader }; - unsafe { - set_forwarding_address(nursery_hdr, old_user); - } - - let mut metadata = nursery_user as usize; - RuntimeRootVisitor::for_mark(&valid_ptrs).visit_metadata_usize_slot(&mut metadata); - unsafe { - assert_eq!( - (*nursery_hdr).gc_flags & GC_FLAG_MARKED, - 0, - "metadata-only slots must not become roots" - ); - } - - RuntimeRootVisitor::for_rewrite(&valid_ptrs).visit_metadata_usize_slot(&mut metadata); - assert_eq!(metadata, old_user as usize); -} - -#[test] -fn test_transient_runtime_handle_slots_mark_and_rewrite() { - clear_marks(); - clear_mark_seeds(); - - let nanbox_f64_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let nanbox_u64_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let raw_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let raw_string_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_STRING); - let heap_word_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - - let old_nanbox_f64 = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let old_nanbox_u64 = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let old_raw = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let old_raw_string = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING); - let old_heap_word = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - unsafe { - set_forwarding_address( - header_from_user_ptr(nanbox_f64_user) as *mut GcHeader, - old_nanbox_f64, - ); - set_forwarding_address( - header_from_user_ptr(nanbox_u64_user) as *mut GcHeader, - old_nanbox_u64, - ); - set_forwarding_address(header_from_user_ptr(raw_user) as *mut GcHeader, old_raw); - set_forwarding_address( - header_from_user_ptr(raw_string_user) as *mut GcHeader, - old_raw_string, - ); - set_forwarding_address( - header_from_user_ptr(heap_word_user) as *mut GcHeader, - old_heap_word, - ); - } - - let scope = RuntimeHandleScope::new(); - let nanbox_f64 = scope.root_nanbox_f64(f64::from_bits(ptr_bits(nanbox_f64_user as usize))); - let nanbox_u64 = scope.root_nanbox_u64(string_bits(nanbox_u64_user as usize)); - let raw = scope.root_raw_mut_ptr(raw_user); - let raw_string = scope.root_string_ptr(raw_string_user as *const crate::StringHeader); - let heap_word = scope.root_heap_word_u64(heap_word_user as u64); - - let mut marker = RuntimeRootVisitor::for_mark(&valid_ptrs); - scan_runtime_handle_roots_mut(&mut marker); - unsafe { - assert_ne!( - (*header_from_user_ptr(nanbox_f64_user)).gc_flags & GC_FLAG_MARKED, - 0 - ); - assert_ne!( - (*header_from_user_ptr(nanbox_u64_user)).gc_flags & GC_FLAG_MARKED, - 0 - ); - assert_ne!( - (*header_from_user_ptr(raw_user)).gc_flags & GC_FLAG_MARKED, - 0 - ); - assert_ne!( - (*header_from_user_ptr(raw_string_user)).gc_flags & GC_FLAG_MARKED, - 0 - ); - assert_ne!( - (*header_from_user_ptr(heap_word_user)).gc_flags & GC_FLAG_MARKED, - 0 - ); - } - - let mut rewriter = RuntimeRootVisitor::for_rewrite(&valid_ptrs); - scan_runtime_handle_roots_mut(&mut rewriter); - - assert_eq!( - nanbox_f64.get_nanbox_f64().to_bits(), - ptr_bits(old_nanbox_f64 as usize) - ); - assert_eq!( - nanbox_u64.get_nanbox_u64(), - string_bits(old_nanbox_u64 as usize) - ); - assert_eq!(raw.get_raw_mut_ptr::(), old_raw); - assert_eq!( - raw_string.get_raw_const_ptr::() as *mut u8, - old_raw_string - ); - assert_eq!(heap_word.get_heap_word_u64(), old_heap_word as u64); -} - -#[test] -fn test_transient_runtime_handle_scope_drop_removes_roots() { - clear_marks(); - clear_mark_seeds(); - - let user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let header = unsafe { header_from_user_ptr(user) as *mut GcHeader }; - let valid_ptrs = build_valid_pointer_set(); - - { - let scope = RuntimeHandleScope::new(); - let _handle = scope.root_nanbox_u64(ptr_bits(user as usize)); - assert!(RuntimeHandleScope::active_len_for_tests() > 0); - } - assert_eq!(RuntimeHandleScope::active_len_for_tests(), 0); - - let mut marker = RuntimeRootVisitor::for_mark(&valid_ptrs); - scan_runtime_handle_roots_mut(&mut marker); - unsafe { - assert_eq!( - (*header).gc_flags & GC_FLAG_MARKED, - 0, - "dropped handle scopes must not retain transient roots" - ); - } -} - -#[test] -fn test_set_gc_field_rewrite_reindexes_elements() { - clear_marks(); - clear_mark_seeds(); - crate::set::test_clear_set_roots(); - - let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let nursery_bits = ptr_bits(nursery_user as usize); - let set = crate::set::js_set_alloc(4); - crate::set::js_set_add(set, f64::from_bits(nursery_bits)); - - let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let old_bits = ptr_bits(old_user as usize); - unsafe { - set_forwarding_address( - header_from_user_ptr(nursery_user) as *mut GcHeader, - old_user, - ); - } - - let valid_ptrs = build_valid_pointer_set(); - unsafe { - rewrite_heap_object_fields(header_from_user_ptr(set as *const u8), &valid_ptrs); - } - - assert_eq!(crate::set::js_set_value_at(set, 0).to_bits(), old_bits); - assert_eq!(crate::set::js_set_has(set, f64::from_bits(old_bits)), 1); - assert_eq!( - crate::set::js_set_has(set, f64::from_bits(nursery_bits)), - 0, - "set lookup index should be rebuilt after element rewrites" - ); - - clear_marks(); - clear_mark_seeds(); - crate::set::test_clear_set_roots(); -} - -#[test] -fn test_transient_runtime_handle_string_concat_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let left_bytes = vec![b'a'; 600_000]; - let right_bytes = vec![b'b'; 600_000]; - let left = crate::string::js_string_from_bytes(left_bytes.as_ptr(), left_bytes.len() as u32); - let right = crate::string::js_string_from_bytes(right_bytes.as_ptr(), right_bytes.len() as u32); - - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = crate::string::js_string_concat(left, right); - - let result_scope = RuntimeHandleScope::new(); - let result_root = result_scope.root_string_ptr(result); - drain_scheduled_minor_gc(before, "concat allocation"); - let result = result_root.get_raw_const_ptr::(); - unsafe { - assert_eq!((*result).byte_len, 1_200_000); - let data = (result as *const u8).add(std::mem::size_of::()); - assert_eq!(*data, b'a'); - assert_eq!(*data.add(599_999), b'a'); - assert_eq!(*data.add(600_000), b'b'); - assert_eq!(*data.add(1_199_999), b'b'); - } -} - -#[test] -fn test_dynamic_string_add_roots_left_string_across_rhs_coercion_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let left = crate::string::js_string_from_bytes(b"dyn-left-".as_ptr(), 9); - let left_value = f64::from_bits(string_bits(left as usize)); - let left_scope = RuntimeHandleScope::new(); - let left_root = left_scope.root_string_ptr(left); - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = unsafe { - crate::value::js_dynamic_string_or_number_add( - left_value, - f64::from_bits(crate::value::TAG_UNDEFINED), - ) - }; - - let result_root = left_scope.root_nanbox_f64(result); - drain_scheduled_minor_gc(before, "rhs ToString allocation"); - unsafe { - assert_string_bytes( - left_root.get_raw_const_ptr::(), - b"dyn-left-", - ); - } - - let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); - assert!(result_value.is_string()); - unsafe { - assert_string_bytes(result_value.as_string_ptr(), b"dyn-left-undefined"); - } -} - -#[test] -fn test_dynamic_bigint_add_roots_both_bigint_across_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - // #2908: a BigInt operator now requires BOTH operands to be BigInt - // (`1n + 1` throws TypeError instead of coercing). This test exercises - // the same GC-rooting guarantee — the left BigInt must survive a minor - // GC scheduled by the right operand's allocation — but with a second - // BigInt operand so the dynamic add takes the (now only legal) BigInt - // path instead of the removed mixed-coercion path. - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let cases: [(&str, unsafe extern "C" fn(f64, f64) -> f64); 2] = [ - ("dynamic add", crate::value::js_dynamic_add), - ( - "dynamic string-or-number add", - crate::value::js_dynamic_string_or_number_add, - ), - ]; - - for (name, op) in cases { - let left = crate::bigint::js_bigint_from_i64(41); - let left_value = crate::value::js_nanbox_bigint(left as i64); - assert!(crate::arena::pointer_in_nursery(left as usize)); - let left_scope = RuntimeHandleScope::new(); - let left_root = left_scope.root_bigint_ptr(left as *const crate::bigint::BigIntHeader); - - let right = crate::bigint::js_bigint_from_i64(1); - let right_value = crate::value::js_nanbox_bigint(right as i64); - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = unsafe { op(left_value, right_value) }; - let result_root = left_scope.root_nanbox_f64(result); - drain_scheduled_minor_gc(before, &format!("{name} both BigInt across GC")); - - let rooted_left = left_root.get_raw_const_ptr::(); - assert_eq!(crate::bigint::js_bigint_to_f64(rooted_left), 41.0); - - let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); - assert!(result_value.is_bigint(), "{name} should return a BigInt"); - assert_eq!( - crate::bigint::js_bigint_to_f64(result_value.as_bigint_ptr()), - 42.0 - ); - } -} - -#[test] -fn test_bigint_method_add_roots_receiver_across_rhs_number_coercion_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let receiver = crate::bigint::js_bigint_from_i64(41); - let receiver_value = crate::value::js_nanbox_bigint(receiver as i64); - assert!(crate::arena::pointer_in_nursery(receiver as usize)); - let receiver_scope = RuntimeHandleScope::new(); - let receiver_root = - receiver_scope.root_bigint_ptr(receiver as *const crate::bigint::BigIntHeader); - let args = [1.0_f64]; - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = unsafe { - crate::object::js_native_call_method( - receiver_value, - b"add".as_ptr() as *const i8, - 3, - args.as_ptr(), - args.len(), - ) - }; - - let result_root = receiver_scope.root_nanbox_f64(result); - drain_scheduled_minor_gc(before, "BigInt method RHS number coercion"); - let rooted_receiver = receiver_root.get_raw_const_ptr::(); - assert_eq!(crate::bigint::js_bigint_to_f64(rooted_receiver), 41.0); - - let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); - assert!( - result_value.is_bigint(), - "BigInt add method should return BigInt" - ); - assert_eq!( - crate::bigint::js_bigint_to_f64(result_value.as_bigint_ptr()), - 42.0 - ); -} - -#[test] -fn test_string_method_split_roots_receiver_across_separator_materialization_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - activate_malloc_registry_for_tests(); - register_runtime_handle_root_scanner_for_tests(); - - let receiver = crate::string::js_string_from_bytes(b"a,b,c".as_ptr(), 5); - let receiver_value = f64::from_bits(string_bits(receiver as usize)); - assert!(crate::arena::pointer_in_nursery(receiver as usize)); - let receiver_scope = RuntimeHandleScope::new(); - let receiver_root = receiver_scope.root_string_ptr(receiver); - let sep = crate::value::JSValue::try_short_string(b",").unwrap(); - let args = [f64::from_bits(sep.bits())]; - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = unsafe { - crate::object::js_native_call_method( - receiver_value, - b"split".as_ptr() as *const i8, - 5, - args.as_ptr(), - args.len(), - ) - }; - - let result_root = receiver_scope.root_nanbox_f64(result); - drain_scheduled_minor_gc(before, "split separator materialization"); - let rooted_receiver = receiver_root.get_raw_const_ptr::(); - unsafe { - assert_string_bytes(rooted_receiver, b"a,b,c"); - let arr = (result_root.get_nanbox_u64() & POINTER_MASK) as *const crate::array::ArrayHeader; - assert_eq!(crate::array::js_array_length(arr), 3); - let expected: [&[u8]; 3] = [b"a", b"b", b"c"]; - for (i, expected) in expected.iter().enumerate() { - let value = crate::array::js_array_get(arr, i as u32); - assert!(value.is_string(), "split element {i} should be a string"); - assert_string_bytes(value.as_string_ptr(), expected); - } - } -} - -#[test] -fn test_string_method_replace_roots_receiver_across_pattern_materialization_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - activate_malloc_registry_for_tests(); - register_runtime_handle_root_scanner_for_tests(); - - let receiver = crate::string::js_string_from_bytes(b"a-a".as_ptr(), 3); - let receiver_value = f64::from_bits(string_bits(receiver as usize)); - assert!(crate::arena::pointer_in_nursery(receiver as usize)); - let receiver_scope = RuntimeHandleScope::new(); - let receiver_root = receiver_scope.root_string_ptr(receiver); - let pattern = crate::value::JSValue::try_short_string(b"-").unwrap(); - let replacement = crate::value::JSValue::try_short_string(b":").unwrap(); - let args = [ - f64::from_bits(pattern.bits()), - f64::from_bits(replacement.bits()), - ]; - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = unsafe { - crate::object::js_native_call_method( - receiver_value, - b"replace".as_ptr() as *const i8, - 7, - args.as_ptr(), - args.len(), - ) - }; - - let result_root = receiver_scope.root_nanbox_f64(result); - drain_scheduled_minor_gc(before, "replace pattern materialization"); - let rooted_receiver = receiver_root.get_raw_const_ptr::(); - unsafe { - assert_string_bytes(rooted_receiver, b"a-a"); - let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); - assert!(result_value.is_string(), "replace should return a string"); - assert_string_bytes(result_value.as_string_ptr(), b"a:a"); - } -} - -#[test] -fn test_transient_runtime_handle_array_push_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let arr = crate::array::js_array_alloc_with_length(200_000); - let value = crate::string::js_string_from_bytes(b"array-payload".as_ptr(), 13); - let value_bits = string_bits(value as usize); - - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let grown = crate::array::js_array_push_f64(arr, f64::from_bits(value_bits)); - - let grown_scope = RuntimeHandleScope::new(); - let grown_root = grown_scope.root_raw_mut_ptr(grown); - drain_scheduled_minor_gc(before, "array grow"); - let grown = grown_root.get_raw_const_ptr::(); - unsafe { - assert_eq!((*grown).length, 200_001); - let elements = - (grown as *const u8).add(std::mem::size_of::()) as *const u64; - let stored = *elements.add(200_000); - assert_eq!(stored & TAG_MASK, STRING_TAG); - let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; - assert_string_bytes(stored_ptr, b"array-payload"); - } -} - -#[test] -fn test_transient_runtime_handle_object_set_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _guard = CopyingNurseryTestGuard::new(1); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let obj = crate::object::js_object_alloc(0, 1); - js_shadow_slot_set(0, ptr_bits(obj as usize)); - let key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); - let value = crate::string::js_string_from_bytes(b"object-payload".as_ptr(), 14); - force_next_general_arena_alloc_slow(); - - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - crate::object::js_object_set_field_by_name( - obj, - key, - f64::from_bits(string_bits(value as usize)), - ); - - drain_scheduled_minor_gc(before, "keys-array allocation"); - let obj_after = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::object::ObjectHeader; - unsafe { - assert!(!(*obj_after).keys_array.is_null()); - let stored_value = crate::object::js_object_get_field(obj_after, 0).bits(); - assert_eq!(stored_value & TAG_MASK, STRING_TAG); - let stored_value_ptr = (stored_value & POINTER_MASK) as *const crate::StringHeader; - assert_string_bytes(stored_value_ptr, b"object-payload"); - - let key_value = crate::array::js_array_get((*obj_after).keys_array, 0).bits(); - assert_eq!(key_value & TAG_MASK, STRING_TAG); - let stored_key_ptr = (key_value & POINTER_MASK) as *const crate::StringHeader; - assert_string_bytes(stored_key_ptr, b"name"); - } -} - -#[test] -fn test_transient_runtime_handle_closure_captures_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - extern "C" fn captured_func(_closure: *const crate::closure::ClosureHeader) -> f64 { - 0.0 - } - - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - crate::closure::test_clear_singleton_closure_caches(); - - let captured = crate::string::js_string_from_bytes(b"closure-payload".as_ptr(), 15); - let captures = [string_bits(captured as usize)]; - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let closure = crate::closure::js_closure_alloc_with_captures_singleton( - captured_func as *const u8, - 1, - captures.as_ptr(), - ); - - let closure_scope = RuntimeHandleScope::new(); - let closure_root = closure_scope.root_raw_mut_ptr(closure); - drain_scheduled_minor_gc(before, "closure arena allocation"); - let closure = closure_root.get_raw_const_ptr::(); - unsafe { - let capture_slot = (closure as *const u8) - .add(std::mem::size_of::()) - as *const u64; - let stored = *capture_slot; - assert_eq!(stored & TAG_MASK, STRING_TAG); - let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; - assert_string_bytes(stored_ptr, b"closure-payload"); - } - - let entries = - crate::closure::test_captured_singleton_closure_cache_entries(captured_func as *const u8); - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].0.len(), 1); - assert_eq!(entries[0].0[0] & TAG_MASK, STRING_TAG); - let cached_capture = (entries[0].0[0] & POINTER_MASK) as *const crate::StringHeader; - unsafe { - assert_string_bytes(cached_capture, b"closure-payload"); - } - crate::closure::test_clear_singleton_closure_caches(); -} - extern "C" fn test_reviver_force_minor_gc( _closure: *const crate::closure::ClosureHeader, _key: f64, @@ -1627,409 +602,3 @@ fn drain_promise_microtasks_for_test() { } panic!("promise microtask drain did not quiesce"); } - -#[test] -fn test_async_hook_option_lookup_roots_callbacks_across_copied_minor_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let init = crate::closure::js_closure_alloc(test_no_capture_singleton_func as *const u8, 0); - let original = init as usize; - let options = hook_options(&[(b"init", init)]); - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let _handle = crate::async_hooks::js_async_hooks_create_hook(options); - drain_scheduled_minor_gc(before, "async hook option key lookup"); - - let (callback, _resource_bits) = crate::async_hooks::test_async_hooks_scanner_snapshot(); - assert_eq!(assert_callable_closure(ptr_bits(callback)), original); -} - -#[test] -fn test_closure_rest_dispatch_roots_args_during_rest_array_alloc_gc() { - let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); - let _guard = CopyingNurseryTestGuard::new(0); - let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - crate::closure::js_register_closure_rest(test_rest_first_value as *const u8, 0); - let closure = crate::closure::js_closure_alloc(test_rest_first_value as *const u8, 0); - let value = test_string_value(b"rest-dispatch"); - let args = [value]; - - force_next_general_arena_alloc_slow(); - trigger_guard.make_arena_trigger_due(); - let before = gc_collection_count(); - let result = unsafe { crate::closure::js_closure_call_array(closure as i64, args.as_ptr(), 1) }; - let result_scope = RuntimeHandleScope::new(); - let result_root = result_scope.root_nanbox_f64(result); - drain_scheduled_minor_gc(before, "rest-array creation"); - assert_string_value(result_root.get_nanbox_f64(), b"rest-dispatch"); -} - -#[test] -fn test_bound_timer_dispatch_roots_args_during_async_hook_init_gc() { - let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - gc_register_mutable_root_scanner(crate::async_hooks::scan_async_hooks_roots_mut); - gc_register_mutable_root_scanner(crate::timer::scan_timer_roots_mut); - - let init_hook = - crate::closure::js_closure_alloc(test_async_hook_init_force_minor_gc as *const u8, 0); - enable_async_hook(&[(b"init", init_hook)]); - - let timer_callback = crate::closure::js_closure_alloc(test_timer_capture_arg as *const u8, 0); - let timer_callback_original = timer_callback as usize; - let arg = test_string_value(b"bound-timer-arg"); - let arg_original = (arg.to_bits() & POINTER_MASK) as usize; - - let module_name = b"timers"; - let namespace = - crate::object::js_create_native_module_namespace(module_name.as_ptr(), module_name.len()); - let bound = crate::closure::js_closure_alloc(crate::closure::BOUND_METHOD_FUNC_PTR, 3); - crate::closure::js_closure_set_capture_f64(bound, 0, namespace); - let method = b"setTimeout"; - crate::closure::js_closure_set_capture_ptr(bound, 1, method.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(bound, 2, method.len() as i64); - - let timer_value = crate::closure::js_closure_call3( - bound, - f64::from_bits(ptr_bits(timer_callback as usize)), - 0.0, - arg, - ); - - let timer_id = (timer_value.to_bits() & POINTER_MASK) as i64; - let (callback, arg_bits) = crate::timer::test_callback_timer_snapshot(timer_id) - .expect("scheduled callback timer should remain queued"); - assert_moved_closure_ptr(ptr_bits(callback), timer_callback_original); - assert_moved_string_value(f64::from_bits(arg_bits), arg_original, b"bound-timer-arg"); - crate::timer::clearTimeout(timer_id); -} - -#[test] -fn test_timer_tick_roots_callback_args_and_previous_context_across_hooks() { - const ALS_HANDLE: i64 = -8_501; - - let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - gc_register_mutable_root_scanner(crate::async_hooks::scan_async_hooks_roots_mut); - gc_register_mutable_root_scanner(crate::timer::scan_timer_roots_mut); - - let before_hook = - crate::closure::js_closure_alloc(test_async_hook_event_force_minor_gc as *const u8, 0); - let after_hook = - crate::closure::js_closure_alloc(test_async_hook_event_force_minor_gc as *const u8, 0); - enable_async_hook(&[(b"before", before_hook), (b"after", after_hook)]); - - crate::async_context::clear_store(ALS_HANDLE); - let callback = crate::closure::js_closure_alloc(test_timer_capture_arg as *const u8, 0); - let arg = test_string_value(b"timer-tick-arg"); - let timer_args = [arg]; - let timer_id = unsafe { - crate::timer::js_set_timeout_callback_args(callback as i64, 0.0, timer_args.as_ptr(), 1) - }; - - let previous = test_string_value(b"timer-previous-context"); - let previous_original = (previous.to_bits() & POINTER_MASK) as usize; - crate::async_context::enter_with(ALS_HANDLE, previous); - TEST_TIMER_ARG_BITS.with(|slot| slot.set(0)); - TEST_TIMER_CALLED.with(|slot| slot.set(false)); - - let before = gc_collection_count(); - assert_eq!(crate::timer::js_callback_timer_tick(), 1); - drain_scheduled_minor_gc( - before, - "timer before/after hooks should trigger copied-minor GC", - ); - assert!(TEST_TIMER_CALLED.with(|slot| slot.get())); - - let restored = crate::async_context::get_store(ALS_HANDLE) - .expect("timer tick should restore previous AsyncLocalStorage context"); - assert_moved_string_value(restored, previous_original, b"timer-previous-context"); - crate::async_context::clear_store(ALS_HANDLE); - crate::timer::clearTimeout(timer_id); -} - -#[test] -fn test_next_tick_previous_context_survives_hook_gc() { - const ALS_HANDLE: i64 = -8_502; - - let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - gc_register_mutable_root_scanner(crate::async_hooks::scan_async_hooks_roots_mut); - gc_register_mutable_root_scanner(crate::builtins::scan_queued_microtask_roots_mut); - - let before_hook = - crate::closure::js_closure_alloc(test_async_hook_event_force_minor_gc as *const u8, 0); - enable_async_hook(&[(b"before", before_hook)]); - - crate::async_context::clear_store(ALS_HANDLE); - let callback = crate::closure::js_closure_alloc(test_no_capture_singleton_func as *const u8, 0); - crate::builtins::js_queue_next_tick(callback as i64); - - let previous = test_string_value(b"nexttick-previous-context"); - let previous_original = (previous.to_bits() & POINTER_MASK) as usize; - crate::async_context::enter_with(ALS_HANDLE, previous); - - let before = gc_collection_count(); - crate::builtins::js_drain_queued_microtasks(); - drain_scheduled_minor_gc( - before, - "nextTick before hook should trigger copied-minor GC", - ); - - let restored = crate::async_context::get_store(ALS_HANDLE) - .expect("nextTick should restore previous AsyncLocalStorage context"); - assert_moved_string_value(restored, previous_original, b"nexttick-previous-context"); - crate::async_context::clear_store(ALS_HANDLE); -} - -#[test] -fn test_array_map_runtime_handles_survive_callback_copied_minor_gc() { - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let input = test_string_value(b"array-map-payload"); - let input_ptr = (input.to_bits() & POINTER_MASK) as usize; - let source = test_array_from_values(&[input]); - let callback = - crate::closure::js_closure_alloc(test_array_identity_force_minor_gc as *const u8, 0); - - let before = gc_collection_count(); - let result = crate::array::js_array_map(source, callback); - drain_scheduled_minor_gc( - before, - "array.map callback should force copied-minor GC while result is runtime-held", - ); - assert_eq!(crate::array::js_array_length(result), 1); - - let stored = crate::array::js_array_get(result, 0).bits(); - assert_eq!(stored & TAG_MASK, STRING_TAG); - let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; - assert_ne!( - stored_ptr as usize, input_ptr, - "mapped heap value should be rewritten to its copied-minor address" - ); - unsafe { - assert_string_bytes(stored_ptr, b"array-map-payload"); - } -} - -#[test] -fn test_map_materializers_runtime_handles_survive_copied_minor_gc() { - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let key = test_string_value(b"map-key"); - let key_original = (key.to_bits() & POINTER_MASK) as usize; - let value = test_string_value(b"map-value"); - let value_original = (value.to_bits() & POINTER_MASK) as usize; - let map = crate::map::js_map_alloc(4); - crate::map::js_map_set(map, key, value); - let map_scope = RuntimeHandleScope::new(); - let map_handle = map_scope.root_raw_mut_ptr(map); - - let before = gc_collection_count(); - crate::map::test_force_next_map_helper_gc(); - let entries = crate::map::js_map_entries(map_handle.get_raw_const_ptr()); - drain_scheduled_minor_gc( - before, - "Map.entries should force copied-minor GC while helper handles are live", - ); - assert_eq!(crate::array::js_array_length(entries), 1); - let pair_bits = crate::array::js_array_get(entries, 0).bits(); - assert_eq!(pair_bits & TAG_MASK, POINTER_TAG); - let pair = (pair_bits & POINTER_MASK) as *mut crate::array::ArrayHeader; - assert_moved_string_value( - crate::array::js_array_get_f64(pair, 0), - key_original, - b"map-key", - ); - assert_moved_string_value( - crate::array::js_array_get_f64(pair, 1), - value_original, - b"map-value", - ); - - crate::map::test_force_next_map_helper_gc(); - let keys = crate::map::js_map_keys(map_handle.get_raw_const_ptr()); - assert_eq!(crate::array::js_array_length(keys), 1); - assert_moved_string_value( - crate::array::js_array_get_f64(keys, 0), - key_original, - b"map-key", - ); - - crate::map::test_force_next_map_helper_gc(); - let values = crate::map::js_map_values(map_handle.get_raw_const_ptr()); - assert_eq!(crate::array::js_array_length(values), 1); - assert_moved_string_value( - crate::array::js_array_get_f64(values, 0), - value_original, - b"map-value", - ); -} - -#[test] -fn test_map_from_array_runtime_handles_survive_copied_minor_gc() { - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let key = test_string_value(b"map-from-array-key"); - let key_original = (key.to_bits() & POINTER_MASK) as usize; - let value = test_string_value(b"map-from-array-value"); - let value_original = (value.to_bits() & POINTER_MASK) as usize; - let pair = test_pair_array(key, value); - let input = test_array_from_pair(pair); - - let before = gc_collection_count(); - crate::map::test_force_next_map_helper_gc(); - let map = crate::map::js_map_from_array(input); - drain_scheduled_minor_gc( - before, - "Map-from-array should force copied-minor GC while input and output handles are live", - ); - assert_eq!(crate::map::js_map_size(map), 1); - assert_moved_string_value( - crate::map::js_map_entry_key_at(map, 0), - key_original, - b"map-from-array-key", - ); - assert_moved_string_value( - crate::map::js_map_entry_value_at(map, 0), - value_original, - b"map-from-array-value", - ); -} - -#[test] -fn test_structured_clone_map_runtime_handles_survive_nested_copied_minor_gc() { - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let outer_key = test_string_value(b"clone-map-outer-key"); - let outer_key_original = (outer_key.to_bits() & POINTER_MASK) as usize; - let inner_key = test_string_value(b"clone-map-inner-key"); - let inner_key_original = (inner_key.to_bits() & POINTER_MASK) as usize; - let inner_value = test_string_value(b"clone-map-inner-value"); - let inner_value_original = (inner_value.to_bits() & POINTER_MASK) as usize; - - let inner_map = crate::map::js_map_alloc(4); - crate::map::js_map_set(inner_map, inner_key, inner_value); - let outer_map = crate::map::js_map_alloc(4); - crate::map::js_map_set( - outer_map, - outer_key, - f64::from_bits(ptr_bits(inner_map as usize)), - ); - - let before = gc_collection_count(); - crate::map::test_force_next_map_helper_gc(); - crate::map::test_force_next_map_helper_gc(); - let cloned = crate::builtins::js_structured_clone(f64::from_bits(ptr_bits(outer_map as usize))); - assert!( - gc_collection_count() >= before + 2, - "structuredClone(Map) should force copied-minor GC in outer and nested Map.entries" - ); - - let cloned_bits = cloned.to_bits(); - assert_eq!(cloned_bits & TAG_MASK, POINTER_TAG); - let cloned_map = (cloned_bits & POINTER_MASK) as *mut crate::map::MapHeader; - assert!(crate::map::is_registered_map(cloned_map as usize)); - assert_eq!(crate::map::js_map_size(cloned_map), 1); - assert_moved_string_value( - crate::map::js_map_entry_key_at(cloned_map, 0), - outer_key_original, - b"clone-map-outer-key", - ); - - let inner_clone_value = crate::map::js_map_entry_value_at(cloned_map, 0); - let inner_clone_bits = inner_clone_value.to_bits(); - assert_eq!(inner_clone_bits & TAG_MASK, POINTER_TAG); - let inner_clone = (inner_clone_bits & POINTER_MASK) as *mut crate::map::MapHeader; - assert!(crate::map::is_registered_map(inner_clone as usize)); - assert_ne!(inner_clone as usize, inner_map as usize); - assert_eq!(crate::map::js_map_size(inner_clone), 1); - assert_moved_string_value( - crate::map::js_map_entry_key_at(inner_clone, 0), - inner_key_original, - b"clone-map-inner-key", - ); - assert_moved_string_value( - crate::map::js_map_entry_value_at(inner_clone, 0), - inner_value_original, - b"clone-map-inner-value", - ); -} - -#[test] -fn test_set_materializers_runtime_handles_survive_copied_minor_gc() { - let _guard = CopyingNurseryTestGuard::new(0); - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - register_runtime_handle_root_scanner_for_tests(); - - let value = test_string_value(b"set-value"); - let value_original = (value.to_bits() & POINTER_MASK) as usize; - let set = crate::set::js_set_alloc(4); - crate::set::js_set_add(set, value); - - let before = gc_collection_count(); - crate::set::test_force_next_set_helper_gc(); - let arr = crate::set::js_set_to_array(set); - drain_scheduled_minor_gc( - before, - "Set-to-array should force copied-minor GC while helper handles are live", - ); - assert_eq!(crate::array::js_array_length(arr), 1); - assert_moved_string_value( - crate::array::js_array_get_f64(arr, 0), - value_original, - b"set-value", - ); - - let from_array_value = test_string_value(b"set-from-array"); - let from_array_original = (from_array_value.to_bits() & POINTER_MASK) as usize; - let input = test_array_from_values(&[from_array_value]); - crate::set::test_force_next_set_helper_gc(); - let from_array = crate::set::js_set_from_array(input); - assert_eq!(crate::set::js_set_size(from_array), 1); - assert_moved_string_value( - crate::set::js_set_value_at(from_array, 0), - from_array_original, - b"set-from-array", - ); - - let iterable = test_string_value(b"ab"); - crate::set::test_force_next_set_helper_gc(); - let from_iterable = crate::set::js_set_from_iterable(iterable); - assert_eq!(crate::set::js_set_size(from_iterable), 2); - unsafe { - let first = crate::set::js_set_value_at(from_iterable, 0); - let second = crate::set::js_set_value_at(from_iterable, 1); - assert_string_bytes( - (first.to_bits() & POINTER_MASK) as *const crate::StringHeader, - b"a", - ); - assert_string_bytes( - (second.to_bits() & POINTER_MASK) as *const crate::StringHeader, - b"b", - ); - } -} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs new file mode 100644 index 0000000000..e95cc709be --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs @@ -0,0 +1,407 @@ +use super::*; + +#[test] +fn test_async_hook_option_lookup_roots_callbacks_across_copied_minor_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let init = crate::closure::js_closure_alloc(test_no_capture_singleton_func as *const u8, 0); + let original = init as usize; + let options = hook_options(&[(b"init", init)]); + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let _handle = crate::async_hooks::js_async_hooks_create_hook(options); + drain_scheduled_minor_gc(before, "async hook option key lookup"); + + let (callback, _resource_bits) = crate::async_hooks::test_async_hooks_scanner_snapshot(); + assert_eq!(assert_callable_closure(ptr_bits(callback)), original); +} + +#[test] +fn test_closure_rest_dispatch_roots_args_during_rest_array_alloc_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + crate::closure::js_register_closure_rest(test_rest_first_value as *const u8, 0); + let closure = crate::closure::js_closure_alloc(test_rest_first_value as *const u8, 0); + let value = test_string_value(b"rest-dispatch"); + let args = [value]; + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = unsafe { crate::closure::js_closure_call_array(closure as i64, args.as_ptr(), 1) }; + let result_scope = RuntimeHandleScope::new(); + let result_root = result_scope.root_nanbox_f64(result); + drain_scheduled_minor_gc(before, "rest-array creation"); + assert_string_value(result_root.get_nanbox_f64(), b"rest-dispatch"); +} + +#[test] +fn test_bound_timer_dispatch_roots_args_during_async_hook_init_gc() { + let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + gc_register_mutable_root_scanner(crate::async_hooks::scan_async_hooks_roots_mut); + gc_register_mutable_root_scanner(crate::timer::scan_timer_roots_mut); + + let init_hook = + crate::closure::js_closure_alloc(test_async_hook_init_force_minor_gc as *const u8, 0); + enable_async_hook(&[(b"init", init_hook)]); + + let timer_callback = crate::closure::js_closure_alloc(test_timer_capture_arg as *const u8, 0); + let timer_callback_original = timer_callback as usize; + let arg = test_string_value(b"bound-timer-arg"); + let arg_original = (arg.to_bits() & POINTER_MASK) as usize; + + let module_name = b"timers"; + let namespace = + crate::object::js_create_native_module_namespace(module_name.as_ptr(), module_name.len()); + let bound = crate::closure::js_closure_alloc(crate::closure::BOUND_METHOD_FUNC_PTR, 3); + crate::closure::js_closure_set_capture_f64(bound, 0, namespace); + let method = b"setTimeout"; + crate::closure::js_closure_set_capture_ptr(bound, 1, method.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(bound, 2, method.len() as i64); + + let timer_value = crate::closure::js_closure_call3( + bound, + f64::from_bits(ptr_bits(timer_callback as usize)), + 0.0, + arg, + ); + + let timer_id = (timer_value.to_bits() & POINTER_MASK) as i64; + let (callback, arg_bits) = crate::timer::test_callback_timer_snapshot(timer_id) + .expect("scheduled callback timer should remain queued"); + assert_moved_closure_ptr(ptr_bits(callback), timer_callback_original); + assert_moved_string_value(f64::from_bits(arg_bits), arg_original, b"bound-timer-arg"); + crate::timer::clearTimeout(timer_id); +} + +#[test] +fn test_timer_tick_roots_callback_args_and_previous_context_across_hooks() { + const ALS_HANDLE: i64 = -8_501; + + let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + gc_register_mutable_root_scanner(crate::async_hooks::scan_async_hooks_roots_mut); + gc_register_mutable_root_scanner(crate::timer::scan_timer_roots_mut); + + let before_hook = + crate::closure::js_closure_alloc(test_async_hook_event_force_minor_gc as *const u8, 0); + let after_hook = + crate::closure::js_closure_alloc(test_async_hook_event_force_minor_gc as *const u8, 0); + enable_async_hook(&[(b"before", before_hook), (b"after", after_hook)]); + + crate::async_context::clear_store(ALS_HANDLE); + let callback = crate::closure::js_closure_alloc(test_timer_capture_arg as *const u8, 0); + let arg = test_string_value(b"timer-tick-arg"); + let timer_args = [arg]; + let timer_id = unsafe { + crate::timer::js_set_timeout_callback_args(callback as i64, 0.0, timer_args.as_ptr(), 1) + }; + + let previous = test_string_value(b"timer-previous-context"); + let previous_original = (previous.to_bits() & POINTER_MASK) as usize; + crate::async_context::enter_with(ALS_HANDLE, previous); + TEST_TIMER_ARG_BITS.with(|slot| slot.set(0)); + TEST_TIMER_CALLED.with(|slot| slot.set(false)); + + let before = gc_collection_count(); + assert_eq!(crate::timer::js_callback_timer_tick(), 1); + drain_scheduled_minor_gc( + before, + "timer before/after hooks should trigger copied-minor GC", + ); + assert!(TEST_TIMER_CALLED.with(|slot| slot.get())); + + let restored = crate::async_context::get_store(ALS_HANDLE) + .expect("timer tick should restore previous AsyncLocalStorage context"); + assert_moved_string_value(restored, previous_original, b"timer-previous-context"); + crate::async_context::clear_store(ALS_HANDLE); + crate::timer::clearTimeout(timer_id); +} + +#[test] +fn test_next_tick_previous_context_survives_hook_gc() { + const ALS_HANDLE: i64 = -8_502; + + let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + gc_register_mutable_root_scanner(crate::async_hooks::scan_async_hooks_roots_mut); + gc_register_mutable_root_scanner(crate::builtins::scan_queued_microtask_roots_mut); + + let before_hook = + crate::closure::js_closure_alloc(test_async_hook_event_force_minor_gc as *const u8, 0); + enable_async_hook(&[(b"before", before_hook)]); + + crate::async_context::clear_store(ALS_HANDLE); + let callback = crate::closure::js_closure_alloc(test_no_capture_singleton_func as *const u8, 0); + crate::builtins::js_queue_next_tick(callback as i64); + + let previous = test_string_value(b"nexttick-previous-context"); + let previous_original = (previous.to_bits() & POINTER_MASK) as usize; + crate::async_context::enter_with(ALS_HANDLE, previous); + + let before = gc_collection_count(); + crate::builtins::js_drain_queued_microtasks(); + drain_scheduled_minor_gc( + before, + "nextTick before hook should trigger copied-minor GC", + ); + + let restored = crate::async_context::get_store(ALS_HANDLE) + .expect("nextTick should restore previous AsyncLocalStorage context"); + assert_moved_string_value(restored, previous_original, b"nexttick-previous-context"); + crate::async_context::clear_store(ALS_HANDLE); +} + +#[test] +fn test_array_map_runtime_handles_survive_callback_copied_minor_gc() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let input = test_string_value(b"array-map-payload"); + let input_ptr = (input.to_bits() & POINTER_MASK) as usize; + let source = test_array_from_values(&[input]); + let callback = + crate::closure::js_closure_alloc(test_array_identity_force_minor_gc as *const u8, 0); + + let before = gc_collection_count(); + let result = crate::array::js_array_map(source, callback); + drain_scheduled_minor_gc( + before, + "array.map callback should force copied-minor GC while result is runtime-held", + ); + assert_eq!(crate::array::js_array_length(result), 1); + + let stored = crate::array::js_array_get(result, 0).bits(); + assert_eq!(stored & TAG_MASK, STRING_TAG); + let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; + assert_ne!( + stored_ptr as usize, input_ptr, + "mapped heap value should be rewritten to its copied-minor address" + ); + unsafe { + assert_string_bytes(stored_ptr, b"array-map-payload"); + } +} + +#[test] +fn test_map_materializers_runtime_handles_survive_copied_minor_gc() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let key = test_string_value(b"map-key"); + let key_original = (key.to_bits() & POINTER_MASK) as usize; + let value = test_string_value(b"map-value"); + let value_original = (value.to_bits() & POINTER_MASK) as usize; + let map = crate::map::js_map_alloc(4); + crate::map::js_map_set(map, key, value); + let map_scope = RuntimeHandleScope::new(); + let map_handle = map_scope.root_raw_mut_ptr(map); + + let before = gc_collection_count(); + crate::map::test_force_next_map_helper_gc(); + let entries = crate::map::js_map_entries(map_handle.get_raw_const_ptr()); + drain_scheduled_minor_gc( + before, + "Map.entries should force copied-minor GC while helper handles are live", + ); + assert_eq!(crate::array::js_array_length(entries), 1); + let pair_bits = crate::array::js_array_get(entries, 0).bits(); + assert_eq!(pair_bits & TAG_MASK, POINTER_TAG); + let pair = (pair_bits & POINTER_MASK) as *mut crate::array::ArrayHeader; + assert_moved_string_value( + crate::array::js_array_get_f64(pair, 0), + key_original, + b"map-key", + ); + assert_moved_string_value( + crate::array::js_array_get_f64(pair, 1), + value_original, + b"map-value", + ); + + crate::map::test_force_next_map_helper_gc(); + let keys = crate::map::js_map_keys(map_handle.get_raw_const_ptr()); + assert_eq!(crate::array::js_array_length(keys), 1); + assert_moved_string_value( + crate::array::js_array_get_f64(keys, 0), + key_original, + b"map-key", + ); + + crate::map::test_force_next_map_helper_gc(); + let values = crate::map::js_map_values(map_handle.get_raw_const_ptr()); + assert_eq!(crate::array::js_array_length(values), 1); + assert_moved_string_value( + crate::array::js_array_get_f64(values, 0), + value_original, + b"map-value", + ); +} + +#[test] +fn test_map_from_array_runtime_handles_survive_copied_minor_gc() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let key = test_string_value(b"map-from-array-key"); + let key_original = (key.to_bits() & POINTER_MASK) as usize; + let value = test_string_value(b"map-from-array-value"); + let value_original = (value.to_bits() & POINTER_MASK) as usize; + let pair = test_pair_array(key, value); + let input = test_array_from_pair(pair); + + let before = gc_collection_count(); + crate::map::test_force_next_map_helper_gc(); + let map = crate::map::js_map_from_array(input); + drain_scheduled_minor_gc( + before, + "Map-from-array should force copied-minor GC while input and output handles are live", + ); + assert_eq!(crate::map::js_map_size(map), 1); + assert_moved_string_value( + crate::map::js_map_entry_key_at(map, 0), + key_original, + b"map-from-array-key", + ); + assert_moved_string_value( + crate::map::js_map_entry_value_at(map, 0), + value_original, + b"map-from-array-value", + ); +} + +#[test] +fn test_structured_clone_map_runtime_handles_survive_nested_copied_minor_gc() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let outer_key = test_string_value(b"clone-map-outer-key"); + let outer_key_original = (outer_key.to_bits() & POINTER_MASK) as usize; + let inner_key = test_string_value(b"clone-map-inner-key"); + let inner_key_original = (inner_key.to_bits() & POINTER_MASK) as usize; + let inner_value = test_string_value(b"clone-map-inner-value"); + let inner_value_original = (inner_value.to_bits() & POINTER_MASK) as usize; + + let inner_map = crate::map::js_map_alloc(4); + crate::map::js_map_set(inner_map, inner_key, inner_value); + let outer_map = crate::map::js_map_alloc(4); + crate::map::js_map_set( + outer_map, + outer_key, + f64::from_bits(ptr_bits(inner_map as usize)), + ); + + let before = gc_collection_count(); + crate::map::test_force_next_map_helper_gc(); + crate::map::test_force_next_map_helper_gc(); + let cloned = crate::builtins::js_structured_clone(f64::from_bits(ptr_bits(outer_map as usize))); + assert!( + gc_collection_count() >= before + 2, + "structuredClone(Map) should force copied-minor GC in outer and nested Map.entries" + ); + + let cloned_bits = cloned.to_bits(); + assert_eq!(cloned_bits & TAG_MASK, POINTER_TAG); + let cloned_map = (cloned_bits & POINTER_MASK) as *mut crate::map::MapHeader; + assert!(crate::map::is_registered_map(cloned_map as usize)); + assert_eq!(crate::map::js_map_size(cloned_map), 1); + assert_moved_string_value( + crate::map::js_map_entry_key_at(cloned_map, 0), + outer_key_original, + b"clone-map-outer-key", + ); + + let inner_clone_value = crate::map::js_map_entry_value_at(cloned_map, 0); + let inner_clone_bits = inner_clone_value.to_bits(); + assert_eq!(inner_clone_bits & TAG_MASK, POINTER_TAG); + let inner_clone = (inner_clone_bits & POINTER_MASK) as *mut crate::map::MapHeader; + assert!(crate::map::is_registered_map(inner_clone as usize)); + assert_ne!(inner_clone as usize, inner_map as usize); + assert_eq!(crate::map::js_map_size(inner_clone), 1); + assert_moved_string_value( + crate::map::js_map_entry_key_at(inner_clone, 0), + inner_key_original, + b"clone-map-inner-key", + ); + assert_moved_string_value( + crate::map::js_map_entry_value_at(inner_clone, 0), + inner_value_original, + b"clone-map-inner-value", + ); +} + +#[test] +fn test_set_materializers_runtime_handles_survive_copied_minor_gc() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let value = test_string_value(b"set-value"); + let value_original = (value.to_bits() & POINTER_MASK) as usize; + let set = crate::set::js_set_alloc(4); + crate::set::js_set_add(set, value); + + let before = gc_collection_count(); + crate::set::test_force_next_set_helper_gc(); + let arr = crate::set::js_set_to_array(set); + drain_scheduled_minor_gc( + before, + "Set-to-array should force copied-minor GC while helper handles are live", + ); + assert_eq!(crate::array::js_array_length(arr), 1); + assert_moved_string_value( + crate::array::js_array_get_f64(arr, 0), + value_original, + b"set-value", + ); + + let from_array_value = test_string_value(b"set-from-array"); + let from_array_original = (from_array_value.to_bits() & POINTER_MASK) as usize; + let input = test_array_from_values(&[from_array_value]); + crate::set::test_force_next_set_helper_gc(); + let from_array = crate::set::js_set_from_array(input); + assert_eq!(crate::set::js_set_size(from_array), 1); + assert_moved_string_value( + crate::set::js_set_value_at(from_array, 0), + from_array_original, + b"set-from-array", + ); + + let iterable = test_string_value(b"ab"); + crate::set::test_force_next_set_helper_gc(); + let from_iterable = crate::set::js_set_from_iterable(iterable); + assert_eq!(crate::set::js_set_size(from_iterable), 2); + unsafe { + let first = crate::set::js_set_value_at(from_iterable, 0); + let second = crate::set::js_set_value_at(from_iterable, 1); + assert_string_bytes( + (first.to_bits() & POINTER_MASK) as *const crate::StringHeader, + b"a", + ); + assert_string_bytes( + (second.to_bits() & POINTER_MASK) as *const crate::StringHeader, + b"b", + ); + } +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs new file mode 100644 index 0000000000..90f3dc91cd --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs @@ -0,0 +1,508 @@ +use super::*; + +#[test] +fn test_implicit_this_root_scanner_marks_and_rewrites() { + // Regression for #1813. The implicit-`this` cell holds the NaN-boxed + // receiver across a dynamically-dispatched non-arrow method body. Under a + // moving GC triggered from inside that body (the @perryts/mysql + // Pool.acquire → handshake → nativeScramble path under concurrent load) + // the receiver relocates. The scanner must (a) MARK it so it is not swept + // when the cell is its only root, and (b) REWRITE the cell to the moved + // copy so the body's next `this`-derived dispatch derefs live memory + // instead of the stale slot (the reported SIGSEGV in js_native_call_method). + clear_marks(); + clear_mark_seeds(); + let prev_this = crate::object::js_implicit_this_get(); + + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let nursery_hdr = unsafe { header_from_user_ptr(nursery_user) as *mut GcHeader }; + + crate::object::js_implicit_this_set(f64::from_bits(ptr_bits(nursery_user as usize))); + + // Mark phase: the live receiver must be discovered as a root. + crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); + unsafe { + assert_ne!( + (*nursery_hdr).gc_flags & GC_FLAG_MARKED, + 0, + "IMPLICIT_THIS scanner must mark the receiver so GC does not sweep `this`" + ); + } + + // Rewrite phase: the cell must follow the forwarding pointer. + unsafe { + set_forwarding_address(nursery_hdr, old_user); + } + crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); + assert_eq!( + crate::object::js_implicit_this_get().to_bits(), + ptr_bits(old_user as usize), + "IMPLICIT_THIS must be rewritten to the receiver's relocated copy (#1813)" + ); + + // Idle / undefined cell must be a no-op (the default state between calls). + crate::object::js_implicit_this_set(f64::from_bits(crate::value::TAG_UNDEFINED)); + crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); + assert_eq!( + crate::object::js_implicit_this_get().to_bits(), + crate::value::TAG_UNDEFINED, + "scanning the idle implicit-`this` cell must leave TAG_UNDEFINED untouched" + ); + + crate::object::js_implicit_this_set(prev_this); + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_class_side_table_scanner_marks_values_but_not_function_keys() { + let _guard = GcTestIsolationGuard::new(); + clear_marks(); + clear_mark_seeds(); + crate::object::test_clear_class_side_table_roots(); + + let dynamic_value = young_leaf(); + let prototype_value = young_leaf(); + let cached_value = young_leaf(); + let prototype_object = crate::object::js_object_alloc(0, 0) as usize; + let parent_closure = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ) as usize; + let function_key = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ) as usize; + unsafe { + init_test_closure(parent_closure as *mut u8); + init_test_closure(function_key as *mut u8); + } + + crate::object::test_seed_class_dynamic_prop_root(0x5201, "dyn", string_bits(dynamic_value)); + crate::object::test_seed_class_prototype_method_root( + 0x5201, + "proto", + string_bits(prototype_value), + ); + crate::object::test_seed_class_prototype_method_value_root( + 0x5201, + "bound", + string_bits(cached_value), + ); + crate::object::test_seed_class_prototype_object_root(0x5201, prototype_object); + crate::object::test_seed_class_parent_closure_root(0x5201, parent_closure); + crate::object::test_seed_function_class_id_key(ptr_bits(function_key), 0x8200_5201); + + let valid_ptrs = build_valid_pointer_set(); + crate::object::scan_class_side_table_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); + + assert_marked_user_ptr(dynamic_value, "dynamic class property value"); + assert_marked_user_ptr(prototype_value, "prototype method value"); + assert_marked_user_ptr(cached_value, "cached bound prototype method value"); + assert_marked_user_ptr(prototype_object, "prototype-object side-table value"); + assert_marked_user_ptr(parent_closure, "parent-closure side-table value"); + assert_unmarked_user_ptr(function_key, "function-to-class metadata key"); + + crate::object::test_clear_class_side_table_roots(); + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_registered_class_side_table_scanner_rewrites_values_and_function_keys() { + let _guard = GcTestIsolationGuard::new(); + crate::object::test_clear_class_side_table_roots(); + gc_register_mutable_root_scanner(crate::object::scan_class_side_table_roots_mut); + + let value_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let key_user = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ); + unsafe { + init_test_closure(key_user); + } + let valid_ptrs = build_valid_pointer_set(); + let value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let key_old = crate::arena::arena_alloc_gc_old( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ); + unsafe { + init_test_closure(key_old); + set_forwarding_address(header_from_user_ptr(value_user) as *mut GcHeader, value_old); + set_forwarding_address(header_from_user_ptr(key_user) as *mut GcHeader, key_old); + } + + let value_bits = ptr_bits(value_user as usize); + let value_old_bits = ptr_bits(value_old as usize); + let key_bits = ptr_bits(key_user as usize); + let key_old_bits = ptr_bits(key_old as usize); + crate::object::test_seed_class_dynamic_prop_root(0x5202, "dyn", value_bits); + crate::object::test_seed_class_prototype_method_root(0x5202, "proto", value_bits); + crate::object::test_seed_class_prototype_method_value_root(0x5202, "bound", value_bits); + crate::object::test_seed_class_prototype_object_root(0x5202, value_user as usize); + crate::object::test_seed_class_parent_closure_root(0x5202, key_user as usize); + crate::object::test_seed_function_class_id_key(key_bits, 0x8200_5202); + + rewrite_mutable_registered_roots(&valid_ptrs); + + assert_eq!( + crate::object::test_class_dynamic_prop_root_bits(0x5202, "dyn"), + value_old_bits + ); + assert_eq!( + crate::object::test_class_prototype_method_root_bits(0x5202, "proto"), + value_old_bits + ); + assert_eq!( + crate::object::test_class_prototype_method_value_root_bits(0x5202, "bound"), + value_old_bits + ); + assert_eq!( + crate::object::test_class_prototype_object_root_addr(0x5202), + value_old as usize + ); + assert_eq!( + crate::object::test_class_parent_closure_root_addr(0x5202), + key_old as usize + ); + assert_eq!( + crate::object::function_class_id(f64::from_bits(key_old_bits)), + 0x8200_5202 + ); + assert_eq!( + crate::object::test_function_class_id_key_for_class(0x8200_5202), + key_old_bits + ); + assert_eq!( + crate::object::function_class_id(f64::from_bits(key_bits)), + 0 + ); + + crate::object::test_clear_class_side_table_roots(); +} + +#[test] +fn test_symbol_side_table_scanner_marks_keys_and_values_without_marking_owner() { + let _guard = GcTestIsolationGuard::new(); + clear_marks(); + clear_mark_seeds(); + crate::symbol::test_clear_symbol_side_table_roots(); + + let owner = crate::object::js_object_alloc(0, 0) as usize; + let sym_key = unsafe { alloc_nursery_test_symbol() }; + let value = young_leaf(); + let static_sym_key = unsafe { alloc_nursery_test_symbol() }; + let static_value = young_leaf(); + + crate::symbol::test_seed_symbol_property_root(owner, sym_key, string_bits(value)); + crate::symbol::test_seed_class_static_symbol_root( + 0x5301, + static_sym_key, + string_bits(static_value), + ); + + let valid_ptrs = build_valid_pointer_set(); + crate::symbol::scan_symbol_side_table_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); + + assert_unmarked_user_ptr(owner, "symbol side-table owner metadata key"); + assert_marked_user_ptr(sym_key, "symbol property key"); + assert_marked_user_ptr(value, "symbol property value"); + assert_marked_user_ptr(static_sym_key, "class static symbol key"); + assert_marked_user_ptr(static_value, "class static symbol value"); + + crate::symbol::test_clear_symbol_side_table_roots(); + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_symbol_side_table_registered_scanner_rewrites_roots_and_metadata() { + let _guard = GcTestIsolationGuard::new(); + crate::symbol::test_clear_symbol_side_table_roots(); + gc_register_mutable_root_scanner(crate::symbol::scan_symbol_side_table_roots_mut); + + let owner = crate::object::js_object_alloc(0, 0) as usize; + let sym_key = unsafe { alloc_nursery_test_symbol() }; + let value = young_leaf(); + let static_sym_key = unsafe { alloc_nursery_test_symbol() }; + let static_value = young_leaf(); + + let valid_ptrs = build_valid_pointer_set(); + let owner_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT) as usize; + let sym_key_old = unsafe { alloc_old_test_symbol() }; + let value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING) as usize; + let static_sym_key_old = unsafe { alloc_old_test_symbol() }; + let static_value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING) as usize; + unsafe { + set_forwarding_address( + header_from_user_ptr(owner as *const u8) as *mut GcHeader, + owner_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(sym_key as *const u8) as *mut GcHeader, + sym_key_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(value as *const u8) as *mut GcHeader, + value_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(static_sym_key as *const u8) as *mut GcHeader, + static_sym_key_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(static_value as *const u8) as *mut GcHeader, + static_value_old as *mut u8, + ); + } + + crate::symbol::test_seed_symbol_pointer_root(sym_key); + crate::symbol::test_seed_symbol_pointer_root(static_sym_key); + crate::symbol::test_seed_symbol_property_root(owner, sym_key, string_bits(value)); + crate::symbol::test_seed_class_static_symbol_root( + 0x5302, + static_sym_key, + string_bits(static_value), + ); + + rewrite_mutable_registered_roots(&valid_ptrs); + + assert!( + !crate::symbol::test_symbol_property_owner_exists(owner), + "symbol side table should remove the stale owner key" + ); + assert_eq!( + crate::symbol::test_symbol_property_root_bits(owner_old, sym_key_old), + Some(string_bits(value_old)) + ); + assert_eq!( + crate::symbol::test_class_static_symbol_root_bits(0x5302, static_sym_key_old), + Some(string_bits(static_value_old)) + ); + assert_eq!( + crate::symbol::test_class_static_symbol_root_bits(0x5302, static_sym_key), + None + ); + assert!(crate::symbol::test_symbol_pointer_root_contains( + sym_key_old + )); + assert!(crate::symbol::test_symbol_pointer_root_contains( + static_sym_key_old + )); + assert!(!crate::symbol::test_symbol_pointer_root_contains(sym_key)); + assert!(!crate::symbol::test_symbol_pointer_root_contains( + static_sym_key + )); + + crate::symbol::test_clear_symbol_side_table_roots(); +} + +#[test] +fn test_runtime_root_visitor_rewrites_raw_pointer_slots() { + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + unsafe { + set_forwarding_address( + header_from_user_ptr(nursery_user) as *mut GcHeader, + old_user, + ); + } + + let mut mut_ptr = nursery_user; + let mut const_ptr = nursery_user as *const u8; + let mut usize_slot = nursery_user as usize; + let mut i64_slot = nursery_user as i64; + + let mut visitor = RuntimeRootVisitor::for_rewrite(&valid_ptrs); + visitor.visit_raw_mut_ptr_slot(&mut mut_ptr); + visitor.visit_raw_const_ptr_slot(&mut const_ptr); + visitor.visit_usize_slot(&mut usize_slot); + visitor.visit_i64_slot(&mut i64_slot); + + assert_eq!(mut_ptr, old_user); + assert_eq!(const_ptr, old_user as *const u8); + assert_eq!(usize_slot, old_user as usize); + assert_eq!(i64_slot, old_user as i64); +} + +/// Issue #1790: the class static-inheritance side-tables +/// (`CLASS_PROTOTYPE_OBJECTS`, `CLASS_PARENT_CLOSURES`) store the heap parent +/// as a raw `usize`. `scan_class_inheritance_roots_mut` must (a) MARK each +/// stored parent as a live root so it survives a collection that can't +/// otherwise reach it, and (b) REWRITE the stored address after the parent is +/// evacuated, so the static-inheritance walk (`Sub.ast` / inherited static +/// methods) resolves to the moved object rather than a freed/stale one. +#[test] +fn test_class_inheritance_side_table_roots_mark_and_rewrite() { + use crate::object::{ + scan_class_inheritance_roots_mut, test_class_parent_closure_root, + test_class_prototype_object_root, test_clear_class_inheritance_roots, + test_decl_class_prototype_root, test_seed_class_inheritance_roots, + test_seed_class_parent_closure_root, test_seed_decl_class_prototype_root, + }; + + const PROTO_CID: u32 = 0xDEAD_0001; + const CLOSURE_CID: u32 = 0xDEAD_0002; + + clear_marks(); + clear_mark_seeds(); + + // Allocate the side-table objects in the nursery before snapshotting the + // valid-pointer set so the mark phase recognizes them as roots. + let proto_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let decl_proto_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let closure_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + let proto_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let decl_proto_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let closure_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let proto_hdr = unsafe { header_from_user_ptr(proto_user) as *mut GcHeader }; + let decl_proto_hdr = unsafe { header_from_user_ptr(decl_proto_user) as *mut GcHeader }; + let closure_hdr = unsafe { header_from_user_ptr(closure_user) as *mut GcHeader }; + + test_seed_class_inheritance_roots(PROTO_CID, proto_user as usize); + test_seed_decl_class_prototype_root(PROTO_CID, decl_proto_user as usize); + test_seed_class_parent_closure_root(CLOSURE_CID, closure_user as usize); + + // Mark phase: all side-table parents become live roots. + scan_class_inheritance_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); + unsafe { + assert_ne!( + (*proto_hdr).gc_flags & GC_FLAG_MARKED, + 0, + "CLASS_PROTOTYPE_OBJECTS parent must be marked as a root" + ); + assert_ne!( + (*decl_proto_hdr).gc_flags & GC_FLAG_MARKED, + 0, + "CLASS_DECL_PROTOTYPE_OBJECTS prototype must be marked as a root" + ); + assert_ne!( + (*closure_hdr).gc_flags & GC_FLAG_MARKED, + 0, + "CLASS_PARENT_CLOSURES parent must be marked as a root" + ); + } + + // Simulate evacuation, then run the rewrite phase: the stored raw pointers + // follow the forwarding address. + unsafe { + set_forwarding_address(proto_hdr, proto_old); + set_forwarding_address(decl_proto_hdr, decl_proto_old); + set_forwarding_address(closure_hdr, closure_old); + } + scan_class_inheritance_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); + + assert_eq!( + test_class_prototype_object_root(PROTO_CID), + proto_old as usize, + "CLASS_PROTOTYPE_OBJECTS parent must be rewritten to the evacuated address" + ); + assert_eq!( + test_decl_class_prototype_root(PROTO_CID), + decl_proto_old as usize, + "CLASS_DECL_PROTOTYPE_OBJECTS prototype must be rewritten to the evacuated address" + ); + assert_eq!( + test_class_parent_closure_root(CLOSURE_CID), + closure_old as usize, + "CLASS_PARENT_CLOSURES parent must be rewritten to the evacuated address" + ); + + // A verify pass must not panic now that the slots point at the live + // (non-forwarded) evacuated objects. + scan_class_inheritance_roots_mut(&mut RuntimeRootVisitor::for_verify( + &valid_ptrs, + "class inheritance side-table roots (test)", + )); + + test_clear_class_inheritance_roots(PROTO_CID, CLOSURE_CID); + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_runtime_root_visitor_rewrites_cell_and_atomic_slots() { + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + unsafe { + set_forwarding_address( + header_from_user_ptr(nursery_user) as *mut GcHeader, + old_user, + ); + } + + let cell = Cell::new(f64::from_bits( + POINTER_TAG | (nursery_user as u64 & POINTER_MASK), + )); + let atomic = std::sync::atomic::AtomicPtr::new(nursery_user); + let atomic_i64 = std::sync::atomic::AtomicI64::new(nursery_user as i64); + let atomic_nanbox_u64 = + std::sync::atomic::AtomicU64::new(POINTER_TAG | (nursery_user as u64 & POINTER_MASK)); + + let mut visitor = RuntimeRootVisitor::for_rewrite(&valid_ptrs); + visitor.visit_cell_f64_slot(&cell); + visitor.visit_atomic_nanbox_u64_slot( + &atomic_nanbox_u64, + std::sync::atomic::Ordering::Acquire, + std::sync::atomic::Ordering::Release, + ); + visitor.visit_atomic_raw_mut_ptr_slot( + &atomic, + std::sync::atomic::Ordering::Acquire, + std::sync::atomic::Ordering::Release, + ); + visitor.visit_atomic_i64_slot( + &atomic_i64, + std::sync::atomic::Ordering::Acquire, + std::sync::atomic::Ordering::Release, + ); + + assert_eq!( + cell.get().to_bits(), + POINTER_TAG | (old_user as u64 & POINTER_MASK) + ); + assert_eq!( + atomic_nanbox_u64.load(std::sync::atomic::Ordering::Acquire), + POINTER_TAG | (old_user as u64 & POINTER_MASK) + ); + assert_eq!(atomic.load(std::sync::atomic::Ordering::Acquire), old_user); + assert_eq!( + atomic_i64.load(std::sync::atomic::Ordering::Acquire), + old_user as i64 + ); +} + +#[test] +fn test_runtime_root_visitor_rewrites_metadata_without_marking() { + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let nursery_hdr = unsafe { header_from_user_ptr(nursery_user) as *mut GcHeader }; + unsafe { + set_forwarding_address(nursery_hdr, old_user); + } + + let mut metadata = nursery_user as usize; + RuntimeRootVisitor::for_mark(&valid_ptrs).visit_metadata_usize_slot(&mut metadata); + unsafe { + assert_eq!( + (*nursery_hdr).gc_flags & GC_FLAG_MARKED, + 0, + "metadata-only slots must not become roots" + ); + } + + RuntimeRootVisitor::for_rewrite(&valid_ptrs).visit_metadata_usize_slot(&mut metadata); + assert_eq!(metadata, old_user as usize); +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs new file mode 100644 index 0000000000..88aa9114b4 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs @@ -0,0 +1,522 @@ +use super::*; + +#[test] +fn test_transient_runtime_handle_slots_mark_and_rewrite() { + clear_marks(); + clear_mark_seeds(); + + let nanbox_f64_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let nanbox_u64_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let raw_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let raw_string_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_STRING); + let heap_word_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + + let old_nanbox_f64 = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let old_nanbox_u64 = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let old_raw = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let old_raw_string = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING); + let old_heap_word = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + unsafe { + set_forwarding_address( + header_from_user_ptr(nanbox_f64_user) as *mut GcHeader, + old_nanbox_f64, + ); + set_forwarding_address( + header_from_user_ptr(nanbox_u64_user) as *mut GcHeader, + old_nanbox_u64, + ); + set_forwarding_address(header_from_user_ptr(raw_user) as *mut GcHeader, old_raw); + set_forwarding_address( + header_from_user_ptr(raw_string_user) as *mut GcHeader, + old_raw_string, + ); + set_forwarding_address( + header_from_user_ptr(heap_word_user) as *mut GcHeader, + old_heap_word, + ); + } + + let scope = RuntimeHandleScope::new(); + let nanbox_f64 = scope.root_nanbox_f64(f64::from_bits(ptr_bits(nanbox_f64_user as usize))); + let nanbox_u64 = scope.root_nanbox_u64(string_bits(nanbox_u64_user as usize)); + let raw = scope.root_raw_mut_ptr(raw_user); + let raw_string = scope.root_string_ptr(raw_string_user as *const crate::StringHeader); + let heap_word = scope.root_heap_word_u64(heap_word_user as u64); + + let mut marker = RuntimeRootVisitor::for_mark(&valid_ptrs); + scan_runtime_handle_roots_mut(&mut marker); + unsafe { + assert_ne!( + (*header_from_user_ptr(nanbox_f64_user)).gc_flags & GC_FLAG_MARKED, + 0 + ); + assert_ne!( + (*header_from_user_ptr(nanbox_u64_user)).gc_flags & GC_FLAG_MARKED, + 0 + ); + assert_ne!( + (*header_from_user_ptr(raw_user)).gc_flags & GC_FLAG_MARKED, + 0 + ); + assert_ne!( + (*header_from_user_ptr(raw_string_user)).gc_flags & GC_FLAG_MARKED, + 0 + ); + assert_ne!( + (*header_from_user_ptr(heap_word_user)).gc_flags & GC_FLAG_MARKED, + 0 + ); + } + + let mut rewriter = RuntimeRootVisitor::for_rewrite(&valid_ptrs); + scan_runtime_handle_roots_mut(&mut rewriter); + + assert_eq!( + nanbox_f64.get_nanbox_f64().to_bits(), + ptr_bits(old_nanbox_f64 as usize) + ); + assert_eq!( + nanbox_u64.get_nanbox_u64(), + string_bits(old_nanbox_u64 as usize) + ); + assert_eq!(raw.get_raw_mut_ptr::(), old_raw); + assert_eq!( + raw_string.get_raw_const_ptr::() as *mut u8, + old_raw_string + ); + assert_eq!(heap_word.get_heap_word_u64(), old_heap_word as u64); +} + +#[test] +fn test_transient_runtime_handle_scope_drop_removes_roots() { + clear_marks(); + clear_mark_seeds(); + + let user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let header = unsafe { header_from_user_ptr(user) as *mut GcHeader }; + let valid_ptrs = build_valid_pointer_set(); + + { + let scope = RuntimeHandleScope::new(); + let _handle = scope.root_nanbox_u64(ptr_bits(user as usize)); + assert!(RuntimeHandleScope::active_len_for_tests() > 0); + } + assert_eq!(RuntimeHandleScope::active_len_for_tests(), 0); + + let mut marker = RuntimeRootVisitor::for_mark(&valid_ptrs); + scan_runtime_handle_roots_mut(&mut marker); + unsafe { + assert_eq!( + (*header).gc_flags & GC_FLAG_MARKED, + 0, + "dropped handle scopes must not retain transient roots" + ); + } +} + +#[test] +fn test_set_gc_field_rewrite_reindexes_elements() { + clear_marks(); + clear_mark_seeds(); + crate::set::test_clear_set_roots(); + + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let nursery_bits = ptr_bits(nursery_user as usize); + let set = crate::set::js_set_alloc(4); + crate::set::js_set_add(set, f64::from_bits(nursery_bits)); + + let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let old_bits = ptr_bits(old_user as usize); + unsafe { + set_forwarding_address( + header_from_user_ptr(nursery_user) as *mut GcHeader, + old_user, + ); + } + + let valid_ptrs = build_valid_pointer_set(); + unsafe { + rewrite_heap_object_fields(header_from_user_ptr(set as *const u8), &valid_ptrs); + } + + assert_eq!(crate::set::js_set_value_at(set, 0).to_bits(), old_bits); + assert_eq!(crate::set::js_set_has(set, f64::from_bits(old_bits)), 1); + assert_eq!( + crate::set::js_set_has(set, f64::from_bits(nursery_bits)), + 0, + "set lookup index should be rebuilt after element rewrites" + ); + + clear_marks(); + clear_mark_seeds(); + crate::set::test_clear_set_roots(); +} + +#[test] +fn test_transient_runtime_handle_string_concat_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let left_bytes = vec![b'a'; 600_000]; + let right_bytes = vec![b'b'; 600_000]; + let left = crate::string::js_string_from_bytes(left_bytes.as_ptr(), left_bytes.len() as u32); + let right = crate::string::js_string_from_bytes(right_bytes.as_ptr(), right_bytes.len() as u32); + + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = crate::string::js_string_concat(left, right); + + let result_scope = RuntimeHandleScope::new(); + let result_root = result_scope.root_string_ptr(result); + drain_scheduled_minor_gc(before, "concat allocation"); + let result = result_root.get_raw_const_ptr::(); + unsafe { + assert_eq!((*result).byte_len, 1_200_000); + let data = (result as *const u8).add(std::mem::size_of::()); + assert_eq!(*data, b'a'); + assert_eq!(*data.add(599_999), b'a'); + assert_eq!(*data.add(600_000), b'b'); + assert_eq!(*data.add(1_199_999), b'b'); + } +} + +#[test] +fn test_dynamic_string_add_roots_left_string_across_rhs_coercion_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let left = crate::string::js_string_from_bytes(b"dyn-left-".as_ptr(), 9); + let left_value = f64::from_bits(string_bits(left as usize)); + let left_scope = RuntimeHandleScope::new(); + let left_root = left_scope.root_string_ptr(left); + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = unsafe { + crate::value::js_dynamic_string_or_number_add( + left_value, + f64::from_bits(crate::value::TAG_UNDEFINED), + ) + }; + + let result_root = left_scope.root_nanbox_f64(result); + drain_scheduled_minor_gc(before, "rhs ToString allocation"); + unsafe { + assert_string_bytes( + left_root.get_raw_const_ptr::(), + b"dyn-left-", + ); + } + + let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); + assert!(result_value.is_string()); + unsafe { + assert_string_bytes(result_value.as_string_ptr(), b"dyn-left-undefined"); + } +} + +#[test] +fn test_dynamic_bigint_add_roots_both_bigint_across_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + // #2908: a BigInt operator now requires BOTH operands to be BigInt + // (`1n + 1` throws TypeError instead of coercing). This test exercises + // the same GC-rooting guarantee — the left BigInt must survive a minor + // GC scheduled by the right operand's allocation — but with a second + // BigInt operand so the dynamic add takes the (now only legal) BigInt + // path instead of the removed mixed-coercion path. + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let cases: [(&str, unsafe extern "C" fn(f64, f64) -> f64); 2] = [ + ("dynamic add", crate::value::js_dynamic_add), + ( + "dynamic string-or-number add", + crate::value::js_dynamic_string_or_number_add, + ), + ]; + + for (name, op) in cases { + let left = crate::bigint::js_bigint_from_i64(41); + let left_value = crate::value::js_nanbox_bigint(left as i64); + assert!(crate::arena::pointer_in_nursery(left as usize)); + let left_scope = RuntimeHandleScope::new(); + let left_root = left_scope.root_bigint_ptr(left as *const crate::bigint::BigIntHeader); + + let right = crate::bigint::js_bigint_from_i64(1); + let right_value = crate::value::js_nanbox_bigint(right as i64); + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = unsafe { op(left_value, right_value) }; + let result_root = left_scope.root_nanbox_f64(result); + drain_scheduled_minor_gc(before, &format!("{name} both BigInt across GC")); + + let rooted_left = left_root.get_raw_const_ptr::(); + assert_eq!(crate::bigint::js_bigint_to_f64(rooted_left), 41.0); + + let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); + assert!(result_value.is_bigint(), "{name} should return a BigInt"); + assert_eq!( + crate::bigint::js_bigint_to_f64(result_value.as_bigint_ptr()), + 42.0 + ); + } +} + +#[test] +fn test_bigint_method_add_roots_receiver_across_rhs_number_coercion_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let receiver = crate::bigint::js_bigint_from_i64(41); + let receiver_value = crate::value::js_nanbox_bigint(receiver as i64); + assert!(crate::arena::pointer_in_nursery(receiver as usize)); + let receiver_scope = RuntimeHandleScope::new(); + let receiver_root = + receiver_scope.root_bigint_ptr(receiver as *const crate::bigint::BigIntHeader); + let args = [1.0_f64]; + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = unsafe { + crate::object::js_native_call_method( + receiver_value, + b"add".as_ptr() as *const i8, + 3, + args.as_ptr(), + args.len(), + ) + }; + + let result_root = receiver_scope.root_nanbox_f64(result); + drain_scheduled_minor_gc(before, "BigInt method RHS number coercion"); + let rooted_receiver = receiver_root.get_raw_const_ptr::(); + assert_eq!(crate::bigint::js_bigint_to_f64(rooted_receiver), 41.0); + + let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); + assert!( + result_value.is_bigint(), + "BigInt add method should return BigInt" + ); + assert_eq!( + crate::bigint::js_bigint_to_f64(result_value.as_bigint_ptr()), + 42.0 + ); +} + +#[test] +fn test_string_method_split_roots_receiver_across_separator_materialization_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + activate_malloc_registry_for_tests(); + register_runtime_handle_root_scanner_for_tests(); + + let receiver = crate::string::js_string_from_bytes(b"a,b,c".as_ptr(), 5); + let receiver_value = f64::from_bits(string_bits(receiver as usize)); + assert!(crate::arena::pointer_in_nursery(receiver as usize)); + let receiver_scope = RuntimeHandleScope::new(); + let receiver_root = receiver_scope.root_string_ptr(receiver); + let sep = crate::value::JSValue::try_short_string(b",").unwrap(); + let args = [f64::from_bits(sep.bits())]; + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = unsafe { + crate::object::js_native_call_method( + receiver_value, + b"split".as_ptr() as *const i8, + 5, + args.as_ptr(), + args.len(), + ) + }; + + let result_root = receiver_scope.root_nanbox_f64(result); + drain_scheduled_minor_gc(before, "split separator materialization"); + let rooted_receiver = receiver_root.get_raw_const_ptr::(); + unsafe { + assert_string_bytes(rooted_receiver, b"a,b,c"); + let arr = (result_root.get_nanbox_u64() & POINTER_MASK) as *const crate::array::ArrayHeader; + assert_eq!(crate::array::js_array_length(arr), 3); + let expected: [&[u8]; 3] = [b"a", b"b", b"c"]; + for (i, expected) in expected.iter().enumerate() { + let value = crate::array::js_array_get(arr, i as u32); + assert!(value.is_string(), "split element {i} should be a string"); + assert_string_bytes(value.as_string_ptr(), expected); + } + } +} + +#[test] +fn test_string_method_replace_roots_receiver_across_pattern_materialization_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + activate_malloc_registry_for_tests(); + register_runtime_handle_root_scanner_for_tests(); + + let receiver = crate::string::js_string_from_bytes(b"a-a".as_ptr(), 3); + let receiver_value = f64::from_bits(string_bits(receiver as usize)); + assert!(crate::arena::pointer_in_nursery(receiver as usize)); + let receiver_scope = RuntimeHandleScope::new(); + let receiver_root = receiver_scope.root_string_ptr(receiver); + let pattern = crate::value::JSValue::try_short_string(b"-").unwrap(); + let replacement = crate::value::JSValue::try_short_string(b":").unwrap(); + let args = [ + f64::from_bits(pattern.bits()), + f64::from_bits(replacement.bits()), + ]; + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let result = unsafe { + crate::object::js_native_call_method( + receiver_value, + b"replace".as_ptr() as *const i8, + 7, + args.as_ptr(), + args.len(), + ) + }; + + let result_root = receiver_scope.root_nanbox_f64(result); + drain_scheduled_minor_gc(before, "replace pattern materialization"); + let rooted_receiver = receiver_root.get_raw_const_ptr::(); + unsafe { + assert_string_bytes(rooted_receiver, b"a-a"); + let result_value = crate::value::JSValue::from_bits(result_root.get_nanbox_u64()); + assert!(result_value.is_string(), "replace should return a string"); + assert_string_bytes(result_value.as_string_ptr(), b"a:a"); + } +} + +#[test] +fn test_transient_runtime_handle_array_push_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let arr = crate::array::js_array_alloc_with_length(200_000); + let value = crate::string::js_string_from_bytes(b"array-payload".as_ptr(), 13); + let value_bits = string_bits(value as usize); + + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let grown = crate::array::js_array_push_f64(arr, f64::from_bits(value_bits)); + + let grown_scope = RuntimeHandleScope::new(); + let grown_root = grown_scope.root_raw_mut_ptr(grown); + drain_scheduled_minor_gc(before, "array grow"); + let grown = grown_root.get_raw_const_ptr::(); + unsafe { + assert_eq!((*grown).length, 200_001); + let elements = + (grown as *const u8).add(std::mem::size_of::()) as *const u64; + let stored = *elements.add(200_000); + assert_eq!(stored & TAG_MASK, STRING_TAG); + let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; + assert_string_bytes(stored_ptr, b"array-payload"); + } +} + +#[test] +fn test_transient_runtime_handle_object_set_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(1); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + let obj = crate::object::js_object_alloc(0, 1); + js_shadow_slot_set(0, ptr_bits(obj as usize)); + let key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); + let value = crate::string::js_string_from_bytes(b"object-payload".as_ptr(), 14); + force_next_general_arena_alloc_slow(); + + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + crate::object::js_object_set_field_by_name( + obj, + key, + f64::from_bits(string_bits(value as usize)), + ); + + drain_scheduled_minor_gc(before, "keys-array allocation"); + let obj_after = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::object::ObjectHeader; + unsafe { + assert!(!(*obj_after).keys_array.is_null()); + let stored_value = crate::object::js_object_get_field(obj_after, 0).bits(); + assert_eq!(stored_value & TAG_MASK, STRING_TAG); + let stored_value_ptr = (stored_value & POINTER_MASK) as *const crate::StringHeader; + assert_string_bytes(stored_value_ptr, b"object-payload"); + + let key_value = crate::array::js_array_get((*obj_after).keys_array, 0).bits(); + assert_eq!(key_value & TAG_MASK, STRING_TAG); + let stored_key_ptr = (key_value & POINTER_MASK) as *const crate::StringHeader; + assert_string_bytes(stored_key_ptr, b"name"); + } +} + +#[test] +fn test_transient_runtime_handle_closure_captures_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + extern "C" fn captured_func(_closure: *const crate::closure::ClosureHeader) -> f64 { + 0.0 + } + + let _guard = CopyingNurseryTestGuard::new(0); + let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + crate::closure::test_clear_singleton_closure_caches(); + + let captured = crate::string::js_string_from_bytes(b"closure-payload".as_ptr(), 15); + let captures = [string_bits(captured as usize)]; + + force_next_general_arena_alloc_slow(); + trigger_guard.make_arena_trigger_due(); + let before = gc_collection_count(); + let closure = crate::closure::js_closure_alloc_with_captures_singleton( + captured_func as *const u8, + 1, + captures.as_ptr(), + ); + + let closure_scope = RuntimeHandleScope::new(); + let closure_root = closure_scope.root_raw_mut_ptr(closure); + drain_scheduled_minor_gc(before, "closure arena allocation"); + let closure = closure_root.get_raw_const_ptr::(); + unsafe { + let capture_slot = (closure as *const u8) + .add(std::mem::size_of::()) + as *const u64; + let stored = *capture_slot; + assert_eq!(stored & TAG_MASK, STRING_TAG); + let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; + assert_string_bytes(stored_ptr, b"closure-payload"); + } + + let entries = + crate::closure::test_captured_singleton_closure_cache_entries(captured_func as *const u8); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].0.len(), 1); + assert_eq!(entries[0].0[0] & TAG_MASK, STRING_TAG); + let cached_capture = (entries[0].0[0] & POINTER_MASK) as *const crate::StringHeader; + unsafe { + assert_string_bytes(cached_capture, b"closure-payload"); + } + crate::closure::test_clear_singleton_closure_caches(); +} diff --git a/crates/perry-runtime/src/node_submodules/test.rs b/crates/perry-runtime/src/node_submodules/test.rs index 8c991e93ad..ea7ffd0ca3 100644 --- a/crates/perry-runtime/src/node_submodules/test.rs +++ b/crates/perry-runtime/src/node_submodules/test.rs @@ -19,6 +19,20 @@ use crate::value::{JSValue, POINTER_MASK, TAG_UNDEFINED}; #[path = "test_property.rs"] mod property_mock; +#[path = "test_reporters.rs"] +mod reporters; +#[path = "test_snapshot.rs"] +mod snapshot; + +// Re-exported / imported so the pre-split `test::` paths keep resolving. +pub(crate) use reporters::{ + thunk_reporter_dot, thunk_reporter_junit, thunk_reporter_lcov, thunk_reporter_spec, + thunk_reporter_tap, +}; +use snapshot::{ + assert_file_snapshot, assert_snapshot, snapshot_object_value, snapshot_set_default_serializers, + snapshot_set_resolve_snapshot_path, +}; const REPORTER_SPEC: i32 = 0; const REPORTER_TAP: i32 = 1; @@ -190,144 +204,6 @@ fn assert_callable_arg(arg: &str, value: f64) { } } -fn json_stringify_pretty(value: f64) -> String { - let spacer = string_value(" "); - let bits = - unsafe { crate::json::js_json_stringify_full(value, undefined_value(), spacer) } as u64; - if bits == TAG_UNDEFINED { - return "undefined".to_string(); - } - let boxed = f64::from_bits(bits); - value_to_string(boxed).unwrap_or_else(|| "undefined".to_string()) -} - -fn snapshot_payload(value: f64) -> String { - let json = json_stringify_pretty(value); - if json == "undefined" { - crate::builtins::format_jsvalue(value, 0) - } else { - json - } -} - -extern "C" fn snapshot_set_default_serializers( - _closure: *const ClosureHeader, - serializers: f64, -) -> f64 { - if !is_array_value(serializers) { - throw_invalid_arg_type("serializers", "Array", serializers); - } - undefined_value() -} - -extern "C" fn snapshot_set_resolve_snapshot_path( - _closure: *const ClosureHeader, - resolver: f64, -) -> f64 { - if !is_callable_value(resolver) { - throw_invalid_arg_type("fn", "function", resolver); - } - SNAPSHOT_RESOLVER.with(|slot| slot.set(resolver)); - undefined_value() -} - -extern "C" fn assert_snapshot(_closure: *const ClosureHeader, value: f64) -> f64 { - CURRENT_ASSERT_COUNT.with(|count| count.set(count.get() + 1)); - let resolver = SNAPSHOT_RESOLVER.with(|slot| slot.get()); - if !is_callable_value(resolver) { - throw_error_with_code( - "Invalid state: snapshot.setResolveSnapshotPath() must be called before t.assert.snapshot()", - "ERR_INVALID_STATE", - ); - } - let resolver_ptr = raw_ptr_from_value(resolver) as *const ClosureHeader; - let path_value = js_closure_call1(resolver_ptr, string_value("")); - let Some(path) = value_to_string(path_value) else { - throw_invalid_arg_type("snapshot path", "string", path_value); - }; - let file = fs::read_to_string(&path).unwrap_or_else(|_| { - throw_error_with_code( - &format!("Invalid state: snapshot file does not exist: {path}"), - "ERR_INVALID_STATE", - ) - }); - let name = CURRENT_TEST_NAME - .with(|n| n.borrow().clone()) - .unwrap_or_else(|| "snapshot".to_string()); - let index = CURRENT_SNAPSHOT_INDEX.with(|idx| { - let next = idx.get() + 1; - idx.set(next); - next - }); - let marker = format!("exports[`{} {}`] = `", name, index); - let Some(start) = file.find(&marker).map(|pos| pos + marker.len()) else { - throw_error_with_code( - &format!("Snapshot `{name} {index}` was not found"), - "ERR_INVALID_STATE", - ); - }; - let Some(end_rel) = file[start..].find("`;") else { - throw_error_with_code("Snapshot file is malformed", "ERR_INVALID_STATE"); - }; - let expected = &file[start..start + end_rel]; - let actual = format!("\n{}\n", snapshot_payload(value)); - if expected.trim_end() != actual.trim_end() { - throw_error_with_code( - &format!( - "Snapshot mismatch for `{name} {index}`\nExpected:\n{expected}\nActual:\n{actual}" - ), - "ERR_ASSERTION", - ); - } - undefined_value() -} - -extern "C" fn assert_file_snapshot( - _closure: *const ClosureHeader, - value: f64, - path_value: f64, -) -> f64 { - CURRENT_ASSERT_COUNT.with(|count| count.set(count.get() + 1)); - let Some(path) = value_to_string(path_value) else { - throw_invalid_arg_type("path", "string", path_value); - }; - let expected = fs::read_to_string(&path).unwrap_or_else(|_| { - throw_error_with_code( - &format!("Invalid state: snapshot file does not exist: {path}"), - "ERR_INVALID_STATE", - ) - }); - let actual = snapshot_payload(value); - if expected.trim_end() != actual.trim_end() { - throw_error_with_code( - &format!("File snapshot mismatch for `{path}`"), - "ERR_ASSERTION", - ); - } - undefined_value() -} - -fn snapshot_object_value() -> f64 { - SNAPSHOT_OBJECT.with(|slot| { - if let Some(ptr) = *slot.borrow() { - return boxed_ptr(ptr); - } - let obj = js_object_alloc(0, 2); - set_field( - obj, - "setDefaultSnapshotSerializers", - closure_value(snapshot_set_default_serializers as *const u8, 1), - ); - set_field( - obj, - "setResolveSnapshotPath", - closure_value(snapshot_set_resolve_snapshot_path as *const u8, 1), - ); - *slot.borrow_mut() = Some(obj); - boxed_ptr(obj) - }) -} - extern "C" fn mock_timers_enable(_closure: *const ClosureHeader, options: f64) -> f64 { let (apis, now) = parse_mock_timer_options(options); crate::timer::js_mock_timers_enable(apis, now); @@ -1844,175 +1720,3 @@ mod tests; #[cfg(test)] #[path = "test_once_unit_tests.rs"] mod once_tests; - -fn reporter_with_kind(kind: i32, source: f64) -> f64 { - if JSValue::from_bits(source.to_bits()).is_undefined() { - return reporter_transform(kind); - } - let events = collect_event_values(source); - let output = format_reporter_events(kind, &events); - readable_from_text(output) -} - -pub(crate) extern "C" fn thunk_reporter_spec(_closure: *const ClosureHeader, source: f64) -> f64 { - reporter_with_kind(REPORTER_SPEC, source) -} - -pub(crate) extern "C" fn thunk_reporter_tap(_closure: *const ClosureHeader, source: f64) -> f64 { - reporter_with_kind(REPORTER_TAP, source) -} - -pub(crate) extern "C" fn thunk_reporter_dot(_closure: *const ClosureHeader, source: f64) -> f64 { - reporter_with_kind(REPORTER_DOT, source) -} - -pub(crate) extern "C" fn thunk_reporter_junit(_closure: *const ClosureHeader, source: f64) -> f64 { - reporter_with_kind(REPORTER_JUNIT, source) -} - -pub(crate) extern "C" fn thunk_reporter_lcov(_closure: *const ClosureHeader, source: f64) -> f64 { - reporter_with_kind(REPORTER_LCOV, source) -} - -fn reporter_transform(kind: i32) -> f64 { - let transform = make_closure(reporter_transform_chunk as *const u8, 3, 1); - js_closure_set_capture_f64(transform, 0, kind as f64); - let opts = js_object_alloc(0, 1); - set_field(opts, "transform", boxed_ptr(transform)); - crate::node_stream::js_node_stream_transform_new(boxed_ptr(opts)) -} - -extern "C" fn reporter_transform_chunk( - closure: *const ClosureHeader, - chunk: f64, - _encoding: f64, - callback: f64, -) -> f64 { - let kind = js_closure_get_capture_f64(closure, 0) as i32; - let output = format_reporter_event(kind, chunk); - if !output.is_empty() { - let this = crate::object::js_implicit_this_get(); - let handle = (this.to_bits() & POINTER_MASK) as i64; - crate::node_stream::js_node_stream_method_push(handle, string_value(&output)); - } - if is_callable_value(callback) { - js_closure_call0(raw_ptr_from_value(callback) as *const ClosureHeader); - } - undefined_value() -} - -fn collect_event_values(source: f64) -> Vec { - if let Some(values) = array_values(source) { - return values; - } - if let Some(Ok(chunks)) = crate::node_stream::js_node_stream_collect_chunks_result(source) { - return array_values(chunks).unwrap_or_else(|| vec![chunks]); - } - vec![source] -} - -fn readable_from_text(text: String) -> f64 { - let mut arr = crate::array::js_array_alloc(if text.is_empty() { 0 } else { 1 }); - if !text.is_empty() { - arr = crate::array::js_array_push_f64(arr, string_value(&text)); - } - crate::node_stream::js_node_stream_readable_from(boxed_ptr(arr)) -} - -fn event_type(event: f64) -> Option { - object_string(event, b"type") -} - -fn event_data(event: f64) -> f64 { - object_property(event, b"data").unwrap_or(undefined_value()) -} - -fn format_reporter_events(kind: i32, events: &[f64]) -> String { - if kind == REPORTER_LCOV { - return String::new(); - } - let mut out = String::new(); - if kind == REPORTER_TAP { - out.push_str("TAP version 13\n"); - } else if kind == REPORTER_JUNIT { - out.push_str("\n\n"); - } - for &event in events { - out.push_str(&format_reporter_event(kind, event)); - } - if kind == REPORTER_DOT && !out.is_empty() && !out.ends_with('\n') { - out.push('\n'); - } - if kind == REPORTER_JUNIT { - out.push_str("\n"); - } - out -} - -fn format_reporter_event(kind: i32, event: f64) -> String { - let Some(typ) = event_type(event) else { - return String::new(); - }; - let data = event_data(event); - match kind { - REPORTER_SPEC => match typ.as_str() { - "test:pass" => object_string(data, b"name") - .map(|name| format!("✔ {name}\n")) - .unwrap_or_default(), - "test:diagnostic" => object_string(data, b"message") - .map(|message| format!("ℹ {message}\n")) - .unwrap_or_default(), - _ => String::new(), - }, - REPORTER_TAP => match typ.as_str() { - "test:start" => object_string(data, b"name") - .map(|name| format!("# Subtest: {name}\n")) - .unwrap_or_default(), - "test:pass" => { - let name = object_string(data, b"name").unwrap_or_default(); - let detail_type = object_property(data, b"details") - .and_then(|details| object_string(details, b"type")) - .unwrap_or_else(|| "test".to_string()); - format!("ok undefined - {name}\n ---\n type: '{detail_type}'\n ...\n") - } - "test:diagnostic" => object_string(data, b"message") - .map(|message| format!("# {message}\n")) - .unwrap_or_default(), - _ => String::new(), - }, - REPORTER_DOT => { - if typ == "test:pass" { - ".".to_string() - } else { - String::new() - } - } - REPORTER_JUNIT => match typ.as_str() { - "test:pass" => { - let name = xml_escape(&object_string(data, b"name").unwrap_or_default()); - let class = object_property(data, b"details") - .and_then(|details| object_string(details, b"type")) - .unwrap_or_else(|| "test".to_string()); - let class = xml_escape(&class); - format!("\t\n") - } - "test:diagnostic" => object_string(data, b"message") - .map(|message| format!("\t\n", xml_escape_comment(&message))) - .unwrap_or_default(), - _ => String::new(), - }, - _ => String::new(), - } -} - -fn xml_escape(input: &str) -> String { - input - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) -} - -fn xml_escape_comment(input: &str) -> String { - input.replace("--", "- -") -} diff --git a/crates/perry-runtime/src/node_submodules/test_reporters.rs b/crates/perry-runtime/src/node_submodules/test_reporters.rs new file mode 100644 index 0000000000..93f8f86a9e --- /dev/null +++ b/crates/perry-runtime/src/node_submodules/test_reporters.rs @@ -0,0 +1,179 @@ +//! `node:test/reporters` — the deterministic spec / tap / dot / junit / lcov +//! formatters and the transform-stream shims that feed them. +//! +//! Split verbatim out of the parent [`super`] module to keep `test.rs` under +//! the 2000-line gate. Pure code move — no behavior change. + +use super::*; + +fn reporter_with_kind(kind: i32, source: f64) -> f64 { + if JSValue::from_bits(source.to_bits()).is_undefined() { + return reporter_transform(kind); + } + let events = collect_event_values(source); + let output = format_reporter_events(kind, &events); + readable_from_text(output) +} + +pub(crate) extern "C" fn thunk_reporter_spec(_closure: *const ClosureHeader, source: f64) -> f64 { + reporter_with_kind(REPORTER_SPEC, source) +} + +pub(crate) extern "C" fn thunk_reporter_tap(_closure: *const ClosureHeader, source: f64) -> f64 { + reporter_with_kind(REPORTER_TAP, source) +} + +pub(crate) extern "C" fn thunk_reporter_dot(_closure: *const ClosureHeader, source: f64) -> f64 { + reporter_with_kind(REPORTER_DOT, source) +} + +pub(crate) extern "C" fn thunk_reporter_junit(_closure: *const ClosureHeader, source: f64) -> f64 { + reporter_with_kind(REPORTER_JUNIT, source) +} + +pub(crate) extern "C" fn thunk_reporter_lcov(_closure: *const ClosureHeader, source: f64) -> f64 { + reporter_with_kind(REPORTER_LCOV, source) +} + +fn reporter_transform(kind: i32) -> f64 { + let transform = make_closure(reporter_transform_chunk as *const u8, 3, 1); + js_closure_set_capture_f64(transform, 0, kind as f64); + let opts = js_object_alloc(0, 1); + set_field(opts, "transform", boxed_ptr(transform)); + crate::node_stream::js_node_stream_transform_new(boxed_ptr(opts)) +} + +extern "C" fn reporter_transform_chunk( + closure: *const ClosureHeader, + chunk: f64, + _encoding: f64, + callback: f64, +) -> f64 { + let kind = js_closure_get_capture_f64(closure, 0) as i32; + let output = format_reporter_event(kind, chunk); + if !output.is_empty() { + let this = crate::object::js_implicit_this_get(); + let handle = (this.to_bits() & POINTER_MASK) as i64; + crate::node_stream::js_node_stream_method_push(handle, string_value(&output)); + } + if is_callable_value(callback) { + js_closure_call0(raw_ptr_from_value(callback) as *const ClosureHeader); + } + undefined_value() +} + +fn collect_event_values(source: f64) -> Vec { + if let Some(values) = array_values(source) { + return values; + } + if let Some(Ok(chunks)) = crate::node_stream::js_node_stream_collect_chunks_result(source) { + return array_values(chunks).unwrap_or_else(|| vec![chunks]); + } + vec![source] +} + +fn readable_from_text(text: String) -> f64 { + let mut arr = crate::array::js_array_alloc(if text.is_empty() { 0 } else { 1 }); + if !text.is_empty() { + arr = crate::array::js_array_push_f64(arr, string_value(&text)); + } + crate::node_stream::js_node_stream_readable_from(boxed_ptr(arr)) +} + +fn event_type(event: f64) -> Option { + object_string(event, b"type") +} + +fn event_data(event: f64) -> f64 { + object_property(event, b"data").unwrap_or(undefined_value()) +} + +fn format_reporter_events(kind: i32, events: &[f64]) -> String { + if kind == REPORTER_LCOV { + return String::new(); + } + let mut out = String::new(); + if kind == REPORTER_TAP { + out.push_str("TAP version 13\n"); + } else if kind == REPORTER_JUNIT { + out.push_str("\n\n"); + } + for &event in events { + out.push_str(&format_reporter_event(kind, event)); + } + if kind == REPORTER_DOT && !out.is_empty() && !out.ends_with('\n') { + out.push('\n'); + } + if kind == REPORTER_JUNIT { + out.push_str("\n"); + } + out +} + +fn format_reporter_event(kind: i32, event: f64) -> String { + let Some(typ) = event_type(event) else { + return String::new(); + }; + let data = event_data(event); + match kind { + REPORTER_SPEC => match typ.as_str() { + "test:pass" => object_string(data, b"name") + .map(|name| format!("✔ {name}\n")) + .unwrap_or_default(), + "test:diagnostic" => object_string(data, b"message") + .map(|message| format!("ℹ {message}\n")) + .unwrap_or_default(), + _ => String::new(), + }, + REPORTER_TAP => match typ.as_str() { + "test:start" => object_string(data, b"name") + .map(|name| format!("# Subtest: {name}\n")) + .unwrap_or_default(), + "test:pass" => { + let name = object_string(data, b"name").unwrap_or_default(); + let detail_type = object_property(data, b"details") + .and_then(|details| object_string(details, b"type")) + .unwrap_or_else(|| "test".to_string()); + format!("ok undefined - {name}\n ---\n type: '{detail_type}'\n ...\n") + } + "test:diagnostic" => object_string(data, b"message") + .map(|message| format!("# {message}\n")) + .unwrap_or_default(), + _ => String::new(), + }, + REPORTER_DOT => { + if typ == "test:pass" { + ".".to_string() + } else { + String::new() + } + } + REPORTER_JUNIT => match typ.as_str() { + "test:pass" => { + let name = xml_escape(&object_string(data, b"name").unwrap_or_default()); + let class = object_property(data, b"details") + .and_then(|details| object_string(details, b"type")) + .unwrap_or_else(|| "test".to_string()); + let class = xml_escape(&class); + format!("\t\n") + } + "test:diagnostic" => object_string(data, b"message") + .map(|message| format!("\t\n", xml_escape_comment(&message))) + .unwrap_or_default(), + _ => String::new(), + }, + _ => String::new(), + } +} + +fn xml_escape(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +fn xml_escape_comment(input: &str) -> String { + input.replace("--", "- -") +} diff --git a/crates/perry-runtime/src/node_submodules/test_snapshot.rs b/crates/perry-runtime/src/node_submodules/test_snapshot.rs new file mode 100644 index 0000000000..b75e1d19bd --- /dev/null +++ b/crates/perry-runtime/src/node_submodules/test_snapshot.rs @@ -0,0 +1,146 @@ +//! `node:test` snapshot support: `t.assert.snapshot()` / +//! `t.assert.fileSnapshot()`, the serializer/path-resolver setters, and the +//! `snapshot` export object they hang off. +//! +//! Split verbatim out of the parent [`super`] module to keep `test.rs` under +//! the 2000-line gate. Pure code move — no behavior change. + +use super::*; + +fn json_stringify_pretty(value: f64) -> String { + let spacer = string_value(" "); + let bits = + unsafe { crate::json::js_json_stringify_full(value, undefined_value(), spacer) } as u64; + if bits == TAG_UNDEFINED { + return "undefined".to_string(); + } + let boxed = f64::from_bits(bits); + value_to_string(boxed).unwrap_or_else(|| "undefined".to_string()) +} + +fn snapshot_payload(value: f64) -> String { + let json = json_stringify_pretty(value); + if json == "undefined" { + crate::builtins::format_jsvalue(value, 0) + } else { + json + } +} + +pub(super) extern "C" fn snapshot_set_default_serializers( + _closure: *const ClosureHeader, + serializers: f64, +) -> f64 { + if !is_array_value(serializers) { + throw_invalid_arg_type("serializers", "Array", serializers); + } + undefined_value() +} + +pub(super) extern "C" fn snapshot_set_resolve_snapshot_path( + _closure: *const ClosureHeader, + resolver: f64, +) -> f64 { + if !is_callable_value(resolver) { + throw_invalid_arg_type("fn", "function", resolver); + } + SNAPSHOT_RESOLVER.with(|slot| slot.set(resolver)); + undefined_value() +} + +pub(super) extern "C" fn assert_snapshot(_closure: *const ClosureHeader, value: f64) -> f64 { + CURRENT_ASSERT_COUNT.with(|count| count.set(count.get() + 1)); + let resolver = SNAPSHOT_RESOLVER.with(|slot| slot.get()); + if !is_callable_value(resolver) { + throw_error_with_code( + "Invalid state: snapshot.setResolveSnapshotPath() must be called before t.assert.snapshot()", + "ERR_INVALID_STATE", + ); + } + let resolver_ptr = raw_ptr_from_value(resolver) as *const ClosureHeader; + let path_value = js_closure_call1(resolver_ptr, string_value("")); + let Some(path) = value_to_string(path_value) else { + throw_invalid_arg_type("snapshot path", "string", path_value); + }; + let file = fs::read_to_string(&path).unwrap_or_else(|_| { + throw_error_with_code( + &format!("Invalid state: snapshot file does not exist: {path}"), + "ERR_INVALID_STATE", + ) + }); + let name = CURRENT_TEST_NAME + .with(|n| n.borrow().clone()) + .unwrap_or_else(|| "snapshot".to_string()); + let index = CURRENT_SNAPSHOT_INDEX.with(|idx| { + let next = idx.get() + 1; + idx.set(next); + next + }); + let marker = format!("exports[`{} {}`] = `", name, index); + let Some(start) = file.find(&marker).map(|pos| pos + marker.len()) else { + throw_error_with_code( + &format!("Snapshot `{name} {index}` was not found"), + "ERR_INVALID_STATE", + ); + }; + let Some(end_rel) = file[start..].find("`;") else { + throw_error_with_code("Snapshot file is malformed", "ERR_INVALID_STATE"); + }; + let expected = &file[start..start + end_rel]; + let actual = format!("\n{}\n", snapshot_payload(value)); + if expected.trim_end() != actual.trim_end() { + throw_error_with_code( + &format!( + "Snapshot mismatch for `{name} {index}`\nExpected:\n{expected}\nActual:\n{actual}" + ), + "ERR_ASSERTION", + ); + } + undefined_value() +} + +pub(super) extern "C" fn assert_file_snapshot( + _closure: *const ClosureHeader, + value: f64, + path_value: f64, +) -> f64 { + CURRENT_ASSERT_COUNT.with(|count| count.set(count.get() + 1)); + let Some(path) = value_to_string(path_value) else { + throw_invalid_arg_type("path", "string", path_value); + }; + let expected = fs::read_to_string(&path).unwrap_or_else(|_| { + throw_error_with_code( + &format!("Invalid state: snapshot file does not exist: {path}"), + "ERR_INVALID_STATE", + ) + }); + let actual = snapshot_payload(value); + if expected.trim_end() != actual.trim_end() { + throw_error_with_code( + &format!("File snapshot mismatch for `{path}`"), + "ERR_ASSERTION", + ); + } + undefined_value() +} + +pub(super) fn snapshot_object_value() -> f64 { + SNAPSHOT_OBJECT.with(|slot| { + if let Some(ptr) = *slot.borrow() { + return boxed_ptr(ptr); + } + let obj = js_object_alloc(0, 2); + set_field( + obj, + "setDefaultSnapshotSerializers", + closure_value(snapshot_set_default_serializers as *const u8, 1), + ); + set_field( + obj, + "setResolveSnapshotPath", + closure_value(snapshot_set_resolve_snapshot_path as *const u8, 1), + ); + *slot.borrow_mut() = Some(obj); + boxed_ptr(obj) + }) +} diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index f5f773f6a7..8b52dd9b48 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -13,9 +13,12 @@ use std::ptr::null_mut; use std::sync::atomic::{AtomicPtr, Ordering}; mod async_hooks_exports; +mod callable_export_arity_table; mod callable_export_check; +mod callable_export_table; pub(crate) mod callable_exports; mod constants; +mod constants_tables; mod module_keys; mod namespace_builders; mod web_locks; diff --git a/crates/perry-runtime/src/object/native_module/callable_export_arity_table.rs b/crates/perry-runtime/src/object/native_module/callable_export_arity_table.rs new file mode 100644 index 0000000000..8f3d587a4a --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/callable_export_arity_table.rs @@ -0,0 +1,750 @@ +/// Test-only reference: original ladder, oracle for `CALLABLE_EXPORT_ARITY_TABLE`. +#[cfg(test)] +fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option { + match (module, prop) { + // bun:ffi (#6562). + ("bun:ffi", "dlopen") => Some(2), + ("bun:ffi", "ptr" | "CString" | "JSCallback" | "CFunction" | "linkSymbols") => Some(1), + ("bun:ffi", "toArrayBuffer" | "toBuffer") => Some(3), + ("bun:ffi", "viewSource") => Some(2), + ("bun:ffi", "read") => Some(0), + // #3687: node:cluster — module-method `.length` matches Node. + ("cluster", "fork" | "disconnect" | "setupPrimary" | "setupMaster" | "Worker") => Some(1), + ("cluster", "emit") => Some(1), + ("cluster", "eventNames") => Some(0), + ("cluster", "getMaxListeners") => Some(0), + ( + "cluster", + "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "removeListener" + | "off" + | "listenerCount", + ) => Some(2), + ("cluster", "listeners" | "rawListeners" | "setMaxListeners") => Some(1), + ("cluster", "removeAllListeners") => Some(1), + // #6563: node-pty `spawn(file, args, options)`. + ("node-pty", "spawn") => Some(3), + ("events", "EventEmitter") => Some(1), + ("events", "EventEmitterAsyncResource") => Some(0), + ("events", "addAbortListener") => Some(2), + ("events", "once") => Some(2), + ("events", "on") => Some(2), + ("events", "getEventListeners") => Some(2), + ("events", "getMaxListeners") => Some(1), + ("events", "listenerCount") => Some(2), + ("events", "setMaxListeners") => Some(0), + ("querystring", "unescapeBuffer" | "unescape") => Some(2), + ("querystring", "escape") => Some(1), + ("querystring", "stringify" | "parse") => Some(4), + ("async_hooks", "AsyncLocalStorage") => Some(0), + ("async_hooks", "AsyncResource") => Some(1), + ("async_hooks", "createHook") => Some(1), + ("async_hooks", "executionAsyncId") => Some(0), + ("async_hooks", "triggerAsyncId") => Some(0), + ("async_hooks", "executionAsyncResource") => Some(0), + ("url", "URL") => Some(1), + ("url", "URLPattern") => Some(0), + ("tls", "getCiphers") => Some(0), + ("tls", "getCACertificates" | "setDefaultCACertificates" | "createSecureContext") => { + Some(1) + } + ("tls", "checkServerIdentity") => Some(2), + ("tls", "convertALPNProtocols") => Some(2), + ("tls", "SecureContext") => Some(1), + // #3726: `crypto.Cipheriv` / `crypto.Decipheriv` constructor exports — + // `(cipher, key, iv, options)` arity matches Node's length 4. + ("crypto", "Cipheriv" | "Decipheriv") => Some(4), + ("crypto", "X509Certificate") => Some(1), + ("crypto", "KeyObject") => Some(2), + ("crypto.KeyObject", "from") => Some(1), + // #2706/#2716 and #2694: crypto module-level callable exports. + ("crypto", "DiffieHellman") => Some(4), + ("crypto", "DiffieHellmanGroup") => Some(1), + ("crypto", "diffieHellman") => Some(2), + ("crypto", "encapsulate") => Some(2), + ("crypto", "decapsulate") => Some(3), + ("crypto", "generateKey" | "generateKeyPair" | "generatePrime") => Some(3), + ("crypto", "generateKeySync" | "generateKeyPairSync") => Some(2), + ("crypto", "generatePrimeSync" | "checkPrime" | "checkPrimeSync" | "setFips") => Some(1), + ("crypto", "secureHeapUsed") => Some(0), + ("crypto", "hkdf") => Some(6), + ("crypto", "hkdfSync") => Some(5), + ("crypto", "scrypt") => Some(4), + ("crypto", "scryptSync") => Some(3), + ("crypto", "argon2") => Some(3), + ("crypto", "argon2Sync") => Some(2), + ("url", "Url") => Some(0), + ("url", "resolveObject") => Some(2), + ("process", "binding" | "_linkedBinding") => Some(1), + ( + "process", + "dlopen" + | "_rawDebug" + | "_debugProcess" + | "_debugEnd" + | "_startProfilerIdleNotifier" + | "_stopProfilerIdleNotifier" + | "reallyExit" + | "_tickCallback" + | "_getActiveHandles" + | "_getActiveRequests" + | "openStdin" + | "_kill", + ) => Some(0), + ("process", "_fatalException") => Some(2), + ("process", "execve") => Some(1), + ("process", "ref" | "unref") => Some(1), + ("process", "setSourceMapsEnabled") => Some(1), + ( + "inspector.Network", + "requestWillBeSent" + | "responseReceived" + | "loadingFinished" + | "loadingFailed" + | "dataSent" + | "dataReceived" + | "webSocketCreated" + | "webSocketClosed" + | "webSocketHandshakeResponseReceived", + ) => Some(1), + ("inspector.NetworkResources", "put") => Some(1), + ( + "inspector.DOMStorage", + "domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage", + ) => Some(1), + ("inspector.Session", "connect" | "connectToMainThread" | "disconnect") => Some(0), + ("inspector.Session" | "inspector/promises.Session", "post") => Some(3), + ( + "process", + "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", + ) => Some(1), + ("process", "hasUncaughtExceptionCaptureCallback") => Some(0), + ("fs", "_toUnixTimestamp") => Some(1), + ("util", "isArray") => Some(1), + ("util", "debug" | "debuglog" | "inherits") => Some(2), + ("console", "context") => Some(1), + ("console", "createTask") => Some(0), + ("util", "MIMEParams") => Some(0), + ("util", "MIMEType") => Some(1), + ("sea", "isSea" | "getAssetKeys") => Some(0), + ("sea", "getRawAsset") => Some(1), + ("sea", "getAsset" | "getAssetAsBlob") => Some(2), + ("stream", "pipeline" | "compose") => Some(0), + ("stream", "finished") => Some(3), + ( + "stream", + "duplexPair" + | "isDisturbed" + | "isErrored" + | "isReadable" + | "isWritable" + | "getDefaultHighWaterMark" + | "_isArrayBufferView" + | "_isUint8Array" + | "_uint8ArrayToBuffer" + | "isDestroyed", + ) => Some(1), + ("stream", "setDefaultHighWaterMark" | "addAbortSignal") => Some(2), + ("net", "connect" | "createConnection") => Some(3), + ("net", "createServer" | "Server") => Some(2), + ("net", "Socket") => Some(1), + ("net", "BlockList" | "SocketAddress") => Some(0), + // #3720: `http2.performServerHandshake(socket[, options])` — length 1. + ("http2", "performServerHandshake") => Some(1), + ("http2", "getDefaultSettings") => Some(0), + ("http2", "getPackedSettings" | "getUnpackedSettings") => Some(1), + // #3905: Node `.length` — connect(authority,options,listener)=3, + // createServer(options,handler)=2. + ("http2", "connect") => Some(3), + ("http2", "createServer" | "createSecureServer") => Some(2), + ("http", "OutgoingMessage") => Some(1), + // #4904: Node `.length` — Agent(options)=1, ClientRequest(input, + // options, cb)=3, IncomingMessage(socket)=1, ServerResponse(req)=1. + ("http", "Agent" | "IncomingMessage" | "ServerResponse") => Some(1), + ("http", "ClientRequest") => Some(3), + // #3697: node:https module-level exports (Node `.length`). + ("https", "request") => Some(0), + ("https", "get") => Some(3), + ("https", "Agent") => Some(1), + // #4904: http twins of the https entries above. + ("http", "request") => Some(0), + ("http", "get") => Some(3), + ("stream", "destroy") => Some(2), + // #3712: node:http module-level helper exports. + ("http", "validateHeaderName" | "validateHeaderValue") => Some(2), + ("http", "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv") => Some(1), + ("http", "_connectionListener") => Some(1), + ("module", "register" | "registerHooks") => Some(1), + // #3904: modern V8 diagnostics/profiler exports (Node .length values). + ("v8", "getCppHeapStatistics") => Some(0), + ( + "v8", + "getHeapSnapshot" + | "isStringOneByteRepresentation" + | "queryObjects" + | "startCpuProfile", + ) => Some(1), + ("v8", "writeHeapSnapshot") => Some(2), + // #3906: implemented top-level v8 helpers reachable as bound callables. + ("v8", "serialize" | "deserialize") => Some(1), + ( + "v8", + "getHeapStatistics" + | "getHeapSpaceStatistics" + | "getHeapCodeStatistics" + | "cachedDataVersionTag" + | "GCProfiler", + ) => Some(0), + // #3127/#3128/#3130/#3284: node:vm no-flag export lengths. + ("vm", "Script") => Some(1), + ("vm", "Module") => Some(1), + ("vm", "SourceTextModule") => Some(1), + ("vm", "SyntheticModule") => Some(2), + ("vm", "createContext" | "measureMemory") => Some(0), + ("vm", "createScript" | "runInThisContext" | "compileFunction") => Some(2), + ("vm", "runInContext" | "runInNewContext") => Some(3), + ("vm", "isContext") => Some(1), + ("net", "_normalizeArgs") => Some(1), + ("net", "_createServerHandle") => Some(5), + ("domain", "Domain" | "createDomain" | "create") => Some(0), + ("util", "diff") => Some(2), + ("dns" | "dns/promises", "Resolver") => Some(0), + ("fs", "ReadStream" | "WriteStream") => Some(2), + ("fs", "Utf8Stream") => Some(0), + ("fs", "Dir" | "Dirent") => Some(3), + ("fs", "Stats") => Some(18), + ("fs", "mkdtempDisposableSync") => Some(2), + ("fs", "openAsBlob") => Some(1), + ("events", "init") => Some(1), + ("repl", "Recoverable") => Some(1), + ("repl", "REPLServer" | "start") => Some(6), + ("wasi", "WASI") => Some(0), + ("perf_hooks", "Performance") => Some(0), + ("perf_hooks", "PerformanceEntry") => Some(0), + ("perf_hooks", "PerformanceMark") => Some(1), + ("perf_hooks", "PerformanceMeasure") => Some(0), + ("perf_hooks", "PerformanceObserver") => Some(1), + ("perf_hooks", "PerformanceObserverEntryList") => Some(0), + ("perf_hooks", "PerformanceResourceTiming") => Some(0), + // #3119/#3126/#3263 node:module helpers. + ("module", "createRequire") => Some(1), + ("module", "Module") => Some(0), + ("module", "enableCompileCache") => Some(1), + ("module", "flushCompileCache") => Some(0), + ("module", "getCompileCacheDir") => Some(0), + ("module", "getSourceMapsSupport") => Some(0), + ("module", "_findPath") => Some(3), + ("module", "_initPaths") => Some(0), + ("module", "_load") => Some(3), + ("module", "_nodeModulePaths") => Some(1), + ("module", "_preloadModules") => Some(1), + ("module", "_resolveFilename") => Some(4), + ("module", "_resolveLookupPaths") => Some(2), + ("module", "setSourceMapsSupport") => Some(1), + ("module", "stripTypeScriptTypes") => Some(1), + ("module", "syncBuiltinESMExports") => Some(0), + ("module", "runMain") => Some(0), + ("tls", "connect") => Some(4), + ("tls", "createServer" | "Server") => Some(2), + ("tls", "TLSSocket") => Some(2), + ("child_process", "_forkChild") => Some(2), + _ => None, + } +} + +/// Sorted (module → sorted (prop, arity)) table replacing the ~15 KB compiled +/// arity ladder. Same oracle-test scheme as `CALLABLE_EXPORT_TABLE`. +static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ + ( + "async_hooks", + &[ + ("AsyncLocalStorage", 0), + ("AsyncResource", 1), + ("createHook", 1), + ("executionAsyncId", 0), + ("executionAsyncResource", 0), + ("triggerAsyncId", 0), + ], + ), + ( + "bun:ffi", + &[ + ("CFunction", 1), + ("CString", 1), + ("JSCallback", 1), + ("dlopen", 2), + ("linkSymbols", 1), + ("ptr", 1), + ("read", 0), + ("toArrayBuffer", 3), + ("toBuffer", 3), + ("viewSource", 2), + ], + ), + ("child_process", &[("_forkChild", 2)]), + ( + "cluster", + &[ + ("Worker", 1), + ("addListener", 2), + ("disconnect", 1), + ("emit", 1), + ("eventNames", 0), + ("fork", 1), + ("getMaxListeners", 0), + ("listenerCount", 2), + ("listeners", 1), + ("off", 2), + ("on", 2), + ("once", 2), + ("prependListener", 2), + ("prependOnceListener", 2), + ("rawListeners", 1), + ("removeAllListeners", 1), + ("removeListener", 2), + ("setMaxListeners", 1), + ("setupMaster", 1), + ("setupPrimary", 1), + ], + ), + ("console", &[("context", 1), ("createTask", 0)]), + ( + "crypto", + &[ + ("Cipheriv", 4), + ("Decipheriv", 4), + ("DiffieHellman", 4), + ("DiffieHellmanGroup", 1), + ("KeyObject", 2), + ("X509Certificate", 1), + ("argon2", 3), + ("argon2Sync", 2), + ("checkPrime", 1), + ("checkPrimeSync", 1), + ("decapsulate", 3), + ("diffieHellman", 2), + ("encapsulate", 2), + ("generateKey", 3), + ("generateKeyPair", 3), + ("generateKeyPairSync", 2), + ("generateKeySync", 2), + ("generatePrime", 3), + ("generatePrimeSync", 1), + ("hkdf", 6), + ("hkdfSync", 5), + ("scrypt", 4), + ("scryptSync", 3), + ("secureHeapUsed", 0), + ("setFips", 1), + ], + ), + ("crypto.KeyObject", &[("from", 1)]), + ("dns", &[("Resolver", 0)]), + ("dns/promises", &[("Resolver", 0)]), + ( + "domain", + &[("Domain", 0), ("create", 0), ("createDomain", 0)], + ), + ( + "events", + &[ + ("EventEmitter", 1), + ("EventEmitterAsyncResource", 0), + ("addAbortListener", 2), + ("getEventListeners", 2), + ("getMaxListeners", 1), + ("init", 1), + ("listenerCount", 2), + ("on", 2), + ("once", 2), + ("setMaxListeners", 0), + ], + ), + ( + "fs", + &[ + ("Dir", 3), + ("Dirent", 3), + ("ReadStream", 2), + ("Stats", 18), + ("Utf8Stream", 0), + ("WriteStream", 2), + ("_toUnixTimestamp", 1), + ("mkdtempDisposableSync", 2), + ("openAsBlob", 1), + ], + ), + ( + "http", + &[ + ("Agent", 1), + ("ClientRequest", 3), + ("IncomingMessage", 1), + ("OutgoingMessage", 1), + ("ServerResponse", 1), + ("_connectionListener", 1), + ("get", 3), + ("request", 0), + ("setGlobalProxyFromEnv", 1), + ("setMaxIdleHTTPParsers", 1), + ("validateHeaderName", 2), + ("validateHeaderValue", 2), + ], + ), + ( + "http2", + &[ + ("connect", 3), + ("createSecureServer", 2), + ("createServer", 2), + ("getDefaultSettings", 0), + ("getPackedSettings", 1), + ("getUnpackedSettings", 1), + ("performServerHandshake", 1), + ], + ), + ("https", &[("Agent", 1), ("get", 3), ("request", 0)]), + ( + "inspector.DOMStorage", + &[ + ("domStorageItemAdded", 1), + ("domStorageItemRemoved", 1), + ("domStorageItemUpdated", 1), + ("domStorageItemsCleared", 1), + ("registerStorage", 1), + ], + ), + ( + "inspector.Network", + &[ + ("dataReceived", 1), + ("dataSent", 1), + ("loadingFailed", 1), + ("loadingFinished", 1), + ("requestWillBeSent", 1), + ("responseReceived", 1), + ("webSocketClosed", 1), + ("webSocketCreated", 1), + ("webSocketHandshakeResponseReceived", 1), + ], + ), + ("inspector.NetworkResources", &[("put", 1)]), + ( + "inspector.Session", + &[ + ("connect", 0), + ("connectToMainThread", 0), + ("disconnect", 0), + ("post", 3), + ], + ), + ("inspector/promises.Session", &[("post", 3)]), + ( + "module", + &[ + ("Module", 0), + ("_findPath", 3), + ("_initPaths", 0), + ("_load", 3), + ("_nodeModulePaths", 1), + ("_preloadModules", 1), + ("_resolveFilename", 4), + ("_resolveLookupPaths", 2), + ("createRequire", 1), + ("enableCompileCache", 1), + ("flushCompileCache", 0), + ("getCompileCacheDir", 0), + ("getSourceMapsSupport", 0), + ("register", 1), + ("registerHooks", 1), + ("runMain", 0), + ("setSourceMapsSupport", 1), + ("stripTypeScriptTypes", 1), + ("syncBuiltinESMExports", 0), + ], + ), + ( + "net", + &[ + ("BlockList", 0), + ("Server", 2), + ("Socket", 1), + ("SocketAddress", 0), + ("_createServerHandle", 5), + ("_normalizeArgs", 1), + ("connect", 3), + ("createConnection", 3), + ("createServer", 2), + ], + ), + ("node-pty", &[("spawn", 3)]), + ( + "perf_hooks", + &[ + ("Performance", 0), + ("PerformanceEntry", 0), + ("PerformanceMark", 1), + ("PerformanceMeasure", 0), + ("PerformanceObserver", 1), + ("PerformanceObserverEntryList", 0), + ("PerformanceResourceTiming", 0), + ], + ), + ( + "process", + &[ + ("_debugEnd", 0), + ("_debugProcess", 0), + ("_fatalException", 2), + ("_getActiveHandles", 0), + ("_getActiveRequests", 0), + ("_kill", 0), + ("_linkedBinding", 1), + ("_rawDebug", 0), + ("_startProfilerIdleNotifier", 0), + ("_stopProfilerIdleNotifier", 0), + ("_tickCallback", 0), + ("addUncaughtExceptionCaptureCallback", 1), + ("binding", 1), + ("dlopen", 0), + ("execve", 1), + ("hasUncaughtExceptionCaptureCallback", 0), + ("openStdin", 0), + ("reallyExit", 0), + ("ref", 1), + ("setSourceMapsEnabled", 1), + ("setUncaughtExceptionCaptureCallback", 1), + ("unref", 1), + ], + ), + ( + "querystring", + &[ + ("escape", 1), + ("parse", 4), + ("stringify", 4), + ("unescape", 2), + ("unescapeBuffer", 2), + ], + ), + ( + "repl", + &[("REPLServer", 6), ("Recoverable", 1), ("start", 6)], + ), + ( + "sea", + &[ + ("getAsset", 2), + ("getAssetAsBlob", 2), + ("getAssetKeys", 0), + ("getRawAsset", 1), + ("isSea", 0), + ], + ), + ( + "stream", + &[ + ("_isArrayBufferView", 1), + ("_isUint8Array", 1), + ("_uint8ArrayToBuffer", 1), + ("addAbortSignal", 2), + ("compose", 0), + ("destroy", 2), + ("duplexPair", 1), + ("finished", 3), + ("getDefaultHighWaterMark", 1), + ("isDestroyed", 1), + ("isDisturbed", 1), + ("isErrored", 1), + ("isReadable", 1), + ("isWritable", 1), + ("pipeline", 0), + ("setDefaultHighWaterMark", 2), + ], + ), + ( + "tls", + &[ + ("SecureContext", 1), + ("Server", 2), + ("TLSSocket", 2), + ("checkServerIdentity", 2), + ("connect", 4), + ("convertALPNProtocols", 2), + ("createSecureContext", 1), + ("createServer", 2), + ("getCACertificates", 1), + ("getCiphers", 0), + ("setDefaultCACertificates", 1), + ], + ), + ( + "url", + &[ + ("URL", 1), + ("URLPattern", 0), + ("Url", 0), + ("resolveObject", 2), + ], + ), + ( + "util", + &[ + ("MIMEParams", 0), + ("MIMEType", 1), + ("debug", 2), + ("debuglog", 2), + ("diff", 2), + ("inherits", 2), + ("isArray", 1), + ], + ), + ( + "v8", + &[ + ("GCProfiler", 0), + ("cachedDataVersionTag", 0), + ("deserialize", 1), + ("getCppHeapStatistics", 0), + ("getHeapCodeStatistics", 0), + ("getHeapSnapshot", 1), + ("getHeapSpaceStatistics", 0), + ("getHeapStatistics", 0), + ("isStringOneByteRepresentation", 1), + ("queryObjects", 1), + ("serialize", 1), + ("startCpuProfile", 1), + ("writeHeapSnapshot", 2), + ], + ), + ( + "vm", + &[ + ("Module", 1), + ("Script", 1), + ("SourceTextModule", 1), + ("SyntheticModule", 2), + ("compileFunction", 2), + ("createContext", 0), + ("createScript", 2), + ("isContext", 1), + ("measureMemory", 0), + ("runInContext", 3), + ("runInNewContext", 3), + ("runInThisContext", 2), + ], + ), + ("wasi", &[("WASI", 0)]), +]; + +pub(super) fn native_callable_export_arity(module: &str, prop: &str) -> Option { + let mi = CALLABLE_EXPORT_ARITY_TABLE + .binary_search_by(|(m, _)| (*m).cmp(module)) + .ok()?; + let row = CALLABLE_EXPORT_ARITY_TABLE[mi].1; + let pi = row.binary_search_by(|(p, _)| (*p).cmp(prop)).ok()?; + Some(row[pi].1) +} + +#[cfg(test)] +mod callable_export_arity_table_tests { + use super::super::callable_exports::TLS_SOCKET_PROTOTYPE_METHODS; + use super::super::*; + use super::*; + + #[test] + fn arity_table_is_sorted() { + for w in CALLABLE_EXPORT_ARITY_TABLE.windows(2) { + assert!(w[0].0 < w[1].0); + } + for (_, row) in CALLABLE_EXPORT_ARITY_TABLE { + for w in row.windows(2) { + assert!(w[0].0 < w[1].0); + } + } + } + + #[test] + fn arity_table_matches_reference_exhaustively() { + let source = concat!( + include_str!("callable_exports.rs"), + include_str!("callable_export_arity_table.rs") + ); + let mut literals: Vec<&str> = Vec::new(); + let mut rest = source; + while let Some(start) = rest.find('"') { + let after = &rest[start + 1..]; + let Some(end) = after.find('"') else { break }; + let lit = &after[..end]; + if !lit.is_empty() && lit.len() < 64 && !lit.contains('\n') { + literals.push(lit); + } + rest = &after[end + 1..]; + } + literals.sort_unstable(); + literals.dedup(); + for module in &literals { + for prop in &literals { + assert_eq!( + native_callable_export_arity(module, prop), + native_callable_export_arity_reference(module, prop), + "divergence at ({module}, {prop})" + ); + } + } + } + + #[test] + fn tls_constructor_prototypes_match_node_parent_classes() { + let server = bound_native_callable_export_value("tls", "Server"); + let server_addr = (server.to_bits() & crate::value::POINTER_MASK) as usize; + let server_proto = crate::closure::closure_get_dynamic_prop(server_addr, "prototype"); + assert!(tls_constructor_prototype_is_instance_of( + server_proto, + "EventEmitter" + )); + assert_eq!( + crate::object::js_instanceof(server_proto, 0xFFFF_0076).to_bits(), + crate::value::TAG_TRUE + ); + let event_emitter = bound_native_callable_export_value("events", "EventEmitter"); + assert_eq!( + crate::object::js_instanceof_dynamic(server_proto, event_emitter).to_bits(), + crate::value::TAG_TRUE + ); + + let socket = bound_native_callable_export_value("tls", "TLSSocket"); + let socket_addr = (socket.to_bits() & crate::value::POINTER_MASK) as usize; + let socket_proto = crate::closure::closure_get_dynamic_prop(socket_addr, "prototype"); + assert!(tls_constructor_prototype_is_instance_of( + socket_proto, + "Duplex" + )); + let socket_proto_obj = + JSValue::from_bits(socket_proto.to_bits()).as_pointer::(); + for &(name, length) in TLS_SOCKET_PROTOTYPE_METHODS { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let method = crate::object::js_object_get_field_by_name(socket_proto_obj, key); + let method_addr = method.as_pointer::() as usize; + assert!(crate::closure::is_closure_ptr(method_addr), "{name}"); + assert_eq!(builtin_closure_length(method_addr), Some(length), "{name}"); + } + assert_eq!( + crate::object::js_instanceof(socket_proto, 0xFFFF_0073).to_bits(), + crate::value::TAG_TRUE + ); + let duplex = bound_native_callable_export_value("stream", "Duplex"); + assert_eq!( + crate::object::js_instanceof_dynamic(socket_proto, duplex).to_bits(), + crate::value::TAG_TRUE + ); + } +} diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs index 5c49fc395c..057d07318e 100644 --- a/crates/perry-runtime/src/object/native_module/callable_export_check.rs +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -1,3 +1,4 @@ +use super::callable_export_table::CALLABLE_EXPORT_TABLE; use super::*; /// Whitelist of (module, property) pairs for which property-read should @@ -1020,1122 +1021,6 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st ) } -/// Sorted (module → sorted props) table of every static callable export. -/// Replaces a ~42 KB compiled match ladder with ~20 KB of data + two binary -/// searches (also faster than the ladder). The dynamic cases (`vm` module -/// flags, platform `lchmod`) stay as code in the lookup below. Generated -/// mechanically from the reference ladder; the `callable_export_table_*` -/// tests prove exact equivalence over the full literal universe. -static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ - ( - "assert", - &[ - "Assert", - "deepEqual", - "deepStrictEqual", - "doesNotMatch", - "doesNotReject", - "doesNotThrow", - "equal", - "fail", - "ifError", - "match", - "notDeepEqual", - "notDeepStrictEqual", - "notEqual", - "notStrictEqual", - "ok", - "partialDeepStrictEqual", - "rejects", - "strictEqual", - "throws", - ], - ), - ( - "assert/strict", - &[ - "Assert", - "deepEqual", - "deepStrictEqual", - "doesNotMatch", - "doesNotReject", - "doesNotThrow", - "equal", - "fail", - "ifError", - "match", - "notDeepEqual", - "notDeepStrictEqual", - "notEqual", - "notStrictEqual", - "ok", - "partialDeepStrictEqual", - "rejects", - "strictEqual", - "throws", - ], - ), - ( - "async_hooks", - &[ - "AsyncLocalStorage", - "AsyncResource", - "createHook", - "executionAsyncId", - "executionAsyncResource", - "triggerAsyncId", - ], - ), - ("buffer", &["atob", "btoa", "isAscii", "isUtf8"]), - ( - "buffer.Buffer", - &[ - "alloc", - "allocUnsafe", - "allocUnsafeSlow", - "byteLength", - "compare", - "concat", - "copyBytesFrom", - "from", - "isBuffer", - "isEncoding", - "of", - ], - ), - ( - "bun:ffi", - &[ - "CFunction", - "CString", - "JSCallback", - "dlopen", - "linkSymbols", - "ptr", - "read", - "toArrayBuffer", - "toBuffer", - "viewSource", - ], - ), - ( - "child_process", - &[ - "ChildProcess", - "_forkChild", - "exec", - "execFile", - "execFileSync", - "execSync", - "fork", - "spawn", - "spawnSync", - ], - ), - ( - "cluster", - &[ - "Worker", - "disconnect", - "fork", - "setupMaster", - "setupPrimary", - ], - ), - ( - "console", - &[ - "Console", - "assert", - "clear", - "context", - "count", - "countReset", - "createTask", - "debug", - "dir", - "dirxml", - "error", - "group", - "groupCollapsed", - "groupEnd", - "info", - "log", - "profile", - "profileEnd", - "table", - "time", - "timeEnd", - "timeLog", - "timeStamp", - "trace", - "warn", - ], - ), - ( - "crypto", - &[ - "Cipheriv", - "Decipheriv", - "DiffieHellman", - "DiffieHellmanGroup", - "ECDH", - "Hash", - "Hmac", - "KeyObject", - "Sign", - "Verify", - "X509Certificate", - "argon2", - "argon2Sync", - "checkPrime", - "checkPrimeSync", - "createCipheriv", - "createDecipheriv", - "createDiffieHellman", - "createDiffieHellmanGroup", - "createECDH", - "createHash", - "createHmac", - "createPrivateKey", - "createPublicKey", - "createSecretKey", - "createSign", - "createVerify", - "decapsulate", - "diffieHellman", - "encapsulate", - "generateKey", - "generateKeyPair", - "generateKeyPairSync", - "generateKeySync", - "generatePrime", - "generatePrimeSync", - "getCipherInfo", - "getCiphers", - "getCurves", - "getDiffieHellman", - "getFips", - "getHashes", - "getRandomValues", - "hash", - "hkdf", - "hkdfSync", - "pbkdf2", - "pbkdf2Sync", - "privateDecrypt", - "privateEncrypt", - "publicDecrypt", - "publicEncrypt", - "randomBytes", - "randomFill", - "randomFillSync", - "randomInt", - "randomUUID", - "randomUUIDv7", - "scrypt", - "scryptSync", - "secureHeapUsed", - "setFips", - "sign", - "timingSafeEqual", - "verify", - ], - ), - ( - "crypto.Certificate", - &["exportChallenge", "exportPublicKey", "verifySpkac"], - ), - ("crypto.KeyObject", &["from"]), - ( - "crypto.subtle", - &[ - "decrypt", - "deriveBits", - "deriveKey", - "digest", - "encrypt", - "exportKey", - "generateKey", - "importKey", - "sign", - "unwrapKey", - "verify", - "wrapKey", - ], - ), - ("crypto.webcrypto", &["getRandomValues", "randomUUID"]), - ("dgram", &["Socket", "createSocket"]), - ( - "dns", - &[ - "Resolver", - "getDefaultResultOrder", - "getServers", - "lookup", - "lookupService", - "resolve", - "resolve4", - "resolve6", - "resolveAny", - "resolveCaa", - "resolveCname", - "resolveMx", - "resolveNaptr", - "resolveNs", - "resolvePtr", - "resolveSoa", - "resolveSrv", - "resolveTlsa", - "resolveTxt", - "reverse", - "setDefaultResultOrder", - "setServers", - ], - ), - ( - "dns/promises", - &[ - "Resolver", - "getDefaultResultOrder", - "getServers", - "lookup", - "lookupService", - "resolve", - "resolve4", - "resolve6", - "resolveAny", - "resolveCaa", - "resolveCname", - "resolveMx", - "resolveNaptr", - "resolveNs", - "resolvePtr", - "resolveSoa", - "resolveSrv", - "resolveTlsa", - "resolveTxt", - "reverse", - "setDefaultResultOrder", - "setServers", - ], - ), - ("domain", &["Domain", "create", "createDomain"]), - ( - "events", - &[ - "EventEmitter", - "EventEmitterAsyncResource", - "addAbortListener", - "getEventListeners", - "getMaxListeners", - "init", - "listenerCount", - "on", - "once", - "setMaxListeners", - ], - ), - ( - "fs", - &[ - "Dir", - "Dirent", - "FileReadStream", - "FileWriteStream", - "ReadStream", - "Stats", - "Utf8Stream", - "WriteStream", - "_toUnixTimestamp", - "access", - "accessSync", - "appendFile", - "appendFileSync", - "chmod", - "chmodSync", - "chown", - "chownSync", - "close", - "closeSync", - "copyFile", - "copyFileSync", - "cp", - "cpSync", - "createReadStream", - "createWriteStream", - "exists", - "existsSync", - "fchmod", - "fchmodSync", - "fchown", - "fchownSync", - "fdatasync", - "fdatasyncSync", - "fstat", - "fstatSync", - "fsync", - "fsyncSync", - "ftruncate", - "ftruncateSync", - "futimes", - "futimesSync", - "glob", - "globSync", - "lchown", - "lchownSync", - "link", - "linkSync", - "lstat", - "lstatSync", - "lutimes", - "lutimesSync", - "mkdir", - "mkdirSync", - "mkdtemp", - "mkdtempDisposableSync", - "mkdtempSync", - "open", - "openAsBlob", - "openSync", - "opendir", - "opendirSync", - "read", - "readFile", - "readFileSync", - "readSync", - "readdir", - "readdirSync", - "readlink", - "readlinkSync", - "readv", - "readvSync", - "realpath", - "realpathSync", - "rename", - "renameSync", - "rm", - "rmSync", - "rmdir", - "rmdirSync", - "stat", - "statSync", - "statfs", - "statfsSync", - "symlink", - "symlinkSync", - "truncate", - "truncateSync", - "unlink", - "unlinkSync", - "unwatchFile", - "utimes", - "utimesSync", - "watch", - "watchFile", - "write", - "writeFile", - "writeFileSync", - "writeSync", - "writev", - "writevSync", - ], - ), - ( - "http", - &[ - "Agent", - "ClientRequest", - "IncomingMessage", - "OutgoingMessage", - "Server", - "ServerResponse", - "_connectionListener", - "createServer", - "get", - "request", - "setGlobalProxyFromEnv", - "setMaxIdleHTTPParsers", - "validateHeaderName", - "validateHeaderValue", - ], - ), - ( - "http2", - &[ - "Server", - "connect", - "createSecureServer", - "createServer", - "getDefaultSettings", - "getPackedSettings", - "getUnpackedSettings", - "performServerHandshake", - ], - ), - ( - "https", - &["Agent", "Server", "createServer", "get", "request"], - ), - ( - "inspector", - &["Session", "close", "open", "url", "waitForDebugger"], - ), - ( - "inspector.DOMStorage", - &[ - "domStorageItemAdded", - "domStorageItemRemoved", - "domStorageItemUpdated", - "domStorageItemsCleared", - "registerStorage", - ], - ), - ( - "inspector.Network", - &[ - "dataReceived", - "dataSent", - "loadingFailed", - "loadingFinished", - "requestWillBeSent", - "responseReceived", - "webSocketClosed", - "webSocketCreated", - "webSocketHandshakeResponseReceived", - ], - ), - ("inspector.NetworkResources", &["put"]), - ( - "inspector.Session", - &[ - "connect", - "connectToMainThread", - "disconnect", - "on", - "once", - "post", - ], - ), - ("inspector/promises", &["Session"]), - ( - "inspector/promises.Session", - &[ - "connect", - "connectToMainThread", - "disconnect", - "on", - "once", - "post", - ], - ), - ( - "module", - &[ - "Module", - "SourceMap", - "_findPath", - "_initPaths", - "_load", - "_nodeModulePaths", - "_preloadModules", - "_resolveFilename", - "_resolveLookupPaths", - "createRequire", - "enableCompileCache", - "findPackageJSON", - "findSourceMap", - "flushCompileCache", - "getCompileCacheDir", - "getSourceMapsSupport", - "isBuiltin", - "register", - "registerHooks", - "runMain", - "setSourceMapsSupport", - "stripTypeScriptTypes", - "syncBuiltinESMExports", - ], - ), - ( - "net", - &[ - "BlockList", - "Server", - "Socket", - "SocketAddress", - "_createServerHandle", - "_normalizeArgs", - "connect", - "createConnection", - "createServer", - ], - ), - ("node-pty", &["spawn"]), - ( - "os", - &[ - "arch", - "availableParallelism", - "cpus", - "endianness", - "freemem", - "getPriority", - "homedir", - "hostname", - "loadavg", - "machine", - "networkInterfaces", - "platform", - "release", - "setPriority", - "tmpdir", - "totalmem", - "type", - "uptime", - "userInfo", - "version", - ], - ), - ( - "path", - &[ - "basename", - "dirname", - "extname", - "format", - "isAbsolute", - "join", - "matchesGlob", - "normalize", - "parse", - "relative", - "resolve", - "toNamespacedPath", - ], - ), - ( - "path.posix", - &[ - "basename", - "dirname", - "extname", - "format", - "isAbsolute", - "join", - "matchesGlob", - "normalize", - "parse", - "relative", - "resolve", - "toNamespacedPath", - ], - ), - ( - "path.win32", - &[ - "basename", - "dirname", - "extname", - "format", - "isAbsolute", - "join", - "matchesGlob", - "normalize", - "parse", - "relative", - "resolve", - "toNamespacedPath", - ], - ), - ( - "perf_histogram", - &[ - "add", - "disable", - "enable", - "percentile", - "percentileBigInt", - "record", - "recordDelta", - "reset", - ], - ), - ( - "perf_hooks", - &[ - "Performance", - "PerformanceEntry", - "PerformanceMark", - "PerformanceMeasure", - "PerformanceObserver", - "PerformanceObserverEntryList", - "PerformanceResourceTiming", - "clearMarks", - "clearMeasures", - "clearResourceTimings", - "createHistogram", - "eventLoopUtilization", - "getEntries", - "getEntriesByName", - "getEntriesByType", - "mark", - "markResourceTiming", - "measure", - "monitorEventLoopDelay", - "now", - "setResourceTimingBufferSize", - "timerify", - "toJSON", - ], - ), - ("perf_observer", &["disconnect", "observe", "takeRecords"]), - ( - "perf_observer_list", - &["getEntries", "getEntriesByName", "getEntriesByType"], - ), - ( - "process", - &[ - "_debugEnd", - "_debugProcess", - "_fatalException", - "_getActiveHandles", - "_getActiveRequests", - "_kill", - "_linkedBinding", - "_rawDebug", - "_startProfilerIdleNotifier", - "_stopProfilerIdleNotifier", - "_tickCallback", - "abort", - "addListener", - "addUncaughtExceptionCaptureCallback", - "availableMemory", - "binding", - "chdir", - "constrainedMemory", - "cpuUsage", - "cwd", - "dlopen", - "emit", - "emitWarning", - "eventNames", - "execve", - "exit", - "getActiveResourcesInfo", - "getBuiltinModule", - "getMaxListeners", - "getegid", - "geteuid", - "getgid", - "getgroups", - "getuid", - "hasUncaughtExceptionCaptureCallback", - "hrtime", - "initgroups", - "kill", - "listenerCount", - "listeners", - "memoryUsage", - "nextTick", - "off", - "on", - "once", - "openStdin", - "prependListener", - "prependOnceListener", - "rawListeners", - "reallyExit", - "ref", - "removeAllListeners", - "removeListener", - "resourceUsage", - "setMaxListeners", - "setSourceMapsEnabled", - "setUncaughtExceptionCaptureCallback", - "setegid", - "seteuid", - "setgid", - "setgroups", - "setuid", - "threadCpuUsage", - "umask", - "unref", - "uptime", - ], - ), - ("punycode", &["decode", "encode", "toASCII", "toUnicode"]), - ("punycode.ucs2", &["decode", "encode"]), - ( - "querystring", - &["escape", "parse", "stringify", "unescape", "unescapeBuffer"], - ), - ( - "readline", - &[ - "clearLine", - "clearScreenDown", - "createInterface", - "cursorTo", - "emitKeypressEvents", - "moveCursor", - ], - ), - ( - "readline/promises", - &["Interface", "Readline", "createInterface"], - ), - ("repl", &["REPLServer", "Recoverable", "start"]), - ( - "sea", - &[ - "getAsset", - "getAssetAsBlob", - "getAssetKeys", - "getRawAsset", - "isSea", - ], - ), - ( - "sqlite", - &["DatabaseSync", "Session", "StatementSync", "backup"], - ), - ( - "stream", - &[ - "Duplex", - "PassThrough", - "Readable", - "Stream", - "Transform", - "Writable", - "_isArrayBufferView", - "_isUint8Array", - "_uint8ArrayToBuffer", - "addAbortSignal", - "compose", - "duplexPair", - "finished", - "getDefaultHighWaterMark", - "isDestroyed", - "isDisturbed", - "isErrored", - "isReadable", - "isWritable", - "pipeline", - "setDefaultHighWaterMark", - ], - ), - ("stream/promises", &["finished", "pipeline"]), - ("string_decoder", &["StringDecoder"]), - ( - "timers", - &[ - "clearImmediate", - "clearInterval", - "clearTimeout", - "setImmediate", - "setInterval", - "setTimeout", - ], - ), - ( - "timers/promises", - &["setImmediate", "setInterval", "setTimeout"], - ), - ( - "tls", - &[ - "SecureContext", - "Server", - "TLSSocket", - "checkServerIdentity", - "connect", - "convertALPNProtocols", - "createSecureContext", - "createServer", - "getCACertificates", - "getCiphers", - "setDefaultCACertificates", - ], - ), - ("tty", &["ReadStream", "WriteStream", "isatty"]), - ( - "url", - &[ - "URL", - "URLPattern", - "URLSearchParams", - "Url", - "domainToASCII", - "domainToUnicode", - "fileURLToPath", - "fileURLToPathBuffer", - "format", - "parse", - "pathToFileURL", - "resolve", - "resolveObject", - "urlToHttpOptions", - ], - ), - ( - "util", - &[ - "MIMEParams", - "MIMEType", - "_errnoException", - "_exceptionWithHostPort", - "_extend", - "aborted", - "callbackify", - "convertProcessSignalToExitCode", - "debug", - "debuglog", - "deprecate", - "diff", - "format", - "formatWithOptions", - "getCallSites", - "getSystemErrorMap", - "getSystemErrorMessage", - "getSystemErrorName", - "inherits", - "inspect", - "isArray", - "isDeepStrictEqual", - "parseArgs", - "parseEnv", - "promisify", - "setTraceSigInt", - "stripVTControlCharacters", - "styleText", - "toUSVString", - "transferableAbortController", - "transferableAbortSignal", - ], - ), - ( - "util.types", - &[ - "isAnyArrayBuffer", - "isArgumentsObject", - "isArrayBuffer", - "isArrayBufferView", - "isAsyncFunction", - "isBigInt64Array", - "isBigIntObject", - "isBigUint64Array", - "isBooleanObject", - "isBoxedPrimitive", - "isCryptoKey", - "isDataView", - "isDate", - "isExternal", - "isFloat16Array", - "isFloat32Array", - "isFloat64Array", - "isGeneratorFunction", - "isGeneratorObject", - "isInt16Array", - "isInt32Array", - "isInt8Array", - "isKeyObject", - "isMap", - "isMapIterator", - "isModuleNamespaceObject", - "isNativeError", - "isNumberObject", - "isPromise", - "isProxy", - "isRegExp", - "isSet", - "isSetIterator", - "isSharedArrayBuffer", - "isStringObject", - "isSymbolObject", - "isTypedArray", - "isUint16Array", - "isUint32Array", - "isUint8Array", - "isUint8ClampedArray", - "isWeakMap", - "isWeakSet", - ], - ), - ( - "util/types", - &[ - "isAnyArrayBuffer", - "isArgumentsObject", - "isArrayBuffer", - "isArrayBufferView", - "isAsyncFunction", - "isBigInt64Array", - "isBigIntObject", - "isBigUint64Array", - "isBooleanObject", - "isBoxedPrimitive", - "isCryptoKey", - "isDataView", - "isDate", - "isExternal", - "isFloat16Array", - "isFloat32Array", - "isFloat64Array", - "isGeneratorFunction", - "isGeneratorObject", - "isInt16Array", - "isInt32Array", - "isInt8Array", - "isKeyObject", - "isMap", - "isMapIterator", - "isModuleNamespaceObject", - "isNativeError", - "isNumberObject", - "isPromise", - "isProxy", - "isRegExp", - "isSet", - "isSetIterator", - "isSharedArrayBuffer", - "isStringObject", - "isSymbolObject", - "isTypedArray", - "isUint16Array", - "isUint32Array", - "isUint8Array", - "isUint8ClampedArray", - "isWeakMap", - "isWeakSet", - ], - ), - ( - "v8", - &[ - "DefaultDeserializer", - "DefaultSerializer", - "Deserializer", - "GCProfiler", - "Serializer", - "cachedDataVersionTag", - "deserialize", - "getCppHeapStatistics", - "getHeapCodeStatistics", - "getHeapSnapshot", - "getHeapSpaceStatistics", - "getHeapStatistics", - "isStringOneByteRepresentation", - "queryObjects", - "serialize", - "setFlagsFromString", - "setHeapSnapshotNearHeapLimit", - "startCpuProfile", - "stopCoverage", - "takeCoverage", - "writeHeapSnapshot", - ], - ), - ("v8.GCProfiler", &["start", "stop"]), - ( - "v8.promiseHooks", - &["createHook", "onAfter", "onBefore", "onInit", "onSettled"], - ), - ( - "v8.startupSnapshot", - &[ - "addDeserializeCallback", - "addSerializeCallback", - "isBuildingSnapshot", - "setDeserializeMainFunction", - ], - ), - ( - "vm", - &[ - "Script", - "compileFunction", - "createContext", - "createScript", - "isContext", - "measureMemory", - "runInContext", - "runInNewContext", - "runInThisContext", - ], - ), - ("wasi", &["WASI"]), - ( - "worker_threads", - &[ - "BroadcastChannel", - "MessageChannel", - "MessagePort", - "Worker", - "getEnvironmentData", - "isMarkedAsUntransferable", - "markAsUncloneable", - "markAsUntransferable", - "moveMessagePortToContext", - "postMessageToThread", - "receiveMessageOnPort", - "setEnvironmentData", - ], - ), - ( - "zlib", - &[ - "BrotliCompress", - "BrotliDecompress", - "Deflate", - "DeflateRaw", - "Gunzip", - "Gzip", - "Inflate", - "InflateRaw", - "Unzip", - "ZstdCompress", - "ZstdDecompress", - "brotliCompress", - "brotliCompressSync", - "brotliDecompress", - "brotliDecompressSync", - "crc32", - "createBrotliCompress", - "createBrotliDecompress", - "createDeflate", - "createDeflateRaw", - "createGunzip", - "createGzip", - "createInflate", - "createInflateRaw", - "createUnzip", - "createZstdCompress", - "createZstdDecompress", - "deflate", - "deflateRaw", - "deflateRawSync", - "deflateSync", - "gunzip", - "gunzipSync", - "gzip", - "gzipSync", - "inflate", - "inflateRaw", - "inflateRawSync", - "inflateSync", - "unzip", - "unzipSync", - "zstdCompress", - "zstdCompressSync", - "zstdDecompress", - "zstdDecompressSync", - ], - ), -]; - pub(crate) fn is_native_module_callable_export(module: &str, prop: &str) -> bool { let module = cjs_default_base_module(module).unwrap_or(module); let module = assert_instance_base_module(module).unwrap_or(module); @@ -2185,7 +1070,10 @@ mod callable_export_table_tests { /// through identical code in both, so they agree by construction. #[test] fn table_matches_reference_ladder_exhaustively() { - let source = include_str!("callable_export_check.rs"); + let source = concat!( + include_str!("callable_export_check.rs"), + include_str!("callable_export_table.rs") + ); let mut literals: Vec<&str> = Vec::new(); let mut rest = source; while let Some(start) = rest.find('"') { diff --git a/crates/perry-runtime/src/object/native_module/callable_export_table.rs b/crates/perry-runtime/src/object/native_module/callable_export_table.rs new file mode 100644 index 0000000000..2336e75a78 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/callable_export_table.rs @@ -0,0 +1,1115 @@ +/// Sorted (module → sorted props) table of every static callable export. +/// Replaces a ~42 KB compiled match ladder with ~20 KB of data + two binary +/// searches (also faster than the ladder). The dynamic cases (`vm` module +/// flags, platform `lchmod`) stay as code in the lookup below. Generated +/// mechanically from the reference ladder; the `callable_export_table_*` +/// tests prove exact equivalence over the full literal universe. +pub(super) static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ + ( + "assert", + &[ + "Assert", + "deepEqual", + "deepStrictEqual", + "doesNotMatch", + "doesNotReject", + "doesNotThrow", + "equal", + "fail", + "ifError", + "match", + "notDeepEqual", + "notDeepStrictEqual", + "notEqual", + "notStrictEqual", + "ok", + "partialDeepStrictEqual", + "rejects", + "strictEqual", + "throws", + ], + ), + ( + "assert/strict", + &[ + "Assert", + "deepEqual", + "deepStrictEqual", + "doesNotMatch", + "doesNotReject", + "doesNotThrow", + "equal", + "fail", + "ifError", + "match", + "notDeepEqual", + "notDeepStrictEqual", + "notEqual", + "notStrictEqual", + "ok", + "partialDeepStrictEqual", + "rejects", + "strictEqual", + "throws", + ], + ), + ( + "async_hooks", + &[ + "AsyncLocalStorage", + "AsyncResource", + "createHook", + "executionAsyncId", + "executionAsyncResource", + "triggerAsyncId", + ], + ), + ("buffer", &["atob", "btoa", "isAscii", "isUtf8"]), + ( + "buffer.Buffer", + &[ + "alloc", + "allocUnsafe", + "allocUnsafeSlow", + "byteLength", + "compare", + "concat", + "copyBytesFrom", + "from", + "isBuffer", + "isEncoding", + "of", + ], + ), + ( + "bun:ffi", + &[ + "CFunction", + "CString", + "JSCallback", + "dlopen", + "linkSymbols", + "ptr", + "read", + "toArrayBuffer", + "toBuffer", + "viewSource", + ], + ), + ( + "child_process", + &[ + "ChildProcess", + "_forkChild", + "exec", + "execFile", + "execFileSync", + "execSync", + "fork", + "spawn", + "spawnSync", + ], + ), + ( + "cluster", + &[ + "Worker", + "disconnect", + "fork", + "setupMaster", + "setupPrimary", + ], + ), + ( + "console", + &[ + "Console", + "assert", + "clear", + "context", + "count", + "countReset", + "createTask", + "debug", + "dir", + "dirxml", + "error", + "group", + "groupCollapsed", + "groupEnd", + "info", + "log", + "profile", + "profileEnd", + "table", + "time", + "timeEnd", + "timeLog", + "timeStamp", + "trace", + "warn", + ], + ), + ( + "crypto", + &[ + "Cipheriv", + "Decipheriv", + "DiffieHellman", + "DiffieHellmanGroup", + "ECDH", + "Hash", + "Hmac", + "KeyObject", + "Sign", + "Verify", + "X509Certificate", + "argon2", + "argon2Sync", + "checkPrime", + "checkPrimeSync", + "createCipheriv", + "createDecipheriv", + "createDiffieHellman", + "createDiffieHellmanGroup", + "createECDH", + "createHash", + "createHmac", + "createPrivateKey", + "createPublicKey", + "createSecretKey", + "createSign", + "createVerify", + "decapsulate", + "diffieHellman", + "encapsulate", + "generateKey", + "generateKeyPair", + "generateKeyPairSync", + "generateKeySync", + "generatePrime", + "generatePrimeSync", + "getCipherInfo", + "getCiphers", + "getCurves", + "getDiffieHellman", + "getFips", + "getHashes", + "getRandomValues", + "hash", + "hkdf", + "hkdfSync", + "pbkdf2", + "pbkdf2Sync", + "privateDecrypt", + "privateEncrypt", + "publicDecrypt", + "publicEncrypt", + "randomBytes", + "randomFill", + "randomFillSync", + "randomInt", + "randomUUID", + "randomUUIDv7", + "scrypt", + "scryptSync", + "secureHeapUsed", + "setFips", + "sign", + "timingSafeEqual", + "verify", + ], + ), + ( + "crypto.Certificate", + &["exportChallenge", "exportPublicKey", "verifySpkac"], + ), + ("crypto.KeyObject", &["from"]), + ( + "crypto.subtle", + &[ + "decrypt", + "deriveBits", + "deriveKey", + "digest", + "encrypt", + "exportKey", + "generateKey", + "importKey", + "sign", + "unwrapKey", + "verify", + "wrapKey", + ], + ), + ("crypto.webcrypto", &["getRandomValues", "randomUUID"]), + ("dgram", &["Socket", "createSocket"]), + ( + "dns", + &[ + "Resolver", + "getDefaultResultOrder", + "getServers", + "lookup", + "lookupService", + "resolve", + "resolve4", + "resolve6", + "resolveAny", + "resolveCaa", + "resolveCname", + "resolveMx", + "resolveNaptr", + "resolveNs", + "resolvePtr", + "resolveSoa", + "resolveSrv", + "resolveTlsa", + "resolveTxt", + "reverse", + "setDefaultResultOrder", + "setServers", + ], + ), + ( + "dns/promises", + &[ + "Resolver", + "getDefaultResultOrder", + "getServers", + "lookup", + "lookupService", + "resolve", + "resolve4", + "resolve6", + "resolveAny", + "resolveCaa", + "resolveCname", + "resolveMx", + "resolveNaptr", + "resolveNs", + "resolvePtr", + "resolveSoa", + "resolveSrv", + "resolveTlsa", + "resolveTxt", + "reverse", + "setDefaultResultOrder", + "setServers", + ], + ), + ("domain", &["Domain", "create", "createDomain"]), + ( + "events", + &[ + "EventEmitter", + "EventEmitterAsyncResource", + "addAbortListener", + "getEventListeners", + "getMaxListeners", + "init", + "listenerCount", + "on", + "once", + "setMaxListeners", + ], + ), + ( + "fs", + &[ + "Dir", + "Dirent", + "FileReadStream", + "FileWriteStream", + "ReadStream", + "Stats", + "Utf8Stream", + "WriteStream", + "_toUnixTimestamp", + "access", + "accessSync", + "appendFile", + "appendFileSync", + "chmod", + "chmodSync", + "chown", + "chownSync", + "close", + "closeSync", + "copyFile", + "copyFileSync", + "cp", + "cpSync", + "createReadStream", + "createWriteStream", + "exists", + "existsSync", + "fchmod", + "fchmodSync", + "fchown", + "fchownSync", + "fdatasync", + "fdatasyncSync", + "fstat", + "fstatSync", + "fsync", + "fsyncSync", + "ftruncate", + "ftruncateSync", + "futimes", + "futimesSync", + "glob", + "globSync", + "lchown", + "lchownSync", + "link", + "linkSync", + "lstat", + "lstatSync", + "lutimes", + "lutimesSync", + "mkdir", + "mkdirSync", + "mkdtemp", + "mkdtempDisposableSync", + "mkdtempSync", + "open", + "openAsBlob", + "openSync", + "opendir", + "opendirSync", + "read", + "readFile", + "readFileSync", + "readSync", + "readdir", + "readdirSync", + "readlink", + "readlinkSync", + "readv", + "readvSync", + "realpath", + "realpathSync", + "rename", + "renameSync", + "rm", + "rmSync", + "rmdir", + "rmdirSync", + "stat", + "statSync", + "statfs", + "statfsSync", + "symlink", + "symlinkSync", + "truncate", + "truncateSync", + "unlink", + "unlinkSync", + "unwatchFile", + "utimes", + "utimesSync", + "watch", + "watchFile", + "write", + "writeFile", + "writeFileSync", + "writeSync", + "writev", + "writevSync", + ], + ), + ( + "http", + &[ + "Agent", + "ClientRequest", + "IncomingMessage", + "OutgoingMessage", + "Server", + "ServerResponse", + "_connectionListener", + "createServer", + "get", + "request", + "setGlobalProxyFromEnv", + "setMaxIdleHTTPParsers", + "validateHeaderName", + "validateHeaderValue", + ], + ), + ( + "http2", + &[ + "Server", + "connect", + "createSecureServer", + "createServer", + "getDefaultSettings", + "getPackedSettings", + "getUnpackedSettings", + "performServerHandshake", + ], + ), + ( + "https", + &["Agent", "Server", "createServer", "get", "request"], + ), + ( + "inspector", + &["Session", "close", "open", "url", "waitForDebugger"], + ), + ( + "inspector.DOMStorage", + &[ + "domStorageItemAdded", + "domStorageItemRemoved", + "domStorageItemUpdated", + "domStorageItemsCleared", + "registerStorage", + ], + ), + ( + "inspector.Network", + &[ + "dataReceived", + "dataSent", + "loadingFailed", + "loadingFinished", + "requestWillBeSent", + "responseReceived", + "webSocketClosed", + "webSocketCreated", + "webSocketHandshakeResponseReceived", + ], + ), + ("inspector.NetworkResources", &["put"]), + ( + "inspector.Session", + &[ + "connect", + "connectToMainThread", + "disconnect", + "on", + "once", + "post", + ], + ), + ("inspector/promises", &["Session"]), + ( + "inspector/promises.Session", + &[ + "connect", + "connectToMainThread", + "disconnect", + "on", + "once", + "post", + ], + ), + ( + "module", + &[ + "Module", + "SourceMap", + "_findPath", + "_initPaths", + "_load", + "_nodeModulePaths", + "_preloadModules", + "_resolveFilename", + "_resolveLookupPaths", + "createRequire", + "enableCompileCache", + "findPackageJSON", + "findSourceMap", + "flushCompileCache", + "getCompileCacheDir", + "getSourceMapsSupport", + "isBuiltin", + "register", + "registerHooks", + "runMain", + "setSourceMapsSupport", + "stripTypeScriptTypes", + "syncBuiltinESMExports", + ], + ), + ( + "net", + &[ + "BlockList", + "Server", + "Socket", + "SocketAddress", + "_createServerHandle", + "_normalizeArgs", + "connect", + "createConnection", + "createServer", + ], + ), + ("node-pty", &["spawn"]), + ( + "os", + &[ + "arch", + "availableParallelism", + "cpus", + "endianness", + "freemem", + "getPriority", + "homedir", + "hostname", + "loadavg", + "machine", + "networkInterfaces", + "platform", + "release", + "setPriority", + "tmpdir", + "totalmem", + "type", + "uptime", + "userInfo", + "version", + ], + ), + ( + "path", + &[ + "basename", + "dirname", + "extname", + "format", + "isAbsolute", + "join", + "matchesGlob", + "normalize", + "parse", + "relative", + "resolve", + "toNamespacedPath", + ], + ), + ( + "path.posix", + &[ + "basename", + "dirname", + "extname", + "format", + "isAbsolute", + "join", + "matchesGlob", + "normalize", + "parse", + "relative", + "resolve", + "toNamespacedPath", + ], + ), + ( + "path.win32", + &[ + "basename", + "dirname", + "extname", + "format", + "isAbsolute", + "join", + "matchesGlob", + "normalize", + "parse", + "relative", + "resolve", + "toNamespacedPath", + ], + ), + ( + "perf_histogram", + &[ + "add", + "disable", + "enable", + "percentile", + "percentileBigInt", + "record", + "recordDelta", + "reset", + ], + ), + ( + "perf_hooks", + &[ + "Performance", + "PerformanceEntry", + "PerformanceMark", + "PerformanceMeasure", + "PerformanceObserver", + "PerformanceObserverEntryList", + "PerformanceResourceTiming", + "clearMarks", + "clearMeasures", + "clearResourceTimings", + "createHistogram", + "eventLoopUtilization", + "getEntries", + "getEntriesByName", + "getEntriesByType", + "mark", + "markResourceTiming", + "measure", + "monitorEventLoopDelay", + "now", + "setResourceTimingBufferSize", + "timerify", + "toJSON", + ], + ), + ("perf_observer", &["disconnect", "observe", "takeRecords"]), + ( + "perf_observer_list", + &["getEntries", "getEntriesByName", "getEntriesByType"], + ), + ( + "process", + &[ + "_debugEnd", + "_debugProcess", + "_fatalException", + "_getActiveHandles", + "_getActiveRequests", + "_kill", + "_linkedBinding", + "_rawDebug", + "_startProfilerIdleNotifier", + "_stopProfilerIdleNotifier", + "_tickCallback", + "abort", + "addListener", + "addUncaughtExceptionCaptureCallback", + "availableMemory", + "binding", + "chdir", + "constrainedMemory", + "cpuUsage", + "cwd", + "dlopen", + "emit", + "emitWarning", + "eventNames", + "execve", + "exit", + "getActiveResourcesInfo", + "getBuiltinModule", + "getMaxListeners", + "getegid", + "geteuid", + "getgid", + "getgroups", + "getuid", + "hasUncaughtExceptionCaptureCallback", + "hrtime", + "initgroups", + "kill", + "listenerCount", + "listeners", + "memoryUsage", + "nextTick", + "off", + "on", + "once", + "openStdin", + "prependListener", + "prependOnceListener", + "rawListeners", + "reallyExit", + "ref", + "removeAllListeners", + "removeListener", + "resourceUsage", + "setMaxListeners", + "setSourceMapsEnabled", + "setUncaughtExceptionCaptureCallback", + "setegid", + "seteuid", + "setgid", + "setgroups", + "setuid", + "threadCpuUsage", + "umask", + "unref", + "uptime", + ], + ), + ("punycode", &["decode", "encode", "toASCII", "toUnicode"]), + ("punycode.ucs2", &["decode", "encode"]), + ( + "querystring", + &["escape", "parse", "stringify", "unescape", "unescapeBuffer"], + ), + ( + "readline", + &[ + "clearLine", + "clearScreenDown", + "createInterface", + "cursorTo", + "emitKeypressEvents", + "moveCursor", + ], + ), + ( + "readline/promises", + &["Interface", "Readline", "createInterface"], + ), + ("repl", &["REPLServer", "Recoverable", "start"]), + ( + "sea", + &[ + "getAsset", + "getAssetAsBlob", + "getAssetKeys", + "getRawAsset", + "isSea", + ], + ), + ( + "sqlite", + &["DatabaseSync", "Session", "StatementSync", "backup"], + ), + ( + "stream", + &[ + "Duplex", + "PassThrough", + "Readable", + "Stream", + "Transform", + "Writable", + "_isArrayBufferView", + "_isUint8Array", + "_uint8ArrayToBuffer", + "addAbortSignal", + "compose", + "duplexPair", + "finished", + "getDefaultHighWaterMark", + "isDestroyed", + "isDisturbed", + "isErrored", + "isReadable", + "isWritable", + "pipeline", + "setDefaultHighWaterMark", + ], + ), + ("stream/promises", &["finished", "pipeline"]), + ("string_decoder", &["StringDecoder"]), + ( + "timers", + &[ + "clearImmediate", + "clearInterval", + "clearTimeout", + "setImmediate", + "setInterval", + "setTimeout", + ], + ), + ( + "timers/promises", + &["setImmediate", "setInterval", "setTimeout"], + ), + ( + "tls", + &[ + "SecureContext", + "Server", + "TLSSocket", + "checkServerIdentity", + "connect", + "convertALPNProtocols", + "createSecureContext", + "createServer", + "getCACertificates", + "getCiphers", + "setDefaultCACertificates", + ], + ), + ("tty", &["ReadStream", "WriteStream", "isatty"]), + ( + "url", + &[ + "URL", + "URLPattern", + "URLSearchParams", + "Url", + "domainToASCII", + "domainToUnicode", + "fileURLToPath", + "fileURLToPathBuffer", + "format", + "parse", + "pathToFileURL", + "resolve", + "resolveObject", + "urlToHttpOptions", + ], + ), + ( + "util", + &[ + "MIMEParams", + "MIMEType", + "_errnoException", + "_exceptionWithHostPort", + "_extend", + "aborted", + "callbackify", + "convertProcessSignalToExitCode", + "debug", + "debuglog", + "deprecate", + "diff", + "format", + "formatWithOptions", + "getCallSites", + "getSystemErrorMap", + "getSystemErrorMessage", + "getSystemErrorName", + "inherits", + "inspect", + "isArray", + "isDeepStrictEqual", + "parseArgs", + "parseEnv", + "promisify", + "setTraceSigInt", + "stripVTControlCharacters", + "styleText", + "toUSVString", + "transferableAbortController", + "transferableAbortSignal", + ], + ), + ( + "util.types", + &[ + "isAnyArrayBuffer", + "isArgumentsObject", + "isArrayBuffer", + "isArrayBufferView", + "isAsyncFunction", + "isBigInt64Array", + "isBigIntObject", + "isBigUint64Array", + "isBooleanObject", + "isBoxedPrimitive", + "isCryptoKey", + "isDataView", + "isDate", + "isExternal", + "isFloat16Array", + "isFloat32Array", + "isFloat64Array", + "isGeneratorFunction", + "isGeneratorObject", + "isInt16Array", + "isInt32Array", + "isInt8Array", + "isKeyObject", + "isMap", + "isMapIterator", + "isModuleNamespaceObject", + "isNativeError", + "isNumberObject", + "isPromise", + "isProxy", + "isRegExp", + "isSet", + "isSetIterator", + "isSharedArrayBuffer", + "isStringObject", + "isSymbolObject", + "isTypedArray", + "isUint16Array", + "isUint32Array", + "isUint8Array", + "isUint8ClampedArray", + "isWeakMap", + "isWeakSet", + ], + ), + ( + "util/types", + &[ + "isAnyArrayBuffer", + "isArgumentsObject", + "isArrayBuffer", + "isArrayBufferView", + "isAsyncFunction", + "isBigInt64Array", + "isBigIntObject", + "isBigUint64Array", + "isBooleanObject", + "isBoxedPrimitive", + "isCryptoKey", + "isDataView", + "isDate", + "isExternal", + "isFloat16Array", + "isFloat32Array", + "isFloat64Array", + "isGeneratorFunction", + "isGeneratorObject", + "isInt16Array", + "isInt32Array", + "isInt8Array", + "isKeyObject", + "isMap", + "isMapIterator", + "isModuleNamespaceObject", + "isNativeError", + "isNumberObject", + "isPromise", + "isProxy", + "isRegExp", + "isSet", + "isSetIterator", + "isSharedArrayBuffer", + "isStringObject", + "isSymbolObject", + "isTypedArray", + "isUint16Array", + "isUint32Array", + "isUint8Array", + "isUint8ClampedArray", + "isWeakMap", + "isWeakSet", + ], + ), + ( + "v8", + &[ + "DefaultDeserializer", + "DefaultSerializer", + "Deserializer", + "GCProfiler", + "Serializer", + "cachedDataVersionTag", + "deserialize", + "getCppHeapStatistics", + "getHeapCodeStatistics", + "getHeapSnapshot", + "getHeapSpaceStatistics", + "getHeapStatistics", + "isStringOneByteRepresentation", + "queryObjects", + "serialize", + "setFlagsFromString", + "setHeapSnapshotNearHeapLimit", + "startCpuProfile", + "stopCoverage", + "takeCoverage", + "writeHeapSnapshot", + ], + ), + ("v8.GCProfiler", &["start", "stop"]), + ( + "v8.promiseHooks", + &["createHook", "onAfter", "onBefore", "onInit", "onSettled"], + ), + ( + "v8.startupSnapshot", + &[ + "addDeserializeCallback", + "addSerializeCallback", + "isBuildingSnapshot", + "setDeserializeMainFunction", + ], + ), + ( + "vm", + &[ + "Script", + "compileFunction", + "createContext", + "createScript", + "isContext", + "measureMemory", + "runInContext", + "runInNewContext", + "runInThisContext", + ], + ), + ("wasi", &["WASI"]), + ( + "worker_threads", + &[ + "BroadcastChannel", + "MessageChannel", + "MessagePort", + "Worker", + "getEnvironmentData", + "isMarkedAsUntransferable", + "markAsUncloneable", + "markAsUntransferable", + "moveMessagePortToContext", + "postMessageToThread", + "receiveMessageOnPort", + "setEnvironmentData", + ], + ), + ( + "zlib", + &[ + "BrotliCompress", + "BrotliDecompress", + "Deflate", + "DeflateRaw", + "Gunzip", + "Gzip", + "Inflate", + "InflateRaw", + "Unzip", + "ZstdCompress", + "ZstdDecompress", + "brotliCompress", + "brotliCompressSync", + "brotliDecompress", + "brotliDecompressSync", + "crc32", + "createBrotliCompress", + "createBrotliDecompress", + "createDeflate", + "createDeflateRaw", + "createGunzip", + "createGzip", + "createInflate", + "createInflateRaw", + "createUnzip", + "createZstdCompress", + "createZstdDecompress", + "deflate", + "deflateRaw", + "deflateRawSync", + "deflateSync", + "gunzip", + "gunzipSync", + "gzip", + "gzipSync", + "inflate", + "inflateRaw", + "inflateRawSync", + "inflateSync", + "unzip", + "unzipSync", + "zstdCompress", + "zstdCompressSync", + "zstdDecompress", + "zstdDecompressSync", + ], + ), +]; diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 37a0276cdd..aad2f4e9ee 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -1,3 +1,4 @@ +use super::callable_export_arity_table::native_callable_export_arity; use super::*; use std::cell::Cell; @@ -227,268 +228,6 @@ pub(crate) fn is_cluster_emitter_method(prop: &str) -> bool { ) } -/// Test-only reference: original ladder, oracle for `CALLABLE_EXPORT_ARITY_TABLE`. -#[cfg(test)] -fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option { - match (module, prop) { - // bun:ffi (#6562). - ("bun:ffi", "dlopen") => Some(2), - ("bun:ffi", "ptr" | "CString" | "JSCallback" | "CFunction" | "linkSymbols") => Some(1), - ("bun:ffi", "toArrayBuffer" | "toBuffer") => Some(3), - ("bun:ffi", "viewSource") => Some(2), - ("bun:ffi", "read") => Some(0), - // #3687: node:cluster — module-method `.length` matches Node. - ("cluster", "fork" | "disconnect" | "setupPrimary" | "setupMaster" | "Worker") => Some(1), - ("cluster", "emit") => Some(1), - ("cluster", "eventNames") => Some(0), - ("cluster", "getMaxListeners") => Some(0), - ( - "cluster", - "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "removeListener" - | "off" - | "listenerCount", - ) => Some(2), - ("cluster", "listeners" | "rawListeners" | "setMaxListeners") => Some(1), - ("cluster", "removeAllListeners") => Some(1), - // #6563: node-pty `spawn(file, args, options)`. - ("node-pty", "spawn") => Some(3), - ("events", "EventEmitter") => Some(1), - ("events", "EventEmitterAsyncResource") => Some(0), - ("events", "addAbortListener") => Some(2), - ("events", "once") => Some(2), - ("events", "on") => Some(2), - ("events", "getEventListeners") => Some(2), - ("events", "getMaxListeners") => Some(1), - ("events", "listenerCount") => Some(2), - ("events", "setMaxListeners") => Some(0), - ("querystring", "unescapeBuffer" | "unescape") => Some(2), - ("querystring", "escape") => Some(1), - ("querystring", "stringify" | "parse") => Some(4), - ("async_hooks", "AsyncLocalStorage") => Some(0), - ("async_hooks", "AsyncResource") => Some(1), - ("async_hooks", "createHook") => Some(1), - ("async_hooks", "executionAsyncId") => Some(0), - ("async_hooks", "triggerAsyncId") => Some(0), - ("async_hooks", "executionAsyncResource") => Some(0), - ("url", "URL") => Some(1), - ("url", "URLPattern") => Some(0), - ("tls", "getCiphers") => Some(0), - ("tls", "getCACertificates" | "setDefaultCACertificates" | "createSecureContext") => { - Some(1) - } - ("tls", "checkServerIdentity") => Some(2), - ("tls", "convertALPNProtocols") => Some(2), - ("tls", "SecureContext") => Some(1), - // #3726: `crypto.Cipheriv` / `crypto.Decipheriv` constructor exports — - // `(cipher, key, iv, options)` arity matches Node's length 4. - ("crypto", "Cipheriv" | "Decipheriv") => Some(4), - ("crypto", "X509Certificate") => Some(1), - ("crypto", "KeyObject") => Some(2), - ("crypto.KeyObject", "from") => Some(1), - // #2706/#2716 and #2694: crypto module-level callable exports. - ("crypto", "DiffieHellman") => Some(4), - ("crypto", "DiffieHellmanGroup") => Some(1), - ("crypto", "diffieHellman") => Some(2), - ("crypto", "encapsulate") => Some(2), - ("crypto", "decapsulate") => Some(3), - ("crypto", "generateKey" | "generateKeyPair" | "generatePrime") => Some(3), - ("crypto", "generateKeySync" | "generateKeyPairSync") => Some(2), - ("crypto", "generatePrimeSync" | "checkPrime" | "checkPrimeSync" | "setFips") => Some(1), - ("crypto", "secureHeapUsed") => Some(0), - ("crypto", "hkdf") => Some(6), - ("crypto", "hkdfSync") => Some(5), - ("crypto", "scrypt") => Some(4), - ("crypto", "scryptSync") => Some(3), - ("crypto", "argon2") => Some(3), - ("crypto", "argon2Sync") => Some(2), - ("url", "Url") => Some(0), - ("url", "resolveObject") => Some(2), - ("process", "binding" | "_linkedBinding") => Some(1), - ( - "process", - "dlopen" - | "_rawDebug" - | "_debugProcess" - | "_debugEnd" - | "_startProfilerIdleNotifier" - | "_stopProfilerIdleNotifier" - | "reallyExit" - | "_tickCallback" - | "_getActiveHandles" - | "_getActiveRequests" - | "openStdin" - | "_kill", - ) => Some(0), - ("process", "_fatalException") => Some(2), - ("process", "execve") => Some(1), - ("process", "ref" | "unref") => Some(1), - ("process", "setSourceMapsEnabled") => Some(1), - ( - "inspector.Network", - "requestWillBeSent" - | "responseReceived" - | "loadingFinished" - | "loadingFailed" - | "dataSent" - | "dataReceived" - | "webSocketCreated" - | "webSocketClosed" - | "webSocketHandshakeResponseReceived", - ) => Some(1), - ("inspector.NetworkResources", "put") => Some(1), - ( - "inspector.DOMStorage", - "domStorageItemAdded" - | "domStorageItemRemoved" - | "domStorageItemUpdated" - | "domStorageItemsCleared" - | "registerStorage", - ) => Some(1), - ("inspector.Session", "connect" | "connectToMainThread" | "disconnect") => Some(0), - ("inspector.Session" | "inspector/promises.Session", "post") => Some(3), - ( - "process", - "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", - ) => Some(1), - ("process", "hasUncaughtExceptionCaptureCallback") => Some(0), - ("fs", "_toUnixTimestamp") => Some(1), - ("util", "isArray") => Some(1), - ("util", "debug" | "debuglog" | "inherits") => Some(2), - ("console", "context") => Some(1), - ("console", "createTask") => Some(0), - ("util", "MIMEParams") => Some(0), - ("util", "MIMEType") => Some(1), - ("sea", "isSea" | "getAssetKeys") => Some(0), - ("sea", "getRawAsset") => Some(1), - ("sea", "getAsset" | "getAssetAsBlob") => Some(2), - ("stream", "pipeline" | "compose") => Some(0), - ("stream", "finished") => Some(3), - ( - "stream", - "duplexPair" - | "isDisturbed" - | "isErrored" - | "isReadable" - | "isWritable" - | "getDefaultHighWaterMark" - | "_isArrayBufferView" - | "_isUint8Array" - | "_uint8ArrayToBuffer" - | "isDestroyed", - ) => Some(1), - ("stream", "setDefaultHighWaterMark" | "addAbortSignal") => Some(2), - ("net", "connect" | "createConnection") => Some(3), - ("net", "createServer" | "Server") => Some(2), - ("net", "Socket") => Some(1), - ("net", "BlockList" | "SocketAddress") => Some(0), - // #3720: `http2.performServerHandshake(socket[, options])` — length 1. - ("http2", "performServerHandshake") => Some(1), - ("http2", "getDefaultSettings") => Some(0), - ("http2", "getPackedSettings" | "getUnpackedSettings") => Some(1), - // #3905: Node `.length` — connect(authority,options,listener)=3, - // createServer(options,handler)=2. - ("http2", "connect") => Some(3), - ("http2", "createServer" | "createSecureServer") => Some(2), - ("http", "OutgoingMessage") => Some(1), - // #4904: Node `.length` — Agent(options)=1, ClientRequest(input, - // options, cb)=3, IncomingMessage(socket)=1, ServerResponse(req)=1. - ("http", "Agent" | "IncomingMessage" | "ServerResponse") => Some(1), - ("http", "ClientRequest") => Some(3), - // #3697: node:https module-level exports (Node `.length`). - ("https", "request") => Some(0), - ("https", "get") => Some(3), - ("https", "Agent") => Some(1), - // #4904: http twins of the https entries above. - ("http", "request") => Some(0), - ("http", "get") => Some(3), - ("stream", "destroy") => Some(2), - // #3712: node:http module-level helper exports. - ("http", "validateHeaderName" | "validateHeaderValue") => Some(2), - ("http", "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv") => Some(1), - ("http", "_connectionListener") => Some(1), - ("module", "register" | "registerHooks") => Some(1), - // #3904: modern V8 diagnostics/profiler exports (Node .length values). - ("v8", "getCppHeapStatistics") => Some(0), - ( - "v8", - "getHeapSnapshot" - | "isStringOneByteRepresentation" - | "queryObjects" - | "startCpuProfile", - ) => Some(1), - ("v8", "writeHeapSnapshot") => Some(2), - // #3906: implemented top-level v8 helpers reachable as bound callables. - ("v8", "serialize" | "deserialize") => Some(1), - ( - "v8", - "getHeapStatistics" - | "getHeapSpaceStatistics" - | "getHeapCodeStatistics" - | "cachedDataVersionTag" - | "GCProfiler", - ) => Some(0), - // #3127/#3128/#3130/#3284: node:vm no-flag export lengths. - ("vm", "Script") => Some(1), - ("vm", "Module") => Some(1), - ("vm", "SourceTextModule") => Some(1), - ("vm", "SyntheticModule") => Some(2), - ("vm", "createContext" | "measureMemory") => Some(0), - ("vm", "createScript" | "runInThisContext" | "compileFunction") => Some(2), - ("vm", "runInContext" | "runInNewContext") => Some(3), - ("vm", "isContext") => Some(1), - ("net", "_normalizeArgs") => Some(1), - ("net", "_createServerHandle") => Some(5), - ("domain", "Domain" | "createDomain" | "create") => Some(0), - ("util", "diff") => Some(2), - ("dns" | "dns/promises", "Resolver") => Some(0), - ("fs", "ReadStream" | "WriteStream") => Some(2), - ("fs", "Utf8Stream") => Some(0), - ("fs", "Dir" | "Dirent") => Some(3), - ("fs", "Stats") => Some(18), - ("fs", "mkdtempDisposableSync") => Some(2), - ("fs", "openAsBlob") => Some(1), - ("events", "init") => Some(1), - ("repl", "Recoverable") => Some(1), - ("repl", "REPLServer" | "start") => Some(6), - ("wasi", "WASI") => Some(0), - ("perf_hooks", "Performance") => Some(0), - ("perf_hooks", "PerformanceEntry") => Some(0), - ("perf_hooks", "PerformanceMark") => Some(1), - ("perf_hooks", "PerformanceMeasure") => Some(0), - ("perf_hooks", "PerformanceObserver") => Some(1), - ("perf_hooks", "PerformanceObserverEntryList") => Some(0), - ("perf_hooks", "PerformanceResourceTiming") => Some(0), - // #3119/#3126/#3263 node:module helpers. - ("module", "createRequire") => Some(1), - ("module", "Module") => Some(0), - ("module", "enableCompileCache") => Some(1), - ("module", "flushCompileCache") => Some(0), - ("module", "getCompileCacheDir") => Some(0), - ("module", "getSourceMapsSupport") => Some(0), - ("module", "_findPath") => Some(3), - ("module", "_initPaths") => Some(0), - ("module", "_load") => Some(3), - ("module", "_nodeModulePaths") => Some(1), - ("module", "_preloadModules") => Some(1), - ("module", "_resolveFilename") => Some(4), - ("module", "_resolveLookupPaths") => Some(2), - ("module", "setSourceMapsSupport") => Some(1), - ("module", "stripTypeScriptTypes") => Some(1), - ("module", "syncBuiltinESMExports") => Some(0), - ("module", "runMain") => Some(0), - ("tls", "connect") => Some(4), - ("tls", "createServer" | "Server") => Some(2), - ("tls", "TLSSocket") => Some(2), - ("child_process", "_forkChild") => Some(2), - _ => None, - } -} - extern "C" fn sqlite_statement_sync_constructor_thunk( _closure: *const crate::closure::ClosureHeader, ) -> f64 { @@ -1432,7 +1171,7 @@ fn attach_tls_secure_context_prototype(constructor_value: f64) { crate::tls::attach_secure_context_constructor_prototype(constructor_value); } -const TLS_SOCKET_PROTOTYPE_METHODS: &[(&str, u32)] = &[ +pub(super) const TLS_SOCKET_PROTOTYPE_METHODS: &[(&str, u32)] = &[ ("setKeyCert", 1), ("getSharedSigalgs", 0), ("getX509Certificate", 0), @@ -2110,487 +1849,3 @@ pub(crate) unsafe fn nm_attach_util( } value } - -/// Sorted (module → sorted (prop, arity)) table replacing the ~15 KB compiled -/// arity ladder. Same oracle-test scheme as `CALLABLE_EXPORT_TABLE`. -static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ - ( - "async_hooks", - &[ - ("AsyncLocalStorage", 0), - ("AsyncResource", 1), - ("createHook", 1), - ("executionAsyncId", 0), - ("executionAsyncResource", 0), - ("triggerAsyncId", 0), - ], - ), - ( - "bun:ffi", - &[ - ("CFunction", 1), - ("CString", 1), - ("JSCallback", 1), - ("dlopen", 2), - ("linkSymbols", 1), - ("ptr", 1), - ("read", 0), - ("toArrayBuffer", 3), - ("toBuffer", 3), - ("viewSource", 2), - ], - ), - ("child_process", &[("_forkChild", 2)]), - ( - "cluster", - &[ - ("Worker", 1), - ("addListener", 2), - ("disconnect", 1), - ("emit", 1), - ("eventNames", 0), - ("fork", 1), - ("getMaxListeners", 0), - ("listenerCount", 2), - ("listeners", 1), - ("off", 2), - ("on", 2), - ("once", 2), - ("prependListener", 2), - ("prependOnceListener", 2), - ("rawListeners", 1), - ("removeAllListeners", 1), - ("removeListener", 2), - ("setMaxListeners", 1), - ("setupMaster", 1), - ("setupPrimary", 1), - ], - ), - ("console", &[("context", 1), ("createTask", 0)]), - ( - "crypto", - &[ - ("Cipheriv", 4), - ("Decipheriv", 4), - ("DiffieHellman", 4), - ("DiffieHellmanGroup", 1), - ("KeyObject", 2), - ("X509Certificate", 1), - ("argon2", 3), - ("argon2Sync", 2), - ("checkPrime", 1), - ("checkPrimeSync", 1), - ("decapsulate", 3), - ("diffieHellman", 2), - ("encapsulate", 2), - ("generateKey", 3), - ("generateKeyPair", 3), - ("generateKeyPairSync", 2), - ("generateKeySync", 2), - ("generatePrime", 3), - ("generatePrimeSync", 1), - ("hkdf", 6), - ("hkdfSync", 5), - ("scrypt", 4), - ("scryptSync", 3), - ("secureHeapUsed", 0), - ("setFips", 1), - ], - ), - ("crypto.KeyObject", &[("from", 1)]), - ("dns", &[("Resolver", 0)]), - ("dns/promises", &[("Resolver", 0)]), - ( - "domain", - &[("Domain", 0), ("create", 0), ("createDomain", 0)], - ), - ( - "events", - &[ - ("EventEmitter", 1), - ("EventEmitterAsyncResource", 0), - ("addAbortListener", 2), - ("getEventListeners", 2), - ("getMaxListeners", 1), - ("init", 1), - ("listenerCount", 2), - ("on", 2), - ("once", 2), - ("setMaxListeners", 0), - ], - ), - ( - "fs", - &[ - ("Dir", 3), - ("Dirent", 3), - ("ReadStream", 2), - ("Stats", 18), - ("Utf8Stream", 0), - ("WriteStream", 2), - ("_toUnixTimestamp", 1), - ("mkdtempDisposableSync", 2), - ("openAsBlob", 1), - ], - ), - ( - "http", - &[ - ("Agent", 1), - ("ClientRequest", 3), - ("IncomingMessage", 1), - ("OutgoingMessage", 1), - ("ServerResponse", 1), - ("_connectionListener", 1), - ("get", 3), - ("request", 0), - ("setGlobalProxyFromEnv", 1), - ("setMaxIdleHTTPParsers", 1), - ("validateHeaderName", 2), - ("validateHeaderValue", 2), - ], - ), - ( - "http2", - &[ - ("connect", 3), - ("createSecureServer", 2), - ("createServer", 2), - ("getDefaultSettings", 0), - ("getPackedSettings", 1), - ("getUnpackedSettings", 1), - ("performServerHandshake", 1), - ], - ), - ("https", &[("Agent", 1), ("get", 3), ("request", 0)]), - ( - "inspector.DOMStorage", - &[ - ("domStorageItemAdded", 1), - ("domStorageItemRemoved", 1), - ("domStorageItemUpdated", 1), - ("domStorageItemsCleared", 1), - ("registerStorage", 1), - ], - ), - ( - "inspector.Network", - &[ - ("dataReceived", 1), - ("dataSent", 1), - ("loadingFailed", 1), - ("loadingFinished", 1), - ("requestWillBeSent", 1), - ("responseReceived", 1), - ("webSocketClosed", 1), - ("webSocketCreated", 1), - ("webSocketHandshakeResponseReceived", 1), - ], - ), - ("inspector.NetworkResources", &[("put", 1)]), - ( - "inspector.Session", - &[ - ("connect", 0), - ("connectToMainThread", 0), - ("disconnect", 0), - ("post", 3), - ], - ), - ("inspector/promises.Session", &[("post", 3)]), - ( - "module", - &[ - ("Module", 0), - ("_findPath", 3), - ("_initPaths", 0), - ("_load", 3), - ("_nodeModulePaths", 1), - ("_preloadModules", 1), - ("_resolveFilename", 4), - ("_resolveLookupPaths", 2), - ("createRequire", 1), - ("enableCompileCache", 1), - ("flushCompileCache", 0), - ("getCompileCacheDir", 0), - ("getSourceMapsSupport", 0), - ("register", 1), - ("registerHooks", 1), - ("runMain", 0), - ("setSourceMapsSupport", 1), - ("stripTypeScriptTypes", 1), - ("syncBuiltinESMExports", 0), - ], - ), - ( - "net", - &[ - ("BlockList", 0), - ("Server", 2), - ("Socket", 1), - ("SocketAddress", 0), - ("_createServerHandle", 5), - ("_normalizeArgs", 1), - ("connect", 3), - ("createConnection", 3), - ("createServer", 2), - ], - ), - ("node-pty", &[("spawn", 3)]), - ( - "perf_hooks", - &[ - ("Performance", 0), - ("PerformanceEntry", 0), - ("PerformanceMark", 1), - ("PerformanceMeasure", 0), - ("PerformanceObserver", 1), - ("PerformanceObserverEntryList", 0), - ("PerformanceResourceTiming", 0), - ], - ), - ( - "process", - &[ - ("_debugEnd", 0), - ("_debugProcess", 0), - ("_fatalException", 2), - ("_getActiveHandles", 0), - ("_getActiveRequests", 0), - ("_kill", 0), - ("_linkedBinding", 1), - ("_rawDebug", 0), - ("_startProfilerIdleNotifier", 0), - ("_stopProfilerIdleNotifier", 0), - ("_tickCallback", 0), - ("addUncaughtExceptionCaptureCallback", 1), - ("binding", 1), - ("dlopen", 0), - ("execve", 1), - ("hasUncaughtExceptionCaptureCallback", 0), - ("openStdin", 0), - ("reallyExit", 0), - ("ref", 1), - ("setSourceMapsEnabled", 1), - ("setUncaughtExceptionCaptureCallback", 1), - ("unref", 1), - ], - ), - ( - "querystring", - &[ - ("escape", 1), - ("parse", 4), - ("stringify", 4), - ("unescape", 2), - ("unescapeBuffer", 2), - ], - ), - ( - "repl", - &[("REPLServer", 6), ("Recoverable", 1), ("start", 6)], - ), - ( - "sea", - &[ - ("getAsset", 2), - ("getAssetAsBlob", 2), - ("getAssetKeys", 0), - ("getRawAsset", 1), - ("isSea", 0), - ], - ), - ( - "stream", - &[ - ("_isArrayBufferView", 1), - ("_isUint8Array", 1), - ("_uint8ArrayToBuffer", 1), - ("addAbortSignal", 2), - ("compose", 0), - ("destroy", 2), - ("duplexPair", 1), - ("finished", 3), - ("getDefaultHighWaterMark", 1), - ("isDestroyed", 1), - ("isDisturbed", 1), - ("isErrored", 1), - ("isReadable", 1), - ("isWritable", 1), - ("pipeline", 0), - ("setDefaultHighWaterMark", 2), - ], - ), - ( - "tls", - &[ - ("SecureContext", 1), - ("Server", 2), - ("TLSSocket", 2), - ("checkServerIdentity", 2), - ("connect", 4), - ("convertALPNProtocols", 2), - ("createSecureContext", 1), - ("createServer", 2), - ("getCACertificates", 1), - ("getCiphers", 0), - ("setDefaultCACertificates", 1), - ], - ), - ( - "url", - &[ - ("URL", 1), - ("URLPattern", 0), - ("Url", 0), - ("resolveObject", 2), - ], - ), - ( - "util", - &[ - ("MIMEParams", 0), - ("MIMEType", 1), - ("debug", 2), - ("debuglog", 2), - ("diff", 2), - ("inherits", 2), - ("isArray", 1), - ], - ), - ( - "v8", - &[ - ("GCProfiler", 0), - ("cachedDataVersionTag", 0), - ("deserialize", 1), - ("getCppHeapStatistics", 0), - ("getHeapCodeStatistics", 0), - ("getHeapSnapshot", 1), - ("getHeapSpaceStatistics", 0), - ("getHeapStatistics", 0), - ("isStringOneByteRepresentation", 1), - ("queryObjects", 1), - ("serialize", 1), - ("startCpuProfile", 1), - ("writeHeapSnapshot", 2), - ], - ), - ( - "vm", - &[ - ("Module", 1), - ("Script", 1), - ("SourceTextModule", 1), - ("SyntheticModule", 2), - ("compileFunction", 2), - ("createContext", 0), - ("createScript", 2), - ("isContext", 1), - ("measureMemory", 0), - ("runInContext", 3), - ("runInNewContext", 3), - ("runInThisContext", 2), - ], - ), - ("wasi", &[("WASI", 0)]), -]; - -fn native_callable_export_arity(module: &str, prop: &str) -> Option { - let mi = CALLABLE_EXPORT_ARITY_TABLE - .binary_search_by(|(m, _)| (*m).cmp(module)) - .ok()?; - let row = CALLABLE_EXPORT_ARITY_TABLE[mi].1; - let pi = row.binary_search_by(|(p, _)| (*p).cmp(prop)).ok()?; - Some(row[pi].1) -} - -#[cfg(test)] -mod callable_export_arity_table_tests { - use super::*; - - #[test] - fn arity_table_is_sorted() { - for w in CALLABLE_EXPORT_ARITY_TABLE.windows(2) { - assert!(w[0].0 < w[1].0); - } - for (_, row) in CALLABLE_EXPORT_ARITY_TABLE { - for w in row.windows(2) { - assert!(w[0].0 < w[1].0); - } - } - } - - #[test] - fn arity_table_matches_reference_exhaustively() { - let source = include_str!("callable_exports.rs"); - let mut literals: Vec<&str> = Vec::new(); - let mut rest = source; - while let Some(start) = rest.find('"') { - let after = &rest[start + 1..]; - let Some(end) = after.find('"') else { break }; - let lit = &after[..end]; - if !lit.is_empty() && lit.len() < 64 && !lit.contains('\n') { - literals.push(lit); - } - rest = &after[end + 1..]; - } - literals.sort_unstable(); - literals.dedup(); - for module in &literals { - for prop in &literals { - assert_eq!( - native_callable_export_arity(module, prop), - native_callable_export_arity_reference(module, prop), - "divergence at ({module}, {prop})" - ); - } - } - } - - #[test] - fn tls_constructor_prototypes_match_node_parent_classes() { - let server = bound_native_callable_export_value("tls", "Server"); - let server_addr = (server.to_bits() & crate::value::POINTER_MASK) as usize; - let server_proto = crate::closure::closure_get_dynamic_prop(server_addr, "prototype"); - assert!(tls_constructor_prototype_is_instance_of( - server_proto, - "EventEmitter" - )); - assert_eq!( - crate::object::js_instanceof(server_proto, 0xFFFF_0076).to_bits(), - crate::value::TAG_TRUE - ); - let event_emitter = bound_native_callable_export_value("events", "EventEmitter"); - assert_eq!( - crate::object::js_instanceof_dynamic(server_proto, event_emitter).to_bits(), - crate::value::TAG_TRUE - ); - - let socket = bound_native_callable_export_value("tls", "TLSSocket"); - let socket_addr = (socket.to_bits() & crate::value::POINTER_MASK) as usize; - let socket_proto = crate::closure::closure_get_dynamic_prop(socket_addr, "prototype"); - assert!(tls_constructor_prototype_is_instance_of( - socket_proto, - "Duplex" - )); - let socket_proto_obj = - JSValue::from_bits(socket_proto.to_bits()).as_pointer::(); - for &(name, length) in TLS_SOCKET_PROTOTYPE_METHODS { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let method = crate::object::js_object_get_field_by_name(socket_proto_obj, key); - let method_addr = method.as_pointer::() as usize; - assert!(crate::closure::is_closure_ptr(method_addr), "{name}"); - assert_eq!(builtin_closure_length(method_addr), Some(length), "{name}"); - } - assert_eq!( - crate::object::js_instanceof(socket_proto, 0xFFFF_0073).to_bits(), - crate::value::TAG_TRUE - ); - let duplex = bound_native_callable_export_value("stream", "Duplex"); - assert_eq!( - crate::object::js_instanceof_dynamic(socket_proto, duplex).to_bits(), - crate::value::TAG_TRUE - ); - } -} diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index 2532c6961c..f307be49ae 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -1,3 +1,6 @@ +use super::constants_tables::{ + crypto_const_lookup, os_errno_const_lookup, os_signal_const_lookup, zlib_const_lookup, +}; use super::*; use std::sync::atomic::Ordering; @@ -1034,1032 +1037,6 @@ pub(crate) unsafe fn get_native_module_constant( } } -/// Sorted table for `zlib_const`: block-level `#[cfg]` predicates mirrored -/// verbatim as per-platform tables; per-arm cfg chains composed with -/// `not(any(..))` so first-match-wins semantics survive; every value kept -/// as its verbatim const expression cast to f64 (uniform row type across -/// platforms). The verbatim reference + literal-universe oracle below runs -/// on EVERY platform in CI, covering the sides not testable locally. -static ZLIB_CONST_TABLE: &[(&str, f64)] = &[ - ("BROTLI_DECODE", (8) as f64), - ("BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES", (-30) as f64), - ("BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP", (-25) as f64), - ("BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES", (-21) as f64), - ("BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1", (-26) as f64), - ("BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2", (-27) as f64), - ("BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS", (-22) as f64), - ("BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET", (-19) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1", (-9) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2", (-10) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_CL_SPACE", (-6) as f64), - ( - "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT", - (-8) as f64, - ), - ("BROTLI_DECODER_ERROR_FORMAT_DICTIONARY", (-12) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_DISTANCE", (-16) as f64), - ( - "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE", - (-3) as f64, - ), - ("BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE", (-1) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE", (-7) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_PADDING_1", (-14) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_PADDING_2", (-15) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_RESERVED", (-2) as f64), - ( - "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET", - (-4) as f64, - ), - ( - "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME", - (-5) as f64, - ), - ("BROTLI_DECODER_ERROR_FORMAT_TRANSFORM", (-11) as f64), - ("BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS", (-13) as f64), - ("BROTLI_DECODER_ERROR_INVALID_ARGUMENTS", (-20) as f64), - ("BROTLI_DECODER_ERROR_UNREACHABLE", (-31) as f64), - ("BROTLI_DECODER_NEEDS_MORE_INPUT", (2) as f64), - ("BROTLI_DECODER_NEEDS_MORE_OUTPUT", (3) as f64), - ("BROTLI_DECODER_NO_ERROR", (0) as f64), - ( - "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION", - (0) as f64, - ), - ("BROTLI_DECODER_PARAM_LARGE_WINDOW", (1) as f64), - ("BROTLI_DECODER_RESULT_ERROR", (0) as f64), - ("BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT", (2) as f64), - ("BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT", (3) as f64), - ("BROTLI_DECODER_RESULT_SUCCESS", (1) as f64), - ("BROTLI_DECODER_SUCCESS", (1) as f64), - ("BROTLI_DEFAULT_MODE", (0) as f64), - ("BROTLI_DEFAULT_QUALITY", (11) as f64), - ("BROTLI_DEFAULT_WINDOW", (22) as f64), - ("BROTLI_ENCODE", (9) as f64), - ("BROTLI_LARGE_MAX_WINDOW_BITS", (30) as f64), - ("BROTLI_MAX_INPUT_BLOCK_BITS", (24) as f64), - ("BROTLI_MAX_QUALITY", (11) as f64), - ("BROTLI_MAX_WINDOW_BITS", (24) as f64), - ("BROTLI_MIN_INPUT_BLOCK_BITS", (16) as f64), - ("BROTLI_MIN_QUALITY", (0) as f64), - ("BROTLI_MIN_WINDOW_BITS", (10) as f64), - ("BROTLI_MODE_FONT", (2) as f64), - ("BROTLI_MODE_GENERIC", (0) as f64), - ("BROTLI_MODE_TEXT", (1) as f64), - ("BROTLI_OPERATION_EMIT_METADATA", (3) as f64), - ("BROTLI_OPERATION_FINISH", (2) as f64), - ("BROTLI_OPERATION_FLUSH", (1) as f64), - ("BROTLI_OPERATION_PROCESS", (0) as f64), - ("BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING", (4) as f64), - ("BROTLI_PARAM_LARGE_WINDOW", (6) as f64), - ("BROTLI_PARAM_LGBLOCK", (3) as f64), - ("BROTLI_PARAM_LGWIN", (2) as f64), - ("BROTLI_PARAM_MODE", (0) as f64), - ("BROTLI_PARAM_NDIRECT", (8) as f64), - ("BROTLI_PARAM_NPOSTFIX", (7) as f64), - ("BROTLI_PARAM_QUALITY", (1) as f64), - ("BROTLI_PARAM_SIZE_HINT", (5) as f64), - ("DEFLATE", (1) as f64), - ("DEFLATERAW", (5) as f64), - ("GUNZIP", (4) as f64), - ("GZIP", (3) as f64), - ("INFLATE", (2) as f64), - ("INFLATERAW", (6) as f64), - ("UNZIP", (7) as f64), - ("ZLIB_VERNUM", (0x1310) as f64), - ("ZSTD_CLEVEL_DEFAULT", (3) as f64), - ("ZSTD_COMPRESS", (10) as f64), - ("ZSTD_DECOMPRESS", (11) as f64), - ("ZSTD_MAXCLEVEL", (22) as f64), - ("ZSTD_MINCLEVEL", (-131072) as f64), - ("ZSTD_btlazy2", (6) as f64), - ("ZSTD_btopt", (7) as f64), - ("ZSTD_btultra", (8) as f64), - ("ZSTD_btultra2", (9) as f64), - ("ZSTD_c_chainLog", (103) as f64), - ("ZSTD_c_checksumFlag", (201) as f64), - ("ZSTD_c_compressionLevel", (100) as f64), - ("ZSTD_c_contentSizeFlag", (200) as f64), - ("ZSTD_c_dictIDFlag", (202) as f64), - ("ZSTD_c_enableLongDistanceMatching", (160) as f64), - ("ZSTD_c_hashLog", (102) as f64), - ("ZSTD_c_jobSize", (401) as f64), - ("ZSTD_c_ldmBucketSizeLog", (163) as f64), - ("ZSTD_c_ldmHashLog", (161) as f64), - ("ZSTD_c_ldmHashRateLog", (164) as f64), - ("ZSTD_c_ldmMinMatch", (162) as f64), - ("ZSTD_c_minMatch", (105) as f64), - ("ZSTD_c_nbWorkers", (400) as f64), - ("ZSTD_c_overlapLog", (402) as f64), - ("ZSTD_c_searchLog", (104) as f64), - ("ZSTD_c_strategy", (107) as f64), - ("ZSTD_c_targetLength", (106) as f64), - ("ZSTD_c_windowLog", (101) as f64), - ("ZSTD_d_windowLogMax", (100) as f64), - ("ZSTD_dfast", (2) as f64), - ("ZSTD_e_continue", (0) as f64), - ("ZSTD_e_end", (2) as f64), - ("ZSTD_e_flush", (1) as f64), - ("ZSTD_error_GENERIC", (1) as f64), - ("ZSTD_error_checksum_wrong", (22) as f64), - ("ZSTD_error_corruption_detected", (20) as f64), - ("ZSTD_error_dictionaryCreation_failed", (34) as f64), - ("ZSTD_error_dictionary_corrupted", (30) as f64), - ("ZSTD_error_dictionary_wrong", (32) as f64), - ("ZSTD_error_dstBuffer_null", (74) as f64), - ("ZSTD_error_dstSize_tooSmall", (70) as f64), - ("ZSTD_error_frameParameter_unsupported", (14) as f64), - ("ZSTD_error_frameParameter_windowTooLarge", (16) as f64), - ("ZSTD_error_init_missing", (62) as f64), - ("ZSTD_error_literals_headerWrong", (24) as f64), - ("ZSTD_error_maxSymbolValue_tooLarge", (46) as f64), - ("ZSTD_error_maxSymbolValue_tooSmall", (48) as f64), - ("ZSTD_error_memory_allocation", (64) as f64), - ("ZSTD_error_noForwardProgress_destFull", (80) as f64), - ("ZSTD_error_noForwardProgress_inputEmpty", (82) as f64), - ("ZSTD_error_no_error", (0) as f64), - ("ZSTD_error_parameter_combination_unsupported", (41) as f64), - ("ZSTD_error_parameter_outOfBound", (42) as f64), - ("ZSTD_error_parameter_unsupported", (40) as f64), - ("ZSTD_error_prefix_unknown", (10) as f64), - ("ZSTD_error_srcSize_wrong", (72) as f64), - ("ZSTD_error_stabilityCondition_notRespected", (50) as f64), - ("ZSTD_error_stage_wrong", (60) as f64), - ("ZSTD_error_tableLog_tooLarge", (44) as f64), - ("ZSTD_error_version_unsupported", (12) as f64), - ("ZSTD_error_workSpace_tooSmall", (66) as f64), - ("ZSTD_fast", (1) as f64), - ("ZSTD_greedy", (3) as f64), - ("ZSTD_lazy", (4) as f64), - ("ZSTD_lazy2", (5) as f64), - ("Z_BEST_COMPRESSION", (9) as f64), - ("Z_BEST_SPEED", (1) as f64), - ("Z_BLOCK", (5) as f64), - ("Z_BUF_ERROR", (-5) as f64), - ("Z_DATA_ERROR", (-3) as f64), - ("Z_DEFAULT_CHUNK", (16384) as f64), - ("Z_DEFAULT_COMPRESSION", (-1) as f64), - ("Z_DEFAULT_LEVEL", (-1) as f64), - ("Z_DEFAULT_MEMLEVEL", (8) as f64), - ("Z_DEFAULT_STRATEGY", (0) as f64), - ("Z_DEFAULT_WINDOWBITS", (15) as f64), - ("Z_ERRNO", (-1) as f64), - ("Z_FILTERED", (1) as f64), - ("Z_FINISH", (4) as f64), - ("Z_FIXED", (4) as f64), - ("Z_FULL_FLUSH", (3) as f64), - ("Z_HUFFMAN_ONLY", (2) as f64), - ("Z_MAX_CHUNK", (0x7fff_ffff) as f64), - ("Z_MAX_LEVEL", (9) as f64), - ("Z_MAX_MEMLEVEL", (9) as f64), - ("Z_MAX_WINDOWBITS", (15) as f64), - ("Z_MEM_ERROR", (-4) as f64), - ("Z_MIN_CHUNK", (64) as f64), - ("Z_MIN_LEVEL", (-1) as f64), - ("Z_MIN_MEMLEVEL", (1) as f64), - ("Z_MIN_WINDOWBITS", (8) as f64), - ("Z_NEED_DICT", (2) as f64), - ("Z_NO_COMPRESSION", (0) as f64), - ("Z_NO_FLUSH", (0) as f64), - ("Z_OK", (0) as f64), - ("Z_PARTIAL_FLUSH", (1) as f64), - ("Z_RLE", (3) as f64), - ("Z_STREAM_END", (1) as f64), - ("Z_STREAM_ERROR", (-2) as f64), - ("Z_SYNC_FLUSH", (2) as f64), - ("Z_VERSION_ERROR", (-6) as f64), -]; - -fn zlib_const_lookup(prop: &str) -> Option { - let i = ZLIB_CONST_TABLE - .binary_search_by(|(n, _)| (*n).cmp(prop)) - .ok()?; - Some(ZLIB_CONST_TABLE[i].1) -} - -#[cfg(test)] -#[allow(clippy::unnecessary_cast)] -fn zlib_const_reference(prop: &str) -> Option { - let v: i64 = match prop { - // Compression levels - "Z_NO_COMPRESSION" => 0, - "Z_BEST_SPEED" => 1, - "Z_BEST_COMPRESSION" => 9, - "Z_DEFAULT_COMPRESSION" => -1, - // Compression strategies - "Z_FILTERED" => 1, - "Z_HUFFMAN_ONLY" => 2, - "Z_RLE" => 3, - "Z_FIXED" => 4, - "Z_DEFAULT_STRATEGY" => 0, - "ZLIB_VERNUM" => 0x1310, - // Flush values - "Z_NO_FLUSH" => 0, - "Z_PARTIAL_FLUSH" => 1, - "Z_SYNC_FLUSH" => 2, - "Z_FULL_FLUSH" => 3, - "Z_FINISH" => 4, - "Z_BLOCK" => 5, - // Return codes - "Z_OK" => 0, - "Z_STREAM_END" => 1, - "Z_NEED_DICT" => 2, - "Z_ERRNO" => -1, - "Z_STREAM_ERROR" => -2, - "Z_DATA_ERROR" => -3, - "Z_MEM_ERROR" => -4, - "Z_BUF_ERROR" => -5, - "Z_VERSION_ERROR" => -6, - // Min/Max window bits and memlevel - "Z_MIN_WINDOWBITS" => 8, - "Z_MAX_WINDOWBITS" => 15, - "Z_DEFAULT_WINDOWBITS" => 15, - "Z_MIN_CHUNK" => 64, - "Z_MAX_CHUNK" => 0x7fff_ffff, - "Z_DEFAULT_CHUNK" => 16384, - "Z_MIN_MEMLEVEL" => 1, - "Z_MAX_MEMLEVEL" => 9, - "Z_DEFAULT_MEMLEVEL" => 8, - "Z_MIN_LEVEL" => -1, - "Z_MAX_LEVEL" => 9, - "Z_DEFAULT_LEVEL" => -1, - // Mode (zlib stream modes — used by zlib.createDeflate etc.) - "DEFLATE" => 1, - "INFLATE" => 2, - "GZIP" => 3, - "GUNZIP" => 4, - "DEFLATERAW" => 5, - "INFLATERAW" => 6, - "UNZIP" => 7, - "BROTLI_DECODE" => 8, - "BROTLI_ENCODE" => 9, - "ZSTD_COMPRESS" => 10, - "ZSTD_DECOMPRESS" => 11, - // Brotli operation/parameter constants — match Node's - // `zlib.constants` exactly (these are the BrotliEncoder/ - // BrotliDecoder parameter ids the underlying brotli library - // exposes). - "BROTLI_OPERATION_PROCESS" => 0, - "BROTLI_OPERATION_FLUSH" => 1, - "BROTLI_OPERATION_FINISH" => 2, - "BROTLI_OPERATION_EMIT_METADATA" => 3, - "BROTLI_PARAM_MODE" => 0, - "BROTLI_MODE_GENERIC" => 0, - "BROTLI_MODE_TEXT" => 1, - "BROTLI_MODE_FONT" => 2, - "BROTLI_DEFAULT_MODE" => 0, - "BROTLI_PARAM_QUALITY" => 1, - "BROTLI_MIN_QUALITY" => 0, - "BROTLI_MAX_QUALITY" => 11, - "BROTLI_DEFAULT_QUALITY" => 11, - "BROTLI_PARAM_LGWIN" => 2, - "BROTLI_MIN_WINDOW_BITS" => 10, - "BROTLI_MAX_WINDOW_BITS" => 24, - "BROTLI_LARGE_MAX_WINDOW_BITS" => 30, - "BROTLI_DEFAULT_WINDOW" => 22, - "BROTLI_PARAM_LGBLOCK" => 3, - "BROTLI_MIN_INPUT_BLOCK_BITS" => 16, - "BROTLI_MAX_INPUT_BLOCK_BITS" => 24, - "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING" => 4, - "BROTLI_PARAM_SIZE_HINT" => 5, - "BROTLI_PARAM_LARGE_WINDOW" => 6, - "BROTLI_PARAM_NPOSTFIX" => 7, - "BROTLI_PARAM_NDIRECT" => 8, - "BROTLI_DECODER_RESULT_ERROR" => 0, - "BROTLI_DECODER_RESULT_SUCCESS" => 1, - "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT" => 2, - "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT" => 3, - "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION" => 0, - "BROTLI_DECODER_PARAM_LARGE_WINDOW" => 1, - // Zstd parameter ids — match Node's `zlib.constants`. - "ZSTD_e_continue" => 0, - "ZSTD_e_flush" => 1, - "ZSTD_e_end" => 2, - "ZSTD_fast" => 1, - "ZSTD_dfast" => 2, - "ZSTD_greedy" => 3, - "ZSTD_lazy" => 4, - "ZSTD_lazy2" => 5, - "ZSTD_btlazy2" => 6, - "ZSTD_btopt" => 7, - "ZSTD_btultra" => 8, - "ZSTD_btultra2" => 9, - "ZSTD_c_compressionLevel" => 100, - "ZSTD_c_windowLog" => 101, - "ZSTD_c_hashLog" => 102, - "ZSTD_c_chainLog" => 103, - "ZSTD_c_searchLog" => 104, - "ZSTD_c_minMatch" => 105, - "ZSTD_c_targetLength" => 106, - "ZSTD_c_strategy" => 107, - "ZSTD_c_enableLongDistanceMatching" => 160, - "ZSTD_c_ldmHashLog" => 161, - "ZSTD_c_ldmMinMatch" => 162, - "ZSTD_c_ldmBucketSizeLog" => 163, - "ZSTD_c_ldmHashRateLog" => 164, - "ZSTD_c_contentSizeFlag" => 200, - "ZSTD_c_checksumFlag" => 201, - "ZSTD_c_dictIDFlag" => 202, - "ZSTD_c_nbWorkers" => 400, - "ZSTD_c_jobSize" => 401, - "ZSTD_c_overlapLog" => 402, - "ZSTD_d_windowLogMax" => 100, - "ZSTD_CLEVEL_DEFAULT" => 3, - "ZSTD_MINCLEVEL" => -131072, - "ZSTD_MAXCLEVEL" => 22, - // #3677: Brotli decoder result/error codes Node exposes on - // `zlib.constants` (the BrotliDecoderResult / BrotliDecoderErrorCode - // enums). Required so `Object.keys(zlib.constants)` enumeration - // matches Node's full set and every enumerated key reads its value. - "BROTLI_DECODER_NO_ERROR" => 0, - "BROTLI_DECODER_SUCCESS" => 1, - "BROTLI_DECODER_NEEDS_MORE_INPUT" => 2, - "BROTLI_DECODER_NEEDS_MORE_OUTPUT" => 3, - "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE" => -1, - "BROTLI_DECODER_ERROR_FORMAT_RESERVED" => -2, - "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE" => -3, - "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET" => -4, - "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME" => -5, - "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE" => -6, - "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE" => -7, - "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT" => -8, - "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1" => -9, - "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2" => -10, - "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM" => -11, - "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY" => -12, - "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS" => -13, - "BROTLI_DECODER_ERROR_FORMAT_PADDING_1" => -14, - "BROTLI_DECODER_ERROR_FORMAT_PADDING_2" => -15, - "BROTLI_DECODER_ERROR_FORMAT_DISTANCE" => -16, - "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET" => -19, - "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS" => -20, - "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES" => -21, - "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS" => -22, - "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP" => -25, - "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1" => -26, - "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2" => -27, - "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES" => -30, - "BROTLI_DECODER_ERROR_UNREACHABLE" => -31, - // #3677: Zstd error codes (ZSTD_ErrorCode enum) Node exposes. - "ZSTD_error_no_error" => 0, - "ZSTD_error_GENERIC" => 1, - "ZSTD_error_prefix_unknown" => 10, - "ZSTD_error_version_unsupported" => 12, - "ZSTD_error_frameParameter_unsupported" => 14, - "ZSTD_error_frameParameter_windowTooLarge" => 16, - "ZSTD_error_corruption_detected" => 20, - "ZSTD_error_checksum_wrong" => 22, - "ZSTD_error_literals_headerWrong" => 24, - "ZSTD_error_dictionary_corrupted" => 30, - "ZSTD_error_dictionary_wrong" => 32, - "ZSTD_error_dictionaryCreation_failed" => 34, - "ZSTD_error_parameter_unsupported" => 40, - "ZSTD_error_parameter_combination_unsupported" => 41, - "ZSTD_error_parameter_outOfBound" => 42, - "ZSTD_error_tableLog_tooLarge" => 44, - "ZSTD_error_maxSymbolValue_tooLarge" => 46, - "ZSTD_error_maxSymbolValue_tooSmall" => 48, - "ZSTD_error_stabilityCondition_notRespected" => 50, - "ZSTD_error_stage_wrong" => 60, - "ZSTD_error_init_missing" => 62, - "ZSTD_error_memory_allocation" => 64, - "ZSTD_error_workSpace_tooSmall" => 66, - "ZSTD_error_dstSize_tooSmall" => 70, - "ZSTD_error_srcSize_wrong" => 72, - "ZSTD_error_dstBuffer_null" => 74, - "ZSTD_error_noForwardProgress_destFull" => 80, - "ZSTD_error_noForwardProgress_inputEmpty" => 82, - _ => return None, - }; - Some(v as f64) -} - -#[cfg(test)] -mod zlib_const_table_tests { - use super::*; - #[test] - fn sorted() { - for w in ZLIB_CONST_TABLE.windows(2) { - assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); - } - } - #[test] - fn matches_reference_exhaustively() { - let source = include_str!("constants.rs"); - let mut rest = source; - while let Some(s) = rest.find('"') { - let after = &rest[s + 1..]; - let Some(e) = after.find('"') else { break }; - let lit = &after[..e]; - if !lit.is_empty() && lit.len() < 64 { - assert_eq!( - zlib_const_lookup(lit), - zlib_const_reference(lit), - "at {lit}" - ); - } - rest = &after[e + 1..]; - } - } -} - -/// Sorted table for `crypto_const`: block-level `#[cfg]` predicates mirrored -/// verbatim as per-platform tables; per-arm cfg chains composed with -/// `not(any(..))` so first-match-wins semantics survive; every value kept -/// as its verbatim const expression cast to f64 (uniform row type across -/// platforms). The verbatim reference + literal-universe oracle below runs -/// on EVERY platform in CI, covering the sides not testable locally. -static CRYPTO_CONST_TABLE: &[(&str, f64)] = &[ - ("DH_CHECK_P_NOT_PRIME", (1.0) as f64), - ("DH_CHECK_P_NOT_SAFE_PRIME", (2.0) as f64), - ("DH_NOT_SUITABLE_GENERATOR", (8.0) as f64), - ("DH_UNABLE_TO_CHECK_GENERATOR", (4.0) as f64), - ("ENGINE_METHOD_ALL", (65535.0) as f64), - ("ENGINE_METHOD_CIPHERS", (64.0) as f64), - ("ENGINE_METHOD_DH", (4.0) as f64), - ("ENGINE_METHOD_DIGESTS", (128.0) as f64), - ("ENGINE_METHOD_DSA", (2.0) as f64), - ("ENGINE_METHOD_EC", (2048.0) as f64), - ("ENGINE_METHOD_NONE", (0.0) as f64), - ("ENGINE_METHOD_PKEY_ASN1_METHS", (1024.0) as f64), - ("ENGINE_METHOD_PKEY_METHS", (512.0) as f64), - ("ENGINE_METHOD_RAND", (8.0) as f64), - ("ENGINE_METHOD_RSA", (1.0) as f64), - ("OPENSSL_VERSION_NUMBER", (811597840.0) as f64), - ("POINT_CONVERSION_COMPRESSED", (2.0) as f64), - ("POINT_CONVERSION_HYBRID", (6.0) as f64), - ("POINT_CONVERSION_UNCOMPRESSED", (4.0) as f64), - ("RSA_NO_PADDING", (3.0) as f64), - ("RSA_PKCS1_OAEP_PADDING", (4.0) as f64), - ("RSA_PKCS1_PADDING", (1.0) as f64), - ("RSA_PKCS1_PSS_PADDING", (6.0) as f64), - ("RSA_PSS_SALTLEN_AUTO", (-2.0) as f64), - ("RSA_PSS_SALTLEN_DIGEST", (-1.0) as f64), - ("RSA_PSS_SALTLEN_MAX_SIGN", (-2.0) as f64), - ("RSA_X931_PADDING", (5.0) as f64), - ("SSL_OP_ALL", (2147485776.0) as f64), - ("SSL_OP_ALLOW_NO_DHE_KEX", (1024.0) as f64), - ( - "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION", - (262144.0) as f64, - ), - ("SSL_OP_CIPHER_SERVER_PREFERENCE", (4194304.0) as f64), - ("SSL_OP_CISCO_ANYCONNECT", (32768.0) as f64), - ("SSL_OP_COOKIE_EXCHANGE", (8192.0) as f64), - ("SSL_OP_CRYPTOPRO_TLSEXT_BUG", (2147483648.0) as f64), - ("SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS", (2048.0) as f64), - ("SSL_OP_LEGACY_SERVER_CONNECT", (4.0) as f64), - ("SSL_OP_NO_COMPRESSION", (131072.0) as f64), - ("SSL_OP_NO_ENCRYPT_THEN_MAC", (524288.0) as f64), - ("SSL_OP_NO_QUERY_MTU", (4096.0) as f64), - ("SSL_OP_NO_RENEGOTIATION", (1073741824.0) as f64), - ( - "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION", - (65536.0) as f64, - ), - ("SSL_OP_NO_SSLv2", (0.0) as f64), - ("SSL_OP_NO_SSLv3", (33554432.0) as f64), - ("SSL_OP_NO_TICKET", (16384.0) as f64), - ("SSL_OP_NO_TLSv1", (67108864.0) as f64), - ("SSL_OP_NO_TLSv1_1", (268435456.0) as f64), - ("SSL_OP_NO_TLSv1_2", (134217728.0) as f64), - ("SSL_OP_NO_TLSv1_3", (536870912.0) as f64), - ("SSL_OP_PRIORITIZE_CHACHA", (2097152.0) as f64), - ("SSL_OP_TLS_ROLLBACK_BUG", (8388608.0) as f64), - ("TLS1_1_VERSION", (770.0) as f64), - ("TLS1_2_VERSION", (771.0) as f64), - ("TLS1_3_VERSION", (772.0) as f64), - ("TLS1_VERSION", (769.0) as f64), -]; - -fn crypto_const_lookup(prop: &str) -> Option { - let i = CRYPTO_CONST_TABLE - .binary_search_by(|(n, _)| (*n).cmp(prop)) - .ok()?; - Some(CRYPTO_CONST_TABLE[i].1) -} - -#[cfg(test)] -#[allow(clippy::unnecessary_cast)] -fn crypto_const_reference(prop: &str) -> Option { - match prop { - "OPENSSL_VERSION_NUMBER" => Some(811597840.0), - "SSL_OP_ALL" => Some(2147485776.0), - "SSL_OP_ALLOW_NO_DHE_KEX" => Some(1024.0), - "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION" => Some(262144.0), - "SSL_OP_CIPHER_SERVER_PREFERENCE" => Some(4194304.0), - "SSL_OP_CISCO_ANYCONNECT" => Some(32768.0), - "SSL_OP_COOKIE_EXCHANGE" => Some(8192.0), - "SSL_OP_CRYPTOPRO_TLSEXT_BUG" => Some(2147483648.0), - "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS" => Some(2048.0), - "SSL_OP_LEGACY_SERVER_CONNECT" => Some(4.0), - "SSL_OP_NO_COMPRESSION" => Some(131072.0), - "SSL_OP_NO_ENCRYPT_THEN_MAC" => Some(524288.0), - "SSL_OP_NO_QUERY_MTU" => Some(4096.0), - "SSL_OP_NO_RENEGOTIATION" => Some(1073741824.0), - "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION" => Some(65536.0), - "SSL_OP_NO_SSLv2" => Some(0.0), - "SSL_OP_NO_SSLv3" => Some(33554432.0), - "SSL_OP_NO_TICKET" => Some(16384.0), - "SSL_OP_NO_TLSv1" => Some(67108864.0), - "SSL_OP_NO_TLSv1_1" => Some(268435456.0), - "SSL_OP_NO_TLSv1_2" => Some(134217728.0), - "SSL_OP_NO_TLSv1_3" => Some(536870912.0), - "SSL_OP_PRIORITIZE_CHACHA" => Some(2097152.0), - "SSL_OP_TLS_ROLLBACK_BUG" => Some(8388608.0), - "ENGINE_METHOD_RSA" => Some(1.0), - "ENGINE_METHOD_DSA" => Some(2.0), - "ENGINE_METHOD_DH" => Some(4.0), - "ENGINE_METHOD_RAND" => Some(8.0), - "ENGINE_METHOD_EC" => Some(2048.0), - "ENGINE_METHOD_CIPHERS" => Some(64.0), - "ENGINE_METHOD_DIGESTS" => Some(128.0), - "ENGINE_METHOD_PKEY_METHS" => Some(512.0), - "ENGINE_METHOD_PKEY_ASN1_METHS" => Some(1024.0), - "ENGINE_METHOD_ALL" => Some(65535.0), - "ENGINE_METHOD_NONE" => Some(0.0), - "DH_CHECK_P_NOT_SAFE_PRIME" => Some(2.0), - "DH_CHECK_P_NOT_PRIME" => Some(1.0), - "DH_UNABLE_TO_CHECK_GENERATOR" => Some(4.0), - "DH_NOT_SUITABLE_GENERATOR" => Some(8.0), - "RSA_PKCS1_PADDING" => Some(1.0), - "RSA_NO_PADDING" => Some(3.0), - "RSA_PKCS1_OAEP_PADDING" => Some(4.0), - "RSA_X931_PADDING" => Some(5.0), - "RSA_PKCS1_PSS_PADDING" => Some(6.0), - "RSA_PSS_SALTLEN_DIGEST" => Some(-1.0), - "RSA_PSS_SALTLEN_MAX_SIGN" => Some(-2.0), - "RSA_PSS_SALTLEN_AUTO" => Some(-2.0), - "TLS1_VERSION" => Some(769.0), - "TLS1_1_VERSION" => Some(770.0), - "TLS1_2_VERSION" => Some(771.0), - "TLS1_3_VERSION" => Some(772.0), - "POINT_CONVERSION_COMPRESSED" => Some(2.0), - "POINT_CONVERSION_UNCOMPRESSED" => Some(4.0), - "POINT_CONVERSION_HYBRID" => Some(6.0), - _ => None, - } -} - -#[cfg(test)] -mod crypto_const_table_tests { - use super::*; - #[test] - fn sorted() { - for w in CRYPTO_CONST_TABLE.windows(2) { - assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); - } - } - #[test] - fn matches_reference_exhaustively() { - let source = include_str!("constants.rs"); - let mut rest = source; - while let Some(s) = rest.find('"') { - let after = &rest[s + 1..]; - let Some(e) = after.find('"') else { break }; - let lit = &after[..e]; - if !lit.is_empty() && lit.len() < 64 { - assert_eq!( - crypto_const_lookup(lit), - crypto_const_reference(lit), - "at {lit}" - ); - } - rest = &after[e + 1..]; - } - } -} - -/// Sorted table for `os_errno_const`: block-level `#[cfg]` predicates mirrored -/// verbatim as per-platform tables; per-arm cfg chains composed with -/// `not(any(..))` so first-match-wins semantics survive; every value kept -/// as its verbatim const expression cast to f64 (uniform row type across -/// platforms). The verbatim reference + literal-universe oracle below runs -/// on EVERY platform in CI, covering the sides not testable locally. -#[cfg(unix)] -static OS_ERRNO_CONST_TABLE: &[(&str, f64)] = &[ - ("E2BIG", (libc::E2BIG) as f64), - ("EACCES", (libc::EACCES) as f64), - ("EADDRINUSE", (libc::EADDRINUSE) as f64), - ("EADDRNOTAVAIL", (libc::EADDRNOTAVAIL) as f64), - ("EAFNOSUPPORT", (libc::EAFNOSUPPORT) as f64), - ("EAGAIN", (libc::EAGAIN) as f64), - ("EALREADY", (libc::EALREADY) as f64), - ("EBADF", (libc::EBADF) as f64), - ("EBADMSG", (libc::EBADMSG) as f64), - ("EBUSY", (libc::EBUSY) as f64), - ("ECANCELED", (libc::ECANCELED) as f64), - ("ECHILD", (libc::ECHILD) as f64), - ("ECONNABORTED", (libc::ECONNABORTED) as f64), - ("ECONNREFUSED", (libc::ECONNREFUSED) as f64), - ("ECONNRESET", (libc::ECONNRESET) as f64), - ("EDEADLK", (libc::EDEADLK) as f64), - ("EDESTADDRREQ", (libc::EDESTADDRREQ) as f64), - ("EDOM", (libc::EDOM) as f64), - ("EDQUOT", (libc::EDQUOT) as f64), - ("EEXIST", (libc::EEXIST) as f64), - ("EFAULT", (libc::EFAULT) as f64), - ("EFBIG", (libc::EFBIG) as f64), - ("EHOSTUNREACH", (libc::EHOSTUNREACH) as f64), - ("EIDRM", (libc::EIDRM) as f64), - ("EILSEQ", (libc::EILSEQ) as f64), - ("EINPROGRESS", (libc::EINPROGRESS) as f64), - ("EINTR", (libc::EINTR) as f64), - ("EINVAL", (libc::EINVAL) as f64), - ("EIO", (libc::EIO) as f64), - ("EISCONN", (libc::EISCONN) as f64), - ("EISDIR", (libc::EISDIR) as f64), - ("ELOOP", (libc::ELOOP) as f64), - ("EMFILE", (libc::EMFILE) as f64), - ("EMLINK", (libc::EMLINK) as f64), - ("EMSGSIZE", (libc::EMSGSIZE) as f64), - ("EMULTIHOP", (libc::EMULTIHOP) as f64), - ("ENAMETOOLONG", (libc::ENAMETOOLONG) as f64), - ("ENETDOWN", (libc::ENETDOWN) as f64), - ("ENETRESET", (libc::ENETRESET) as f64), - ("ENETUNREACH", (libc::ENETUNREACH) as f64), - ("ENFILE", (libc::ENFILE) as f64), - ("ENOBUFS", (libc::ENOBUFS) as f64), - ("ENODATA", (libc::ENODATA) as f64), - ("ENODEV", (libc::ENODEV) as f64), - ("ENOENT", (libc::ENOENT) as f64), - ("ENOEXEC", (libc::ENOEXEC) as f64), - ("ENOLCK", (libc::ENOLCK) as f64), - ("ENOLINK", (libc::ENOLINK) as f64), - ("ENOMEM", (libc::ENOMEM) as f64), - ("ENOMSG", (libc::ENOMSG) as f64), - ("ENOPROTOOPT", (libc::ENOPROTOOPT) as f64), - ("ENOSPC", (libc::ENOSPC) as f64), - ("ENOSR", (libc::ENOSR) as f64), - ("ENOSTR", (libc::ENOSTR) as f64), - ("ENOSYS", (libc::ENOSYS) as f64), - ("ENOTCONN", (libc::ENOTCONN) as f64), - ("ENOTDIR", (libc::ENOTDIR) as f64), - ("ENOTEMPTY", (libc::ENOTEMPTY) as f64), - ("ENOTSOCK", (libc::ENOTSOCK) as f64), - ("ENOTSUP", (libc::ENOTSUP) as f64), - ("ENOTTY", (libc::ENOTTY) as f64), - ("ENXIO", (libc::ENXIO) as f64), - ("EOPNOTSUPP", (libc::EOPNOTSUPP) as f64), - ("EOVERFLOW", (libc::EOVERFLOW) as f64), - ("EPERM", (libc::EPERM) as f64), - ("EPIPE", (libc::EPIPE) as f64), - ("EPROTO", (libc::EPROTO) as f64), - ("EPROTONOSUPPORT", (libc::EPROTONOSUPPORT) as f64), - ("EPROTOTYPE", (libc::EPROTOTYPE) as f64), - ("ERANGE", (libc::ERANGE) as f64), - ("EROFS", (libc::EROFS) as f64), - ("ESPIPE", (libc::ESPIPE) as f64), - ("ESRCH", (libc::ESRCH) as f64), - ("ESTALE", (libc::ESTALE) as f64), - ("ETIME", (libc::ETIME) as f64), - ("ETIMEDOUT", (libc::ETIMEDOUT) as f64), - ("ETXTBSY", (libc::ETXTBSY) as f64), - ("EWOULDBLOCK", (libc::EWOULDBLOCK) as f64), - ("EXDEV", (libc::EXDEV) as f64), -]; -#[cfg(not(unix))] -static OS_ERRNO_CONST_TABLE: &[(&str, f64)] = &[ - ("EACCES", (13.0) as f64), - ("EAGAIN", (11.0) as f64), - ("EBADF", (9.0) as f64), - ("EBUSY", (16.0) as f64), - ("EEXIST", (17.0) as f64), - ("EFAULT", (14.0) as f64), - ("EINTR", (4.0) as f64), - ("EINVAL", (22.0) as f64), - ("EIO", (5.0) as f64), - ("EISDIR", (21.0) as f64), - ("EMFILE", (24.0) as f64), - ("ENFILE", (23.0) as f64), - ("ENODEV", (19.0) as f64), - ("ENOENT", (2.0) as f64), - ("ENOMEM", (12.0) as f64), - ("ENOSPC", (28.0) as f64), - ("ENOTDIR", (20.0) as f64), - ("ENOTEMPTY", (41.0) as f64), - ("EPERM", (1.0) as f64), - ("EPIPE", (32.0) as f64), - ("ERANGE", (34.0) as f64), - ("EROFS", (30.0) as f64), -]; - -fn os_errno_const_lookup(prop: &str) -> Option { - let i = OS_ERRNO_CONST_TABLE - .binary_search_by(|(n, _)| (*n).cmp(prop)) - .ok()?; - Some(OS_ERRNO_CONST_TABLE[i].1) -} - -#[cfg(test)] -#[allow(clippy::unnecessary_cast)] -fn os_errno_const_reference(prop: &str) -> Option { - #[cfg(unix)] - { - let v: Option = match prop { - "E2BIG" => Some(libc::E2BIG), - "EACCES" => Some(libc::EACCES), - "EADDRINUSE" => Some(libc::EADDRINUSE), - "EADDRNOTAVAIL" => Some(libc::EADDRNOTAVAIL), - "EAFNOSUPPORT" => Some(libc::EAFNOSUPPORT), - "EAGAIN" => Some(libc::EAGAIN), - "EALREADY" => Some(libc::EALREADY), - "EBADF" => Some(libc::EBADF), - "EBADMSG" => Some(libc::EBADMSG), - "EBUSY" => Some(libc::EBUSY), - "ECANCELED" => Some(libc::ECANCELED), - "ECHILD" => Some(libc::ECHILD), - "ECONNABORTED" => Some(libc::ECONNABORTED), - "ECONNREFUSED" => Some(libc::ECONNREFUSED), - "ECONNRESET" => Some(libc::ECONNRESET), - "EDEADLK" => Some(libc::EDEADLK), - "EDESTADDRREQ" => Some(libc::EDESTADDRREQ), - "EDOM" => Some(libc::EDOM), - "EDQUOT" => Some(libc::EDQUOT), - "EEXIST" => Some(libc::EEXIST), - "EFAULT" => Some(libc::EFAULT), - "EFBIG" => Some(libc::EFBIG), - "EHOSTUNREACH" => Some(libc::EHOSTUNREACH), - "EIDRM" => Some(libc::EIDRM), - "EILSEQ" => Some(libc::EILSEQ), - "EINPROGRESS" => Some(libc::EINPROGRESS), - "EINTR" => Some(libc::EINTR), - "EINVAL" => Some(libc::EINVAL), - "EIO" => Some(libc::EIO), - "EISCONN" => Some(libc::EISCONN), - "EISDIR" => Some(libc::EISDIR), - "ELOOP" => Some(libc::ELOOP), - "EMFILE" => Some(libc::EMFILE), - "EMLINK" => Some(libc::EMLINK), - "EMSGSIZE" => Some(libc::EMSGSIZE), - "EMULTIHOP" => Some(libc::EMULTIHOP), - "ENAMETOOLONG" => Some(libc::ENAMETOOLONG), - "ENETDOWN" => Some(libc::ENETDOWN), - "ENETRESET" => Some(libc::ENETRESET), - "ENETUNREACH" => Some(libc::ENETUNREACH), - "ENFILE" => Some(libc::ENFILE), - "ENOBUFS" => Some(libc::ENOBUFS), - "ENODATA" => Some(libc::ENODATA), - "ENODEV" => Some(libc::ENODEV), - "ENOENT" => Some(libc::ENOENT), - "ENOEXEC" => Some(libc::ENOEXEC), - "ENOLCK" => Some(libc::ENOLCK), - "ENOLINK" => Some(libc::ENOLINK), - "ENOMEM" => Some(libc::ENOMEM), - "ENOMSG" => Some(libc::ENOMSG), - "ENOPROTOOPT" => Some(libc::ENOPROTOOPT), - "ENOSPC" => Some(libc::ENOSPC), - "ENOSR" => Some(libc::ENOSR), - "ENOSTR" => Some(libc::ENOSTR), - "ENOSYS" => Some(libc::ENOSYS), - "ENOTCONN" => Some(libc::ENOTCONN), - "ENOTDIR" => Some(libc::ENOTDIR), - "ENOTEMPTY" => Some(libc::ENOTEMPTY), - "ENOTSOCK" => Some(libc::ENOTSOCK), - "ENOTSUP" => Some(libc::ENOTSUP), - "ENOTTY" => Some(libc::ENOTTY), - "ENXIO" => Some(libc::ENXIO), - "EOPNOTSUPP" => Some(libc::EOPNOTSUPP), - "EOVERFLOW" => Some(libc::EOVERFLOW), - "EPERM" => Some(libc::EPERM), - "EPIPE" => Some(libc::EPIPE), - "EPROTO" => Some(libc::EPROTO), - "EPROTONOSUPPORT" => Some(libc::EPROTONOSUPPORT), - "EPROTOTYPE" => Some(libc::EPROTOTYPE), - "ERANGE" => Some(libc::ERANGE), - "EROFS" => Some(libc::EROFS), - "ESPIPE" => Some(libc::ESPIPE), - "ESRCH" => Some(libc::ESRCH), - "ESTALE" => Some(libc::ESTALE), - "ETIME" => Some(libc::ETIME), - "ETIMEDOUT" => Some(libc::ETIMEDOUT), - "ETXTBSY" => Some(libc::ETXTBSY), - "EWOULDBLOCK" => Some(libc::EWOULDBLOCK), - "EXDEV" => Some(libc::EXDEV), - _ => None, - }; - v.map(|x| x as f64) - } - #[cfg(not(unix))] - { - match prop { - "EACCES" => Some(13.0), - "EAGAIN" => Some(11.0), - "EBADF" => Some(9.0), - "EBUSY" => Some(16.0), - "EEXIST" => Some(17.0), - "EFAULT" => Some(14.0), - "EINTR" => Some(4.0), - "EINVAL" => Some(22.0), - "EIO" => Some(5.0), - "EISDIR" => Some(21.0), - "EMFILE" => Some(24.0), - "ENFILE" => Some(23.0), - "ENODEV" => Some(19.0), - "ENOENT" => Some(2.0), - "ENOMEM" => Some(12.0), - "ENOSPC" => Some(28.0), - "ENOTDIR" => Some(20.0), - "ENOTEMPTY" => Some(41.0), - "EPERM" => Some(1.0), - "EPIPE" => Some(32.0), - "ERANGE" => Some(34.0), - "EROFS" => Some(30.0), - _ => None, - } - } -} - -#[cfg(test)] -mod os_errno_const_table_tests { - use super::*; - #[test] - fn sorted() { - for w in OS_ERRNO_CONST_TABLE.windows(2) { - assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); - } - } - #[test] - fn matches_reference_exhaustively() { - let source = include_str!("constants.rs"); - let mut rest = source; - while let Some(s) = rest.find('"') { - let after = &rest[s + 1..]; - let Some(e) = after.find('"') else { break }; - let lit = &after[..e]; - if !lit.is_empty() && lit.len() < 64 { - assert_eq!( - os_errno_const_lookup(lit), - os_errno_const_reference(lit), - "at {lit}" - ); - } - rest = &after[e + 1..]; - } - } -} - -/// Sorted table for `os_signal_const`: block-level `#[cfg]` predicates mirrored -/// verbatim as per-platform tables; per-arm cfg chains composed with -/// `not(any(..))` so first-match-wins semantics survive; every value kept -/// as its verbatim const expression cast to f64 (uniform row type across -/// platforms). The verbatim reference + literal-universe oracle below runs -/// on EVERY platform in CI, covering the sides not testable locally. -#[cfg(unix)] -static OS_SIGNAL_CONST_TABLE: &[(&str, f64)] = &[ - ("SIGABRT", (libc::SIGABRT) as f64), - ("SIGALRM", (libc::SIGALRM) as f64), - ("SIGBUS", (libc::SIGBUS) as f64), - ("SIGCHLD", (libc::SIGCHLD) as f64), - ("SIGCONT", (libc::SIGCONT) as f64), - ("SIGFPE", (libc::SIGFPE) as f64), - ("SIGHUP", (libc::SIGHUP) as f64), - ("SIGILL", (libc::SIGILL) as f64), - #[cfg(target_os = "macos")] - ("SIGINFO", (29i32) as f64), - ("SIGINT", (libc::SIGINT) as f64), - ("SIGIO", (libc::SIGIO) as f64), - ("SIGIOT", (libc::SIGABRT) as f64), - ("SIGKILL", (libc::SIGKILL) as f64), - ("SIGPIPE", (libc::SIGPIPE) as f64), - #[cfg(any(target_os = "linux", target_os = "android"))] - ("SIGPOLL", (libc::SIGPOLL) as f64), - ("SIGPROF", (libc::SIGPROF) as f64), - #[cfg(target_os = "linux")] - ("SIGPWR", (libc::SIGPWR) as f64), - ("SIGQUIT", (libc::SIGQUIT) as f64), - ("SIGSEGV", (libc::SIGSEGV) as f64), - #[cfg(target_os = "linux")] - ("SIGSTKFLT", (libc::SIGSTKFLT) as f64), - ("SIGSTOP", (libc::SIGSTOP) as f64), - ("SIGSYS", (libc::SIGSYS) as f64), - ("SIGTERM", (libc::SIGTERM) as f64), - ("SIGTRAP", (libc::SIGTRAP) as f64), - ("SIGTSTP", (libc::SIGTSTP) as f64), - ("SIGTTIN", (libc::SIGTTIN) as f64), - ("SIGTTOU", (libc::SIGTTOU) as f64), - ("SIGURG", (libc::SIGURG) as f64), - ("SIGUSR1", (libc::SIGUSR1) as f64), - ("SIGUSR2", (libc::SIGUSR2) as f64), - ("SIGVTALRM", (libc::SIGVTALRM) as f64), - ("SIGWINCH", (libc::SIGWINCH) as f64), - ("SIGXCPU", (libc::SIGXCPU) as f64), - ("SIGXFSZ", (libc::SIGXFSZ) as f64), -]; -#[cfg(not(unix))] -static OS_SIGNAL_CONST_TABLE: &[(&str, f64)] = &[ - ("SIGABRT", (22.0) as f64), - ("SIGBREAK", (21.0) as f64), - ("SIGFPE", (8.0) as f64), - ("SIGHUP", (1.0) as f64), - ("SIGILL", (4.0) as f64), - ("SIGINT", (2.0) as f64), - ("SIGKILL", (9.0) as f64), - ("SIGSEGV", (11.0) as f64), - ("SIGTERM", (15.0) as f64), -]; - -fn os_signal_const_lookup(prop: &str) -> Option { - let i = OS_SIGNAL_CONST_TABLE - .binary_search_by(|(n, _)| (*n).cmp(prop)) - .ok()?; - Some(OS_SIGNAL_CONST_TABLE[i].1) -} - -#[cfg(test)] -#[allow(clippy::unnecessary_cast)] -fn os_signal_const_reference(prop: &str) -> Option { - #[cfg(unix)] - { - let v: Option = match prop { - "SIGHUP" => Some(libc::SIGHUP), - "SIGINT" => Some(libc::SIGINT), - "SIGQUIT" => Some(libc::SIGQUIT), - "SIGILL" => Some(libc::SIGILL), - "SIGTRAP" => Some(libc::SIGTRAP), - "SIGABRT" => Some(libc::SIGABRT), - "SIGIOT" => Some(libc::SIGABRT), - "SIGBUS" => Some(libc::SIGBUS), - "SIGFPE" => Some(libc::SIGFPE), - "SIGKILL" => Some(libc::SIGKILL), - "SIGUSR1" => Some(libc::SIGUSR1), - "SIGSEGV" => Some(libc::SIGSEGV), - "SIGUSR2" => Some(libc::SIGUSR2), - "SIGPIPE" => Some(libc::SIGPIPE), - "SIGALRM" => Some(libc::SIGALRM), - "SIGTERM" => Some(libc::SIGTERM), - "SIGCHLD" => Some(libc::SIGCHLD), - #[cfg(target_os = "linux")] - "SIGSTKFLT" => Some(libc::SIGSTKFLT), - "SIGCONT" => Some(libc::SIGCONT), - "SIGSTOP" => Some(libc::SIGSTOP), - "SIGTSTP" => Some(libc::SIGTSTP), - "SIGTTIN" => Some(libc::SIGTTIN), - "SIGTTOU" => Some(libc::SIGTTOU), - "SIGURG" => Some(libc::SIGURG), - "SIGXCPU" => Some(libc::SIGXCPU), - "SIGXFSZ" => Some(libc::SIGXFSZ), - "SIGVTALRM" => Some(libc::SIGVTALRM), - "SIGPROF" => Some(libc::SIGPROF), - "SIGWINCH" => Some(libc::SIGWINCH), - "SIGIO" => Some(libc::SIGIO), - #[cfg(any(target_os = "linux", target_os = "android"))] - "SIGPOLL" => Some(libc::SIGPOLL), - #[cfg(target_os = "linux")] - "SIGPWR" => Some(libc::SIGPWR), - "SIGSYS" => Some(libc::SIGSYS), - #[cfg(target_os = "macos")] - "SIGINFO" => Some(29i32), - _ => None, - }; - v.map(|x| x as f64) - } - #[cfg(not(unix))] - { - match prop { - "SIGHUP" => Some(1.0), - "SIGINT" => Some(2.0), - "SIGILL" => Some(4.0), - "SIGABRT" => Some(22.0), - "SIGFPE" => Some(8.0), - "SIGKILL" => Some(9.0), - "SIGSEGV" => Some(11.0), - "SIGTERM" => Some(15.0), - "SIGBREAK" => Some(21.0), - _ => None, - } - } -} - -#[cfg(test)] -mod os_signal_const_table_tests { - use super::*; - #[test] - fn sorted() { - for w in OS_SIGNAL_CONST_TABLE.windows(2) { - assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); - } - } - #[test] - fn matches_reference_exhaustively() { - let source = include_str!("constants.rs"); - let mut rest = source; - while let Some(s) = rest.find('"') { - let after = &rest[s + 1..]; - let Some(e) = after.find('"') else { break }; - let lit = &after[..e]; - if !lit.is_empty() && lit.len() < 64 { - assert_eq!( - os_signal_const_lookup(lit), - os_signal_const_reference(lit), - "at {lit}" - ); - } - rest = &after[e + 1..]; - } - } -} - /// Sorted table for `fs_const_tail`: block-level `#[cfg]` predicates mirrored /// verbatim as per-platform tables; per-arm cfg chains composed with /// `not(any(..))` so first-match-wins semantics survive; every value kept @@ -2265,7 +1242,10 @@ mod fs_const_tail_table_tests { } #[test] fn matches_reference_exhaustively() { - let source = include_str!("constants.rs"); + let source = concat!( + include_str!("constants.rs"), + include_str!("constants_tables.rs") + ); let mut rest = source; while let Some(s) = rest.find('"') { let after = &rest[s + 1..]; @@ -2408,7 +1388,10 @@ mod sqlite_const_table_tests { } #[test] fn matches_reference_exhaustively() { - let source = include_str!("constants.rs"); + let source = concat!( + include_str!("constants.rs"), + include_str!("constants_tables.rs") + ); let mut rest = source; while let Some(s) = rest.find('"') { let after = &rest[s + 1..]; diff --git a/crates/perry-runtime/src/object/native_module/constants_tables.rs b/crates/perry-runtime/src/object/native_module/constants_tables.rs new file mode 100644 index 0000000000..f873dd0e84 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/constants_tables.rs @@ -0,0 +1,1037 @@ +/// Sorted table for `zlib_const`: block-level `#[cfg]` predicates mirrored +/// verbatim as per-platform tables; per-arm cfg chains composed with +/// `not(any(..))` so first-match-wins semantics survive; every value kept +/// as its verbatim const expression cast to f64 (uniform row type across +/// platforms). The verbatim reference + literal-universe oracle below runs +/// on EVERY platform in CI, covering the sides not testable locally. +static ZLIB_CONST_TABLE: &[(&str, f64)] = &[ + ("BROTLI_DECODE", (8) as f64), + ("BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES", (-30) as f64), + ("BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP", (-25) as f64), + ("BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES", (-21) as f64), + ("BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1", (-26) as f64), + ("BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2", (-27) as f64), + ("BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS", (-22) as f64), + ("BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET", (-19) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1", (-9) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2", (-10) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_CL_SPACE", (-6) as f64), + ( + "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT", + (-8) as f64, + ), + ("BROTLI_DECODER_ERROR_FORMAT_DICTIONARY", (-12) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_DISTANCE", (-16) as f64), + ( + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE", + (-3) as f64, + ), + ("BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE", (-1) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE", (-7) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_PADDING_1", (-14) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_PADDING_2", (-15) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_RESERVED", (-2) as f64), + ( + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET", + (-4) as f64, + ), + ( + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME", + (-5) as f64, + ), + ("BROTLI_DECODER_ERROR_FORMAT_TRANSFORM", (-11) as f64), + ("BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS", (-13) as f64), + ("BROTLI_DECODER_ERROR_INVALID_ARGUMENTS", (-20) as f64), + ("BROTLI_DECODER_ERROR_UNREACHABLE", (-31) as f64), + ("BROTLI_DECODER_NEEDS_MORE_INPUT", (2) as f64), + ("BROTLI_DECODER_NEEDS_MORE_OUTPUT", (3) as f64), + ("BROTLI_DECODER_NO_ERROR", (0) as f64), + ( + "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION", + (0) as f64, + ), + ("BROTLI_DECODER_PARAM_LARGE_WINDOW", (1) as f64), + ("BROTLI_DECODER_RESULT_ERROR", (0) as f64), + ("BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT", (2) as f64), + ("BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT", (3) as f64), + ("BROTLI_DECODER_RESULT_SUCCESS", (1) as f64), + ("BROTLI_DECODER_SUCCESS", (1) as f64), + ("BROTLI_DEFAULT_MODE", (0) as f64), + ("BROTLI_DEFAULT_QUALITY", (11) as f64), + ("BROTLI_DEFAULT_WINDOW", (22) as f64), + ("BROTLI_ENCODE", (9) as f64), + ("BROTLI_LARGE_MAX_WINDOW_BITS", (30) as f64), + ("BROTLI_MAX_INPUT_BLOCK_BITS", (24) as f64), + ("BROTLI_MAX_QUALITY", (11) as f64), + ("BROTLI_MAX_WINDOW_BITS", (24) as f64), + ("BROTLI_MIN_INPUT_BLOCK_BITS", (16) as f64), + ("BROTLI_MIN_QUALITY", (0) as f64), + ("BROTLI_MIN_WINDOW_BITS", (10) as f64), + ("BROTLI_MODE_FONT", (2) as f64), + ("BROTLI_MODE_GENERIC", (0) as f64), + ("BROTLI_MODE_TEXT", (1) as f64), + ("BROTLI_OPERATION_EMIT_METADATA", (3) as f64), + ("BROTLI_OPERATION_FINISH", (2) as f64), + ("BROTLI_OPERATION_FLUSH", (1) as f64), + ("BROTLI_OPERATION_PROCESS", (0) as f64), + ("BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING", (4) as f64), + ("BROTLI_PARAM_LARGE_WINDOW", (6) as f64), + ("BROTLI_PARAM_LGBLOCK", (3) as f64), + ("BROTLI_PARAM_LGWIN", (2) as f64), + ("BROTLI_PARAM_MODE", (0) as f64), + ("BROTLI_PARAM_NDIRECT", (8) as f64), + ("BROTLI_PARAM_NPOSTFIX", (7) as f64), + ("BROTLI_PARAM_QUALITY", (1) as f64), + ("BROTLI_PARAM_SIZE_HINT", (5) as f64), + ("DEFLATE", (1) as f64), + ("DEFLATERAW", (5) as f64), + ("GUNZIP", (4) as f64), + ("GZIP", (3) as f64), + ("INFLATE", (2) as f64), + ("INFLATERAW", (6) as f64), + ("UNZIP", (7) as f64), + ("ZLIB_VERNUM", (0x1310) as f64), + ("ZSTD_CLEVEL_DEFAULT", (3) as f64), + ("ZSTD_COMPRESS", (10) as f64), + ("ZSTD_DECOMPRESS", (11) as f64), + ("ZSTD_MAXCLEVEL", (22) as f64), + ("ZSTD_MINCLEVEL", (-131072) as f64), + ("ZSTD_btlazy2", (6) as f64), + ("ZSTD_btopt", (7) as f64), + ("ZSTD_btultra", (8) as f64), + ("ZSTD_btultra2", (9) as f64), + ("ZSTD_c_chainLog", (103) as f64), + ("ZSTD_c_checksumFlag", (201) as f64), + ("ZSTD_c_compressionLevel", (100) as f64), + ("ZSTD_c_contentSizeFlag", (200) as f64), + ("ZSTD_c_dictIDFlag", (202) as f64), + ("ZSTD_c_enableLongDistanceMatching", (160) as f64), + ("ZSTD_c_hashLog", (102) as f64), + ("ZSTD_c_jobSize", (401) as f64), + ("ZSTD_c_ldmBucketSizeLog", (163) as f64), + ("ZSTD_c_ldmHashLog", (161) as f64), + ("ZSTD_c_ldmHashRateLog", (164) as f64), + ("ZSTD_c_ldmMinMatch", (162) as f64), + ("ZSTD_c_minMatch", (105) as f64), + ("ZSTD_c_nbWorkers", (400) as f64), + ("ZSTD_c_overlapLog", (402) as f64), + ("ZSTD_c_searchLog", (104) as f64), + ("ZSTD_c_strategy", (107) as f64), + ("ZSTD_c_targetLength", (106) as f64), + ("ZSTD_c_windowLog", (101) as f64), + ("ZSTD_d_windowLogMax", (100) as f64), + ("ZSTD_dfast", (2) as f64), + ("ZSTD_e_continue", (0) as f64), + ("ZSTD_e_end", (2) as f64), + ("ZSTD_e_flush", (1) as f64), + ("ZSTD_error_GENERIC", (1) as f64), + ("ZSTD_error_checksum_wrong", (22) as f64), + ("ZSTD_error_corruption_detected", (20) as f64), + ("ZSTD_error_dictionaryCreation_failed", (34) as f64), + ("ZSTD_error_dictionary_corrupted", (30) as f64), + ("ZSTD_error_dictionary_wrong", (32) as f64), + ("ZSTD_error_dstBuffer_null", (74) as f64), + ("ZSTD_error_dstSize_tooSmall", (70) as f64), + ("ZSTD_error_frameParameter_unsupported", (14) as f64), + ("ZSTD_error_frameParameter_windowTooLarge", (16) as f64), + ("ZSTD_error_init_missing", (62) as f64), + ("ZSTD_error_literals_headerWrong", (24) as f64), + ("ZSTD_error_maxSymbolValue_tooLarge", (46) as f64), + ("ZSTD_error_maxSymbolValue_tooSmall", (48) as f64), + ("ZSTD_error_memory_allocation", (64) as f64), + ("ZSTD_error_noForwardProgress_destFull", (80) as f64), + ("ZSTD_error_noForwardProgress_inputEmpty", (82) as f64), + ("ZSTD_error_no_error", (0) as f64), + ("ZSTD_error_parameter_combination_unsupported", (41) as f64), + ("ZSTD_error_parameter_outOfBound", (42) as f64), + ("ZSTD_error_parameter_unsupported", (40) as f64), + ("ZSTD_error_prefix_unknown", (10) as f64), + ("ZSTD_error_srcSize_wrong", (72) as f64), + ("ZSTD_error_stabilityCondition_notRespected", (50) as f64), + ("ZSTD_error_stage_wrong", (60) as f64), + ("ZSTD_error_tableLog_tooLarge", (44) as f64), + ("ZSTD_error_version_unsupported", (12) as f64), + ("ZSTD_error_workSpace_tooSmall", (66) as f64), + ("ZSTD_fast", (1) as f64), + ("ZSTD_greedy", (3) as f64), + ("ZSTD_lazy", (4) as f64), + ("ZSTD_lazy2", (5) as f64), + ("Z_BEST_COMPRESSION", (9) as f64), + ("Z_BEST_SPEED", (1) as f64), + ("Z_BLOCK", (5) as f64), + ("Z_BUF_ERROR", (-5) as f64), + ("Z_DATA_ERROR", (-3) as f64), + ("Z_DEFAULT_CHUNK", (16384) as f64), + ("Z_DEFAULT_COMPRESSION", (-1) as f64), + ("Z_DEFAULT_LEVEL", (-1) as f64), + ("Z_DEFAULT_MEMLEVEL", (8) as f64), + ("Z_DEFAULT_STRATEGY", (0) as f64), + ("Z_DEFAULT_WINDOWBITS", (15) as f64), + ("Z_ERRNO", (-1) as f64), + ("Z_FILTERED", (1) as f64), + ("Z_FINISH", (4) as f64), + ("Z_FIXED", (4) as f64), + ("Z_FULL_FLUSH", (3) as f64), + ("Z_HUFFMAN_ONLY", (2) as f64), + ("Z_MAX_CHUNK", (0x7fff_ffff) as f64), + ("Z_MAX_LEVEL", (9) as f64), + ("Z_MAX_MEMLEVEL", (9) as f64), + ("Z_MAX_WINDOWBITS", (15) as f64), + ("Z_MEM_ERROR", (-4) as f64), + ("Z_MIN_CHUNK", (64) as f64), + ("Z_MIN_LEVEL", (-1) as f64), + ("Z_MIN_MEMLEVEL", (1) as f64), + ("Z_MIN_WINDOWBITS", (8) as f64), + ("Z_NEED_DICT", (2) as f64), + ("Z_NO_COMPRESSION", (0) as f64), + ("Z_NO_FLUSH", (0) as f64), + ("Z_OK", (0) as f64), + ("Z_PARTIAL_FLUSH", (1) as f64), + ("Z_RLE", (3) as f64), + ("Z_STREAM_END", (1) as f64), + ("Z_STREAM_ERROR", (-2) as f64), + ("Z_SYNC_FLUSH", (2) as f64), + ("Z_VERSION_ERROR", (-6) as f64), +]; + +pub(super) fn zlib_const_lookup(prop: &str) -> Option { + let i = ZLIB_CONST_TABLE + .binary_search_by(|(n, _)| (*n).cmp(prop)) + .ok()?; + Some(ZLIB_CONST_TABLE[i].1) +} + +#[cfg(test)] +#[allow(clippy::unnecessary_cast)] +fn zlib_const_reference(prop: &str) -> Option { + let v: i64 = match prop { + // Compression levels + "Z_NO_COMPRESSION" => 0, + "Z_BEST_SPEED" => 1, + "Z_BEST_COMPRESSION" => 9, + "Z_DEFAULT_COMPRESSION" => -1, + // Compression strategies + "Z_FILTERED" => 1, + "Z_HUFFMAN_ONLY" => 2, + "Z_RLE" => 3, + "Z_FIXED" => 4, + "Z_DEFAULT_STRATEGY" => 0, + "ZLIB_VERNUM" => 0x1310, + // Flush values + "Z_NO_FLUSH" => 0, + "Z_PARTIAL_FLUSH" => 1, + "Z_SYNC_FLUSH" => 2, + "Z_FULL_FLUSH" => 3, + "Z_FINISH" => 4, + "Z_BLOCK" => 5, + // Return codes + "Z_OK" => 0, + "Z_STREAM_END" => 1, + "Z_NEED_DICT" => 2, + "Z_ERRNO" => -1, + "Z_STREAM_ERROR" => -2, + "Z_DATA_ERROR" => -3, + "Z_MEM_ERROR" => -4, + "Z_BUF_ERROR" => -5, + "Z_VERSION_ERROR" => -6, + // Min/Max window bits and memlevel + "Z_MIN_WINDOWBITS" => 8, + "Z_MAX_WINDOWBITS" => 15, + "Z_DEFAULT_WINDOWBITS" => 15, + "Z_MIN_CHUNK" => 64, + "Z_MAX_CHUNK" => 0x7fff_ffff, + "Z_DEFAULT_CHUNK" => 16384, + "Z_MIN_MEMLEVEL" => 1, + "Z_MAX_MEMLEVEL" => 9, + "Z_DEFAULT_MEMLEVEL" => 8, + "Z_MIN_LEVEL" => -1, + "Z_MAX_LEVEL" => 9, + "Z_DEFAULT_LEVEL" => -1, + // Mode (zlib stream modes — used by zlib.createDeflate etc.) + "DEFLATE" => 1, + "INFLATE" => 2, + "GZIP" => 3, + "GUNZIP" => 4, + "DEFLATERAW" => 5, + "INFLATERAW" => 6, + "UNZIP" => 7, + "BROTLI_DECODE" => 8, + "BROTLI_ENCODE" => 9, + "ZSTD_COMPRESS" => 10, + "ZSTD_DECOMPRESS" => 11, + // Brotli operation/parameter constants — match Node's + // `zlib.constants` exactly (these are the BrotliEncoder/ + // BrotliDecoder parameter ids the underlying brotli library + // exposes). + "BROTLI_OPERATION_PROCESS" => 0, + "BROTLI_OPERATION_FLUSH" => 1, + "BROTLI_OPERATION_FINISH" => 2, + "BROTLI_OPERATION_EMIT_METADATA" => 3, + "BROTLI_PARAM_MODE" => 0, + "BROTLI_MODE_GENERIC" => 0, + "BROTLI_MODE_TEXT" => 1, + "BROTLI_MODE_FONT" => 2, + "BROTLI_DEFAULT_MODE" => 0, + "BROTLI_PARAM_QUALITY" => 1, + "BROTLI_MIN_QUALITY" => 0, + "BROTLI_MAX_QUALITY" => 11, + "BROTLI_DEFAULT_QUALITY" => 11, + "BROTLI_PARAM_LGWIN" => 2, + "BROTLI_MIN_WINDOW_BITS" => 10, + "BROTLI_MAX_WINDOW_BITS" => 24, + "BROTLI_LARGE_MAX_WINDOW_BITS" => 30, + "BROTLI_DEFAULT_WINDOW" => 22, + "BROTLI_PARAM_LGBLOCK" => 3, + "BROTLI_MIN_INPUT_BLOCK_BITS" => 16, + "BROTLI_MAX_INPUT_BLOCK_BITS" => 24, + "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING" => 4, + "BROTLI_PARAM_SIZE_HINT" => 5, + "BROTLI_PARAM_LARGE_WINDOW" => 6, + "BROTLI_PARAM_NPOSTFIX" => 7, + "BROTLI_PARAM_NDIRECT" => 8, + "BROTLI_DECODER_RESULT_ERROR" => 0, + "BROTLI_DECODER_RESULT_SUCCESS" => 1, + "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT" => 2, + "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT" => 3, + "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION" => 0, + "BROTLI_DECODER_PARAM_LARGE_WINDOW" => 1, + // Zstd parameter ids — match Node's `zlib.constants`. + "ZSTD_e_continue" => 0, + "ZSTD_e_flush" => 1, + "ZSTD_e_end" => 2, + "ZSTD_fast" => 1, + "ZSTD_dfast" => 2, + "ZSTD_greedy" => 3, + "ZSTD_lazy" => 4, + "ZSTD_lazy2" => 5, + "ZSTD_btlazy2" => 6, + "ZSTD_btopt" => 7, + "ZSTD_btultra" => 8, + "ZSTD_btultra2" => 9, + "ZSTD_c_compressionLevel" => 100, + "ZSTD_c_windowLog" => 101, + "ZSTD_c_hashLog" => 102, + "ZSTD_c_chainLog" => 103, + "ZSTD_c_searchLog" => 104, + "ZSTD_c_minMatch" => 105, + "ZSTD_c_targetLength" => 106, + "ZSTD_c_strategy" => 107, + "ZSTD_c_enableLongDistanceMatching" => 160, + "ZSTD_c_ldmHashLog" => 161, + "ZSTD_c_ldmMinMatch" => 162, + "ZSTD_c_ldmBucketSizeLog" => 163, + "ZSTD_c_ldmHashRateLog" => 164, + "ZSTD_c_contentSizeFlag" => 200, + "ZSTD_c_checksumFlag" => 201, + "ZSTD_c_dictIDFlag" => 202, + "ZSTD_c_nbWorkers" => 400, + "ZSTD_c_jobSize" => 401, + "ZSTD_c_overlapLog" => 402, + "ZSTD_d_windowLogMax" => 100, + "ZSTD_CLEVEL_DEFAULT" => 3, + "ZSTD_MINCLEVEL" => -131072, + "ZSTD_MAXCLEVEL" => 22, + // #3677: Brotli decoder result/error codes Node exposes on + // `zlib.constants` (the BrotliDecoderResult / BrotliDecoderErrorCode + // enums). Required so `Object.keys(zlib.constants)` enumeration + // matches Node's full set and every enumerated key reads its value. + "BROTLI_DECODER_NO_ERROR" => 0, + "BROTLI_DECODER_SUCCESS" => 1, + "BROTLI_DECODER_NEEDS_MORE_INPUT" => 2, + "BROTLI_DECODER_NEEDS_MORE_OUTPUT" => 3, + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE" => -1, + "BROTLI_DECODER_ERROR_FORMAT_RESERVED" => -2, + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE" => -3, + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET" => -4, + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME" => -5, + "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE" => -6, + "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE" => -7, + "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT" => -8, + "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1" => -9, + "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2" => -10, + "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM" => -11, + "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY" => -12, + "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS" => -13, + "BROTLI_DECODER_ERROR_FORMAT_PADDING_1" => -14, + "BROTLI_DECODER_ERROR_FORMAT_PADDING_2" => -15, + "BROTLI_DECODER_ERROR_FORMAT_DISTANCE" => -16, + "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET" => -19, + "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS" => -20, + "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES" => -21, + "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS" => -22, + "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP" => -25, + "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1" => -26, + "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2" => -27, + "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES" => -30, + "BROTLI_DECODER_ERROR_UNREACHABLE" => -31, + // #3677: Zstd error codes (ZSTD_ErrorCode enum) Node exposes. + "ZSTD_error_no_error" => 0, + "ZSTD_error_GENERIC" => 1, + "ZSTD_error_prefix_unknown" => 10, + "ZSTD_error_version_unsupported" => 12, + "ZSTD_error_frameParameter_unsupported" => 14, + "ZSTD_error_frameParameter_windowTooLarge" => 16, + "ZSTD_error_corruption_detected" => 20, + "ZSTD_error_checksum_wrong" => 22, + "ZSTD_error_literals_headerWrong" => 24, + "ZSTD_error_dictionary_corrupted" => 30, + "ZSTD_error_dictionary_wrong" => 32, + "ZSTD_error_dictionaryCreation_failed" => 34, + "ZSTD_error_parameter_unsupported" => 40, + "ZSTD_error_parameter_combination_unsupported" => 41, + "ZSTD_error_parameter_outOfBound" => 42, + "ZSTD_error_tableLog_tooLarge" => 44, + "ZSTD_error_maxSymbolValue_tooLarge" => 46, + "ZSTD_error_maxSymbolValue_tooSmall" => 48, + "ZSTD_error_stabilityCondition_notRespected" => 50, + "ZSTD_error_stage_wrong" => 60, + "ZSTD_error_init_missing" => 62, + "ZSTD_error_memory_allocation" => 64, + "ZSTD_error_workSpace_tooSmall" => 66, + "ZSTD_error_dstSize_tooSmall" => 70, + "ZSTD_error_srcSize_wrong" => 72, + "ZSTD_error_dstBuffer_null" => 74, + "ZSTD_error_noForwardProgress_destFull" => 80, + "ZSTD_error_noForwardProgress_inputEmpty" => 82, + _ => return None, + }; + Some(v as f64) +} + +#[cfg(test)] +mod zlib_const_table_tests { + use super::*; + #[test] + fn sorted() { + for w in ZLIB_CONST_TABLE.windows(2) { + assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); + } + } + #[test] + fn matches_reference_exhaustively() { + let source = concat!( + include_str!("constants.rs"), + include_str!("constants_tables.rs") + ); + let mut rest = source; + while let Some(s) = rest.find('"') { + let after = &rest[s + 1..]; + let Some(e) = after.find('"') else { break }; + let lit = &after[..e]; + if !lit.is_empty() && lit.len() < 64 { + assert_eq!( + zlib_const_lookup(lit), + zlib_const_reference(lit), + "at {lit}" + ); + } + rest = &after[e + 1..]; + } + } +} + +/// Sorted table for `crypto_const`: block-level `#[cfg]` predicates mirrored +/// verbatim as per-platform tables; per-arm cfg chains composed with +/// `not(any(..))` so first-match-wins semantics survive; every value kept +/// as its verbatim const expression cast to f64 (uniform row type across +/// platforms). The verbatim reference + literal-universe oracle below runs +/// on EVERY platform in CI, covering the sides not testable locally. +static CRYPTO_CONST_TABLE: &[(&str, f64)] = &[ + ("DH_CHECK_P_NOT_PRIME", (1.0) as f64), + ("DH_CHECK_P_NOT_SAFE_PRIME", (2.0) as f64), + ("DH_NOT_SUITABLE_GENERATOR", (8.0) as f64), + ("DH_UNABLE_TO_CHECK_GENERATOR", (4.0) as f64), + ("ENGINE_METHOD_ALL", (65535.0) as f64), + ("ENGINE_METHOD_CIPHERS", (64.0) as f64), + ("ENGINE_METHOD_DH", (4.0) as f64), + ("ENGINE_METHOD_DIGESTS", (128.0) as f64), + ("ENGINE_METHOD_DSA", (2.0) as f64), + ("ENGINE_METHOD_EC", (2048.0) as f64), + ("ENGINE_METHOD_NONE", (0.0) as f64), + ("ENGINE_METHOD_PKEY_ASN1_METHS", (1024.0) as f64), + ("ENGINE_METHOD_PKEY_METHS", (512.0) as f64), + ("ENGINE_METHOD_RAND", (8.0) as f64), + ("ENGINE_METHOD_RSA", (1.0) as f64), + ("OPENSSL_VERSION_NUMBER", (811597840.0) as f64), + ("POINT_CONVERSION_COMPRESSED", (2.0) as f64), + ("POINT_CONVERSION_HYBRID", (6.0) as f64), + ("POINT_CONVERSION_UNCOMPRESSED", (4.0) as f64), + ("RSA_NO_PADDING", (3.0) as f64), + ("RSA_PKCS1_OAEP_PADDING", (4.0) as f64), + ("RSA_PKCS1_PADDING", (1.0) as f64), + ("RSA_PKCS1_PSS_PADDING", (6.0) as f64), + ("RSA_PSS_SALTLEN_AUTO", (-2.0) as f64), + ("RSA_PSS_SALTLEN_DIGEST", (-1.0) as f64), + ("RSA_PSS_SALTLEN_MAX_SIGN", (-2.0) as f64), + ("RSA_X931_PADDING", (5.0) as f64), + ("SSL_OP_ALL", (2147485776.0) as f64), + ("SSL_OP_ALLOW_NO_DHE_KEX", (1024.0) as f64), + ( + "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION", + (262144.0) as f64, + ), + ("SSL_OP_CIPHER_SERVER_PREFERENCE", (4194304.0) as f64), + ("SSL_OP_CISCO_ANYCONNECT", (32768.0) as f64), + ("SSL_OP_COOKIE_EXCHANGE", (8192.0) as f64), + ("SSL_OP_CRYPTOPRO_TLSEXT_BUG", (2147483648.0) as f64), + ("SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS", (2048.0) as f64), + ("SSL_OP_LEGACY_SERVER_CONNECT", (4.0) as f64), + ("SSL_OP_NO_COMPRESSION", (131072.0) as f64), + ("SSL_OP_NO_ENCRYPT_THEN_MAC", (524288.0) as f64), + ("SSL_OP_NO_QUERY_MTU", (4096.0) as f64), + ("SSL_OP_NO_RENEGOTIATION", (1073741824.0) as f64), + ( + "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION", + (65536.0) as f64, + ), + ("SSL_OP_NO_SSLv2", (0.0) as f64), + ("SSL_OP_NO_SSLv3", (33554432.0) as f64), + ("SSL_OP_NO_TICKET", (16384.0) as f64), + ("SSL_OP_NO_TLSv1", (67108864.0) as f64), + ("SSL_OP_NO_TLSv1_1", (268435456.0) as f64), + ("SSL_OP_NO_TLSv1_2", (134217728.0) as f64), + ("SSL_OP_NO_TLSv1_3", (536870912.0) as f64), + ("SSL_OP_PRIORITIZE_CHACHA", (2097152.0) as f64), + ("SSL_OP_TLS_ROLLBACK_BUG", (8388608.0) as f64), + ("TLS1_1_VERSION", (770.0) as f64), + ("TLS1_2_VERSION", (771.0) as f64), + ("TLS1_3_VERSION", (772.0) as f64), + ("TLS1_VERSION", (769.0) as f64), +]; + +pub(super) fn crypto_const_lookup(prop: &str) -> Option { + let i = CRYPTO_CONST_TABLE + .binary_search_by(|(n, _)| (*n).cmp(prop)) + .ok()?; + Some(CRYPTO_CONST_TABLE[i].1) +} + +#[cfg(test)] +#[allow(clippy::unnecessary_cast)] +fn crypto_const_reference(prop: &str) -> Option { + match prop { + "OPENSSL_VERSION_NUMBER" => Some(811597840.0), + "SSL_OP_ALL" => Some(2147485776.0), + "SSL_OP_ALLOW_NO_DHE_KEX" => Some(1024.0), + "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION" => Some(262144.0), + "SSL_OP_CIPHER_SERVER_PREFERENCE" => Some(4194304.0), + "SSL_OP_CISCO_ANYCONNECT" => Some(32768.0), + "SSL_OP_COOKIE_EXCHANGE" => Some(8192.0), + "SSL_OP_CRYPTOPRO_TLSEXT_BUG" => Some(2147483648.0), + "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS" => Some(2048.0), + "SSL_OP_LEGACY_SERVER_CONNECT" => Some(4.0), + "SSL_OP_NO_COMPRESSION" => Some(131072.0), + "SSL_OP_NO_ENCRYPT_THEN_MAC" => Some(524288.0), + "SSL_OP_NO_QUERY_MTU" => Some(4096.0), + "SSL_OP_NO_RENEGOTIATION" => Some(1073741824.0), + "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION" => Some(65536.0), + "SSL_OP_NO_SSLv2" => Some(0.0), + "SSL_OP_NO_SSLv3" => Some(33554432.0), + "SSL_OP_NO_TICKET" => Some(16384.0), + "SSL_OP_NO_TLSv1" => Some(67108864.0), + "SSL_OP_NO_TLSv1_1" => Some(268435456.0), + "SSL_OP_NO_TLSv1_2" => Some(134217728.0), + "SSL_OP_NO_TLSv1_3" => Some(536870912.0), + "SSL_OP_PRIORITIZE_CHACHA" => Some(2097152.0), + "SSL_OP_TLS_ROLLBACK_BUG" => Some(8388608.0), + "ENGINE_METHOD_RSA" => Some(1.0), + "ENGINE_METHOD_DSA" => Some(2.0), + "ENGINE_METHOD_DH" => Some(4.0), + "ENGINE_METHOD_RAND" => Some(8.0), + "ENGINE_METHOD_EC" => Some(2048.0), + "ENGINE_METHOD_CIPHERS" => Some(64.0), + "ENGINE_METHOD_DIGESTS" => Some(128.0), + "ENGINE_METHOD_PKEY_METHS" => Some(512.0), + "ENGINE_METHOD_PKEY_ASN1_METHS" => Some(1024.0), + "ENGINE_METHOD_ALL" => Some(65535.0), + "ENGINE_METHOD_NONE" => Some(0.0), + "DH_CHECK_P_NOT_SAFE_PRIME" => Some(2.0), + "DH_CHECK_P_NOT_PRIME" => Some(1.0), + "DH_UNABLE_TO_CHECK_GENERATOR" => Some(4.0), + "DH_NOT_SUITABLE_GENERATOR" => Some(8.0), + "RSA_PKCS1_PADDING" => Some(1.0), + "RSA_NO_PADDING" => Some(3.0), + "RSA_PKCS1_OAEP_PADDING" => Some(4.0), + "RSA_X931_PADDING" => Some(5.0), + "RSA_PKCS1_PSS_PADDING" => Some(6.0), + "RSA_PSS_SALTLEN_DIGEST" => Some(-1.0), + "RSA_PSS_SALTLEN_MAX_SIGN" => Some(-2.0), + "RSA_PSS_SALTLEN_AUTO" => Some(-2.0), + "TLS1_VERSION" => Some(769.0), + "TLS1_1_VERSION" => Some(770.0), + "TLS1_2_VERSION" => Some(771.0), + "TLS1_3_VERSION" => Some(772.0), + "POINT_CONVERSION_COMPRESSED" => Some(2.0), + "POINT_CONVERSION_UNCOMPRESSED" => Some(4.0), + "POINT_CONVERSION_HYBRID" => Some(6.0), + _ => None, + } +} + +#[cfg(test)] +mod crypto_const_table_tests { + use super::*; + #[test] + fn sorted() { + for w in CRYPTO_CONST_TABLE.windows(2) { + assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); + } + } + #[test] + fn matches_reference_exhaustively() { + let source = concat!( + include_str!("constants.rs"), + include_str!("constants_tables.rs") + ); + let mut rest = source; + while let Some(s) = rest.find('"') { + let after = &rest[s + 1..]; + let Some(e) = after.find('"') else { break }; + let lit = &after[..e]; + if !lit.is_empty() && lit.len() < 64 { + assert_eq!( + crypto_const_lookup(lit), + crypto_const_reference(lit), + "at {lit}" + ); + } + rest = &after[e + 1..]; + } + } +} + +/// Sorted table for `os_errno_const`: block-level `#[cfg]` predicates mirrored +/// verbatim as per-platform tables; per-arm cfg chains composed with +/// `not(any(..))` so first-match-wins semantics survive; every value kept +/// as its verbatim const expression cast to f64 (uniform row type across +/// platforms). The verbatim reference + literal-universe oracle below runs +/// on EVERY platform in CI, covering the sides not testable locally. +#[cfg(unix)] +static OS_ERRNO_CONST_TABLE: &[(&str, f64)] = &[ + ("E2BIG", (libc::E2BIG) as f64), + ("EACCES", (libc::EACCES) as f64), + ("EADDRINUSE", (libc::EADDRINUSE) as f64), + ("EADDRNOTAVAIL", (libc::EADDRNOTAVAIL) as f64), + ("EAFNOSUPPORT", (libc::EAFNOSUPPORT) as f64), + ("EAGAIN", (libc::EAGAIN) as f64), + ("EALREADY", (libc::EALREADY) as f64), + ("EBADF", (libc::EBADF) as f64), + ("EBADMSG", (libc::EBADMSG) as f64), + ("EBUSY", (libc::EBUSY) as f64), + ("ECANCELED", (libc::ECANCELED) as f64), + ("ECHILD", (libc::ECHILD) as f64), + ("ECONNABORTED", (libc::ECONNABORTED) as f64), + ("ECONNREFUSED", (libc::ECONNREFUSED) as f64), + ("ECONNRESET", (libc::ECONNRESET) as f64), + ("EDEADLK", (libc::EDEADLK) as f64), + ("EDESTADDRREQ", (libc::EDESTADDRREQ) as f64), + ("EDOM", (libc::EDOM) as f64), + ("EDQUOT", (libc::EDQUOT) as f64), + ("EEXIST", (libc::EEXIST) as f64), + ("EFAULT", (libc::EFAULT) as f64), + ("EFBIG", (libc::EFBIG) as f64), + ("EHOSTUNREACH", (libc::EHOSTUNREACH) as f64), + ("EIDRM", (libc::EIDRM) as f64), + ("EILSEQ", (libc::EILSEQ) as f64), + ("EINPROGRESS", (libc::EINPROGRESS) as f64), + ("EINTR", (libc::EINTR) as f64), + ("EINVAL", (libc::EINVAL) as f64), + ("EIO", (libc::EIO) as f64), + ("EISCONN", (libc::EISCONN) as f64), + ("EISDIR", (libc::EISDIR) as f64), + ("ELOOP", (libc::ELOOP) as f64), + ("EMFILE", (libc::EMFILE) as f64), + ("EMLINK", (libc::EMLINK) as f64), + ("EMSGSIZE", (libc::EMSGSIZE) as f64), + ("EMULTIHOP", (libc::EMULTIHOP) as f64), + ("ENAMETOOLONG", (libc::ENAMETOOLONG) as f64), + ("ENETDOWN", (libc::ENETDOWN) as f64), + ("ENETRESET", (libc::ENETRESET) as f64), + ("ENETUNREACH", (libc::ENETUNREACH) as f64), + ("ENFILE", (libc::ENFILE) as f64), + ("ENOBUFS", (libc::ENOBUFS) as f64), + ("ENODATA", (libc::ENODATA) as f64), + ("ENODEV", (libc::ENODEV) as f64), + ("ENOENT", (libc::ENOENT) as f64), + ("ENOEXEC", (libc::ENOEXEC) as f64), + ("ENOLCK", (libc::ENOLCK) as f64), + ("ENOLINK", (libc::ENOLINK) as f64), + ("ENOMEM", (libc::ENOMEM) as f64), + ("ENOMSG", (libc::ENOMSG) as f64), + ("ENOPROTOOPT", (libc::ENOPROTOOPT) as f64), + ("ENOSPC", (libc::ENOSPC) as f64), + ("ENOSR", (libc::ENOSR) as f64), + ("ENOSTR", (libc::ENOSTR) as f64), + ("ENOSYS", (libc::ENOSYS) as f64), + ("ENOTCONN", (libc::ENOTCONN) as f64), + ("ENOTDIR", (libc::ENOTDIR) as f64), + ("ENOTEMPTY", (libc::ENOTEMPTY) as f64), + ("ENOTSOCK", (libc::ENOTSOCK) as f64), + ("ENOTSUP", (libc::ENOTSUP) as f64), + ("ENOTTY", (libc::ENOTTY) as f64), + ("ENXIO", (libc::ENXIO) as f64), + ("EOPNOTSUPP", (libc::EOPNOTSUPP) as f64), + ("EOVERFLOW", (libc::EOVERFLOW) as f64), + ("EPERM", (libc::EPERM) as f64), + ("EPIPE", (libc::EPIPE) as f64), + ("EPROTO", (libc::EPROTO) as f64), + ("EPROTONOSUPPORT", (libc::EPROTONOSUPPORT) as f64), + ("EPROTOTYPE", (libc::EPROTOTYPE) as f64), + ("ERANGE", (libc::ERANGE) as f64), + ("EROFS", (libc::EROFS) as f64), + ("ESPIPE", (libc::ESPIPE) as f64), + ("ESRCH", (libc::ESRCH) as f64), + ("ESTALE", (libc::ESTALE) as f64), + ("ETIME", (libc::ETIME) as f64), + ("ETIMEDOUT", (libc::ETIMEDOUT) as f64), + ("ETXTBSY", (libc::ETXTBSY) as f64), + ("EWOULDBLOCK", (libc::EWOULDBLOCK) as f64), + ("EXDEV", (libc::EXDEV) as f64), +]; +#[cfg(not(unix))] +static OS_ERRNO_CONST_TABLE: &[(&str, f64)] = &[ + ("EACCES", (13.0) as f64), + ("EAGAIN", (11.0) as f64), + ("EBADF", (9.0) as f64), + ("EBUSY", (16.0) as f64), + ("EEXIST", (17.0) as f64), + ("EFAULT", (14.0) as f64), + ("EINTR", (4.0) as f64), + ("EINVAL", (22.0) as f64), + ("EIO", (5.0) as f64), + ("EISDIR", (21.0) as f64), + ("EMFILE", (24.0) as f64), + ("ENFILE", (23.0) as f64), + ("ENODEV", (19.0) as f64), + ("ENOENT", (2.0) as f64), + ("ENOMEM", (12.0) as f64), + ("ENOSPC", (28.0) as f64), + ("ENOTDIR", (20.0) as f64), + ("ENOTEMPTY", (41.0) as f64), + ("EPERM", (1.0) as f64), + ("EPIPE", (32.0) as f64), + ("ERANGE", (34.0) as f64), + ("EROFS", (30.0) as f64), +]; + +pub(super) fn os_errno_const_lookup(prop: &str) -> Option { + let i = OS_ERRNO_CONST_TABLE + .binary_search_by(|(n, _)| (*n).cmp(prop)) + .ok()?; + Some(OS_ERRNO_CONST_TABLE[i].1) +} + +#[cfg(test)] +#[allow(clippy::unnecessary_cast)] +fn os_errno_const_reference(prop: &str) -> Option { + #[cfg(unix)] + { + let v: Option = match prop { + "E2BIG" => Some(libc::E2BIG), + "EACCES" => Some(libc::EACCES), + "EADDRINUSE" => Some(libc::EADDRINUSE), + "EADDRNOTAVAIL" => Some(libc::EADDRNOTAVAIL), + "EAFNOSUPPORT" => Some(libc::EAFNOSUPPORT), + "EAGAIN" => Some(libc::EAGAIN), + "EALREADY" => Some(libc::EALREADY), + "EBADF" => Some(libc::EBADF), + "EBADMSG" => Some(libc::EBADMSG), + "EBUSY" => Some(libc::EBUSY), + "ECANCELED" => Some(libc::ECANCELED), + "ECHILD" => Some(libc::ECHILD), + "ECONNABORTED" => Some(libc::ECONNABORTED), + "ECONNREFUSED" => Some(libc::ECONNREFUSED), + "ECONNRESET" => Some(libc::ECONNRESET), + "EDEADLK" => Some(libc::EDEADLK), + "EDESTADDRREQ" => Some(libc::EDESTADDRREQ), + "EDOM" => Some(libc::EDOM), + "EDQUOT" => Some(libc::EDQUOT), + "EEXIST" => Some(libc::EEXIST), + "EFAULT" => Some(libc::EFAULT), + "EFBIG" => Some(libc::EFBIG), + "EHOSTUNREACH" => Some(libc::EHOSTUNREACH), + "EIDRM" => Some(libc::EIDRM), + "EILSEQ" => Some(libc::EILSEQ), + "EINPROGRESS" => Some(libc::EINPROGRESS), + "EINTR" => Some(libc::EINTR), + "EINVAL" => Some(libc::EINVAL), + "EIO" => Some(libc::EIO), + "EISCONN" => Some(libc::EISCONN), + "EISDIR" => Some(libc::EISDIR), + "ELOOP" => Some(libc::ELOOP), + "EMFILE" => Some(libc::EMFILE), + "EMLINK" => Some(libc::EMLINK), + "EMSGSIZE" => Some(libc::EMSGSIZE), + "EMULTIHOP" => Some(libc::EMULTIHOP), + "ENAMETOOLONG" => Some(libc::ENAMETOOLONG), + "ENETDOWN" => Some(libc::ENETDOWN), + "ENETRESET" => Some(libc::ENETRESET), + "ENETUNREACH" => Some(libc::ENETUNREACH), + "ENFILE" => Some(libc::ENFILE), + "ENOBUFS" => Some(libc::ENOBUFS), + "ENODATA" => Some(libc::ENODATA), + "ENODEV" => Some(libc::ENODEV), + "ENOENT" => Some(libc::ENOENT), + "ENOEXEC" => Some(libc::ENOEXEC), + "ENOLCK" => Some(libc::ENOLCK), + "ENOLINK" => Some(libc::ENOLINK), + "ENOMEM" => Some(libc::ENOMEM), + "ENOMSG" => Some(libc::ENOMSG), + "ENOPROTOOPT" => Some(libc::ENOPROTOOPT), + "ENOSPC" => Some(libc::ENOSPC), + "ENOSR" => Some(libc::ENOSR), + "ENOSTR" => Some(libc::ENOSTR), + "ENOSYS" => Some(libc::ENOSYS), + "ENOTCONN" => Some(libc::ENOTCONN), + "ENOTDIR" => Some(libc::ENOTDIR), + "ENOTEMPTY" => Some(libc::ENOTEMPTY), + "ENOTSOCK" => Some(libc::ENOTSOCK), + "ENOTSUP" => Some(libc::ENOTSUP), + "ENOTTY" => Some(libc::ENOTTY), + "ENXIO" => Some(libc::ENXIO), + "EOPNOTSUPP" => Some(libc::EOPNOTSUPP), + "EOVERFLOW" => Some(libc::EOVERFLOW), + "EPERM" => Some(libc::EPERM), + "EPIPE" => Some(libc::EPIPE), + "EPROTO" => Some(libc::EPROTO), + "EPROTONOSUPPORT" => Some(libc::EPROTONOSUPPORT), + "EPROTOTYPE" => Some(libc::EPROTOTYPE), + "ERANGE" => Some(libc::ERANGE), + "EROFS" => Some(libc::EROFS), + "ESPIPE" => Some(libc::ESPIPE), + "ESRCH" => Some(libc::ESRCH), + "ESTALE" => Some(libc::ESTALE), + "ETIME" => Some(libc::ETIME), + "ETIMEDOUT" => Some(libc::ETIMEDOUT), + "ETXTBSY" => Some(libc::ETXTBSY), + "EWOULDBLOCK" => Some(libc::EWOULDBLOCK), + "EXDEV" => Some(libc::EXDEV), + _ => None, + }; + v.map(|x| x as f64) + } + #[cfg(not(unix))] + { + match prop { + "EACCES" => Some(13.0), + "EAGAIN" => Some(11.0), + "EBADF" => Some(9.0), + "EBUSY" => Some(16.0), + "EEXIST" => Some(17.0), + "EFAULT" => Some(14.0), + "EINTR" => Some(4.0), + "EINVAL" => Some(22.0), + "EIO" => Some(5.0), + "EISDIR" => Some(21.0), + "EMFILE" => Some(24.0), + "ENFILE" => Some(23.0), + "ENODEV" => Some(19.0), + "ENOENT" => Some(2.0), + "ENOMEM" => Some(12.0), + "ENOSPC" => Some(28.0), + "ENOTDIR" => Some(20.0), + "ENOTEMPTY" => Some(41.0), + "EPERM" => Some(1.0), + "EPIPE" => Some(32.0), + "ERANGE" => Some(34.0), + "EROFS" => Some(30.0), + _ => None, + } + } +} + +#[cfg(test)] +mod os_errno_const_table_tests { + use super::*; + #[test] + fn sorted() { + for w in OS_ERRNO_CONST_TABLE.windows(2) { + assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); + } + } + #[test] + fn matches_reference_exhaustively() { + let source = concat!( + include_str!("constants.rs"), + include_str!("constants_tables.rs") + ); + let mut rest = source; + while let Some(s) = rest.find('"') { + let after = &rest[s + 1..]; + let Some(e) = after.find('"') else { break }; + let lit = &after[..e]; + if !lit.is_empty() && lit.len() < 64 { + assert_eq!( + os_errno_const_lookup(lit), + os_errno_const_reference(lit), + "at {lit}" + ); + } + rest = &after[e + 1..]; + } + } +} + +/// Sorted table for `os_signal_const`: block-level `#[cfg]` predicates mirrored +/// verbatim as per-platform tables; per-arm cfg chains composed with +/// `not(any(..))` so first-match-wins semantics survive; every value kept +/// as its verbatim const expression cast to f64 (uniform row type across +/// platforms). The verbatim reference + literal-universe oracle below runs +/// on EVERY platform in CI, covering the sides not testable locally. +#[cfg(unix)] +static OS_SIGNAL_CONST_TABLE: &[(&str, f64)] = &[ + ("SIGABRT", (libc::SIGABRT) as f64), + ("SIGALRM", (libc::SIGALRM) as f64), + ("SIGBUS", (libc::SIGBUS) as f64), + ("SIGCHLD", (libc::SIGCHLD) as f64), + ("SIGCONT", (libc::SIGCONT) as f64), + ("SIGFPE", (libc::SIGFPE) as f64), + ("SIGHUP", (libc::SIGHUP) as f64), + ("SIGILL", (libc::SIGILL) as f64), + #[cfg(target_os = "macos")] + ("SIGINFO", (29i32) as f64), + ("SIGINT", (libc::SIGINT) as f64), + ("SIGIO", (libc::SIGIO) as f64), + ("SIGIOT", (libc::SIGABRT) as f64), + ("SIGKILL", (libc::SIGKILL) as f64), + ("SIGPIPE", (libc::SIGPIPE) as f64), + #[cfg(any(target_os = "linux", target_os = "android"))] + ("SIGPOLL", (libc::SIGPOLL) as f64), + ("SIGPROF", (libc::SIGPROF) as f64), + #[cfg(target_os = "linux")] + ("SIGPWR", (libc::SIGPWR) as f64), + ("SIGQUIT", (libc::SIGQUIT) as f64), + ("SIGSEGV", (libc::SIGSEGV) as f64), + #[cfg(target_os = "linux")] + ("SIGSTKFLT", (libc::SIGSTKFLT) as f64), + ("SIGSTOP", (libc::SIGSTOP) as f64), + ("SIGSYS", (libc::SIGSYS) as f64), + ("SIGTERM", (libc::SIGTERM) as f64), + ("SIGTRAP", (libc::SIGTRAP) as f64), + ("SIGTSTP", (libc::SIGTSTP) as f64), + ("SIGTTIN", (libc::SIGTTIN) as f64), + ("SIGTTOU", (libc::SIGTTOU) as f64), + ("SIGURG", (libc::SIGURG) as f64), + ("SIGUSR1", (libc::SIGUSR1) as f64), + ("SIGUSR2", (libc::SIGUSR2) as f64), + ("SIGVTALRM", (libc::SIGVTALRM) as f64), + ("SIGWINCH", (libc::SIGWINCH) as f64), + ("SIGXCPU", (libc::SIGXCPU) as f64), + ("SIGXFSZ", (libc::SIGXFSZ) as f64), +]; +#[cfg(not(unix))] +static OS_SIGNAL_CONST_TABLE: &[(&str, f64)] = &[ + ("SIGABRT", (22.0) as f64), + ("SIGBREAK", (21.0) as f64), + ("SIGFPE", (8.0) as f64), + ("SIGHUP", (1.0) as f64), + ("SIGILL", (4.0) as f64), + ("SIGINT", (2.0) as f64), + ("SIGKILL", (9.0) as f64), + ("SIGSEGV", (11.0) as f64), + ("SIGTERM", (15.0) as f64), +]; + +pub(super) fn os_signal_const_lookup(prop: &str) -> Option { + let i = OS_SIGNAL_CONST_TABLE + .binary_search_by(|(n, _)| (*n).cmp(prop)) + .ok()?; + Some(OS_SIGNAL_CONST_TABLE[i].1) +} + +#[cfg(test)] +#[allow(clippy::unnecessary_cast)] +fn os_signal_const_reference(prop: &str) -> Option { + #[cfg(unix)] + { + let v: Option = match prop { + "SIGHUP" => Some(libc::SIGHUP), + "SIGINT" => Some(libc::SIGINT), + "SIGQUIT" => Some(libc::SIGQUIT), + "SIGILL" => Some(libc::SIGILL), + "SIGTRAP" => Some(libc::SIGTRAP), + "SIGABRT" => Some(libc::SIGABRT), + "SIGIOT" => Some(libc::SIGABRT), + "SIGBUS" => Some(libc::SIGBUS), + "SIGFPE" => Some(libc::SIGFPE), + "SIGKILL" => Some(libc::SIGKILL), + "SIGUSR1" => Some(libc::SIGUSR1), + "SIGSEGV" => Some(libc::SIGSEGV), + "SIGUSR2" => Some(libc::SIGUSR2), + "SIGPIPE" => Some(libc::SIGPIPE), + "SIGALRM" => Some(libc::SIGALRM), + "SIGTERM" => Some(libc::SIGTERM), + "SIGCHLD" => Some(libc::SIGCHLD), + #[cfg(target_os = "linux")] + "SIGSTKFLT" => Some(libc::SIGSTKFLT), + "SIGCONT" => Some(libc::SIGCONT), + "SIGSTOP" => Some(libc::SIGSTOP), + "SIGTSTP" => Some(libc::SIGTSTP), + "SIGTTIN" => Some(libc::SIGTTIN), + "SIGTTOU" => Some(libc::SIGTTOU), + "SIGURG" => Some(libc::SIGURG), + "SIGXCPU" => Some(libc::SIGXCPU), + "SIGXFSZ" => Some(libc::SIGXFSZ), + "SIGVTALRM" => Some(libc::SIGVTALRM), + "SIGPROF" => Some(libc::SIGPROF), + "SIGWINCH" => Some(libc::SIGWINCH), + "SIGIO" => Some(libc::SIGIO), + #[cfg(any(target_os = "linux", target_os = "android"))] + "SIGPOLL" => Some(libc::SIGPOLL), + #[cfg(target_os = "linux")] + "SIGPWR" => Some(libc::SIGPWR), + "SIGSYS" => Some(libc::SIGSYS), + #[cfg(target_os = "macos")] + "SIGINFO" => Some(29i32), + _ => None, + }; + v.map(|x| x as f64) + } + #[cfg(not(unix))] + { + match prop { + "SIGHUP" => Some(1.0), + "SIGINT" => Some(2.0), + "SIGILL" => Some(4.0), + "SIGABRT" => Some(22.0), + "SIGFPE" => Some(8.0), + "SIGKILL" => Some(9.0), + "SIGSEGV" => Some(11.0), + "SIGTERM" => Some(15.0), + "SIGBREAK" => Some(21.0), + _ => None, + } + } +} + +#[cfg(test)] +mod os_signal_const_table_tests { + use super::*; + #[test] + fn sorted() { + for w in OS_SIGNAL_CONST_TABLE.windows(2) { + assert!(w[0].0 < w[1].0, "{} vs {}", w[0].0, w[1].0); + } + } + #[test] + fn matches_reference_exhaustively() { + let source = concat!( + include_str!("constants.rs"), + include_str!("constants_tables.rs") + ); + let mut rest = source; + while let Some(s) = rest.find('"') { + let after = &rest[s + 1..]; + let Some(e) = after.find('"') else { break }; + let lit = &after[..e]; + if !lit.is_empty() && lit.len() < 64 { + assert_eq!( + os_signal_const_lookup(lit), + os_signal_const_reference(lit), + "at {lit}" + ); + } + rest = &after[e + 1..]; + } + } +} diff --git a/crates/perry-runtime/src/process.rs b/crates/perry-runtime/src/process.rs index a36a81a3dc..aca9f59613 100644 --- a/crates/perry-runtime/src/process.rs +++ b/crates/perry-runtime/src/process.rs @@ -12,8 +12,10 @@ use std::cell::{Cell, RefCell}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::OnceLock; +mod attributes; mod credentials; mod env_misc; +mod exec_env; pub(crate) use env_misc::{ exit_after_current_thread_collection_teardown, format_out_of_range_number, process_env_delete_field, process_env_get_field, process_env_has_field, process_env_set_field, @@ -35,21 +37,25 @@ pub use ipc::*; pub use env_misc::{ is_process_env_object, is_process_env_ptr, js_getenv, js_getenv_value, js_process_abort, js_process_active_resources_info, js_process_add_uncaught_exception_capture_callback, - js_process_available_memory, js_process_binding, js_process_chdir_jsv, - js_process_constrained_memory, js_process_cpu_usage, js_process_debug_end, - js_process_debug_process, js_process_dlopen, js_process_emit_warning, js_process_env, - js_process_execve, js_process_exit, js_process_exit_code_get, js_process_exit_code_set, - js_process_fatal_exception, js_process_get_active_handles, js_process_get_active_requests, - js_process_has_uncaught_exception_capture_callback, js_process_internal_kill, - js_process_linked_binding, js_process_load_env_file, js_process_memory_usage, + js_process_available_memory, js_process_binding, js_process_constrained_memory, + js_process_cpu_usage, js_process_debug_end, js_process_debug_process, js_process_dlopen, + js_process_emit_warning, js_process_env, js_process_exit, js_process_exit_code_get, + js_process_exit_code_set, js_process_fatal_exception, js_process_get_active_handles, + js_process_get_active_requests, js_process_has_uncaught_exception_capture_callback, + js_process_internal_kill, js_process_linked_binding, js_process_memory_usage, js_process_open_stdin, js_process_raw_debug, js_process_really_exit, js_process_ref, - js_process_resource_usage, js_process_set_title, js_process_set_uncaught_exception_capture_callback, js_process_start_profiler_idle_notifier, js_process_stop_profiler_idle_notifier, js_process_thread_cpu_usage, js_process_tick_callback, - js_process_title, js_process_umask, js_process_umask_set, js_process_unref, js_removeenv, - js_setenv, + js_process_unref, js_removeenv, js_setenv, }; +// ── exec_env / attributes re-exports (preserve `crate::process::*` paths) ─── +pub use attributes::{ + js_process_resource_usage, js_process_set_title, js_process_title, js_process_umask, + js_process_umask_set, +}; +pub use exec_env::{js_process_chdir_jsv, js_process_execve, js_process_load_env_file}; + // ── finalization re-exports ───────────────────────────────────────────────── pub use finalization::{ js_process_run_finalization_before_exit, js_process_run_finalization_exit, diff --git a/crates/perry-runtime/src/process/attributes.rs b/crates/perry-runtime/src/process/attributes.rs new file mode 100644 index 0000000000..9ba0f319c1 --- /dev/null +++ b/crates/perry-runtime/src/process/attributes.rs @@ -0,0 +1,276 @@ +//! Mutable/queryable process attributes: `process.resourceUsage()`, +//! `process.title` (get/set) and `process.umask()` (get/set). +//! +//! Split verbatim out of the sibling [`super::env_misc`] module to keep that +//! file under the 2000-line gate. Pure code move — no behavior change. + +use super::env_misc::read_js_string_lossy; +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +/// process.resourceUsage() -> object with getrusage(RUSAGE_SELF) +/// counters matching Node's shape (#1376). Linux's `ru_maxrss` is in +/// kilobytes; macOS/BSD's is in bytes — Node normalizes Linux to bytes, +/// so we do too. Non-unix targets return zeroed fields. +#[no_mangle] +pub extern "C" fn js_process_resource_usage() -> f64 { + #[allow(unused_mut)] + let mut user_cpu: f64 = 0.0; + #[allow(unused_mut)] + let mut system_cpu: f64 = 0.0; + #[allow(unused_mut)] + let mut max_rss: f64 = 0.0; + #[allow(unused_mut)] + let mut shared_mem: f64 = 0.0; + #[allow(unused_mut)] + let mut unshared_data: f64 = 0.0; + #[allow(unused_mut)] + let mut unshared_stack: f64 = 0.0; + #[allow(unused_mut)] + let mut minor_faults: f64 = 0.0; + #[allow(unused_mut)] + let mut major_faults: f64 = 0.0; + #[allow(unused_mut)] + let mut swapped_out: f64 = 0.0; + #[allow(unused_mut)] + let mut fs_read: f64 = 0.0; + #[allow(unused_mut)] + let mut fs_write: f64 = 0.0; + #[allow(unused_mut)] + let mut ipc_sent: f64 = 0.0; + #[allow(unused_mut)] + let mut ipc_recv: f64 = 0.0; + #[allow(unused_mut)] + let mut signals: f64 = 0.0; + #[allow(unused_mut)] + let mut vcsw: f64 = 0.0; + #[allow(unused_mut)] + let mut ivcsw: f64 = 0.0; + + #[cfg(unix)] + { + let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; + if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 { + user_cpu = (usage.ru_utime.tv_sec as f64) * 1_000_000.0 + usage.ru_utime.tv_usec as f64; + system_cpu = + (usage.ru_stime.tv_sec as f64) * 1_000_000.0 + usage.ru_stime.tv_usec as f64; + #[cfg(target_os = "linux")] + { + max_rss = (usage.ru_maxrss as f64) * 1024.0; + } + #[cfg(not(target_os = "linux"))] + { + max_rss = usage.ru_maxrss as f64; + } + shared_mem = usage.ru_ixrss as f64; + unshared_data = usage.ru_idrss as f64; + unshared_stack = usage.ru_isrss as f64; + minor_faults = usage.ru_minflt as f64; + major_faults = usage.ru_majflt as f64; + swapped_out = usage.ru_nswap as f64; + fs_read = usage.ru_inblock as f64; + fs_write = usage.ru_oublock as f64; + ipc_sent = usage.ru_msgsnd as f64; + ipc_recv = usage.ru_msgrcv as f64; + signals = usage.ru_nsignals as f64; + vcsw = usage.ru_nvcsw as f64; + ivcsw = usage.ru_nivcsw as f64; + } + } + + let obj = crate::object::js_object_alloc(0, 16); + let set_field = |name: &str, value: f64| { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, value); + }; + set_field("userCPUTime", user_cpu); + set_field("systemCPUTime", system_cpu); + set_field("maxRSS", max_rss); + set_field("sharedMemorySize", shared_mem); + set_field("unsharedDataSize", unshared_data); + set_field("unsharedStackSize", unshared_stack); + set_field("minorPageFault", minor_faults); + set_field("majorPageFault", major_faults); + set_field("swappedOut", swapped_out); + set_field("fsRead", fs_read); + set_field("fsWrite", fs_write); + set_field("ipcSent", ipc_sent); + set_field("ipcReceived", ipc_recv); + set_field("signalsCount", signals); + set_field("voluntaryContextSwitches", vcsw); + set_field("involuntaryContextSwitches", ivcsw); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// process.title -> string. Returns the value set via the setter, or +/// falls back to argv[0]. +#[no_mangle] +pub extern "C" fn js_process_title() -> f64 { + use crate::value::JSValue; + let stored: Option = PROCESS_TITLE.with(|c| c.borrow().clone()); + let s = stored.unwrap_or_else(|| std::env::args().next().unwrap_or_default()); + let bytes = s.as_bytes(); + let ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) +} + +/// process.title = value — coerces to string and stores in the cell. +#[no_mangle] +pub extern "C" fn js_process_set_title(value: f64) { + let ptr = crate::value::js_jsvalue_to_string(value); + let s = if ptr.is_null() { + String::new() + } else { + unsafe { + let header = &*ptr; + let len = header.byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + } + }; + #[cfg(target_os = "linux")] + { + let mut buf = [0i8; 16]; + let src = s.as_bytes(); + let copy_len = std::cmp::min(src.len(), 15); + for i in 0..copy_len { + buf[i] = src[i] as i8; + } + unsafe { + libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0); + } + } + PROCESS_TITLE.with(|c| *c.borrow_mut() = Some(s)); +} + +/// process.umask() -> number. Returns the current file-mode creation mask +/// without modifying it. POSIX's `umask` syscall has no read-only form, so +/// we set the mask to 0, capture the previous value, then restore it. +#[no_mangle] +pub extern "C" fn js_process_umask() -> f64 { + #[cfg(unix)] + unsafe { + let prev = libc::umask(0); + libc::umask(prev); + prev as f64 + } + #[cfg(not(unix))] + { + 0.0 + } +} + +/// process.umask(mask) -> number. Validates and parses `mask` the way Node's +/// `process.umask` (`parseMode`) does, sets the file-mode creation mask, and +/// returns the previous value (#2920). +/// +/// Node accepts either a 32-bit unsigned integer or an octal string: +/// - a non-number / non-string (`null`, object, boolean, …) throws +/// `TypeError [ERR_INVALID_ARG_TYPE]` ("must be of type number"); `null` +/// reports as `Received undefined` to match Node's `parseMode`; +/// - an octal string (`"077"`) is parsed via radix-8 `parseInt`; a string that +/// is not all-octal-digits (empty, `"abc"`, `"8"`, `"0xff"`, leading/trailing +/// whitespace) throws `TypeError [ERR_INVALID_ARG_VALUE]`; +/// - a non-integer / `NaN` / `Infinity` number throws +/// `RangeError [ERR_OUT_OF_RANGE]` ("must be an integer"); +/// - a value `< 0` or `> 4294967295` (either form) throws +/// `RangeError [ERR_OUT_OF_RANGE]` ("must be >= 0 && <= 4294967295"). +/// +/// An explicit `undefined` is handled at the call site as the read-only +/// no-argument form (so `js_process_umask` is called instead), matching Node's +/// `umask(undefined)` no-op-returns-current behavior. +#[no_mangle] +pub extern "C" fn js_process_umask_set(mask: f64) -> f64 { + // An explicit `undefined` argument is the read-only form (Node: + // `umask(undefined)` returns the current mask without changing it). + if JSValue::from_bits(mask.to_bits()).is_undefined() { + return js_process_umask(); + } + let parsed = parse_umask_mask(mask); + #[cfg(unix)] + unsafe { + libc::umask(parsed as libc::mode_t) as f64 + } + #[cfg(not(unix))] + { + let _ = parsed; + 0.0 + } +} + +/// Node's `parseMode("mask", value)` for `process.umask`. Diverges via +/// `js_throw` on an invalid value; otherwise returns the validated 32-bit +/// unsigned mask. +fn parse_umask_mask(mask: f64) -> u32 { + use crate::fs::validate::{ + describe_received, is_numeric, throw_range_error_named, throw_type_error_with_code, + }; + let jv = JSValue::from_bits(mask.to_bits()); + + if jv.is_any_string() { + let s = read_js_string_lossy(mask); + // Node parses the string with radix 8 (`parseInt(str, 8)`) but only + // after asserting the whole string is octal digits — leading/trailing + // whitespace, prefixes, empty, or non-octal chars are rejected. + let valid = !s.is_empty() && s.bytes().all(|b| (b'0'..=b'7').contains(&b)); + let parsed = if valid { + u64::from_str_radix(&s, 8).ok() + } else { + None + }; + match parsed { + Some(n) if n <= u32::MAX as u64 => return n as u32, + Some(n) => { + let message = format!( + "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", + n + ); + throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); + } + None => { + let message = format!( + "The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received '{}'", + s + ); + throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); + } + } + } + + if !is_numeric(jv) { + // Node's `parseMode` treats `null` like a missing value here, so its + // ERR_INVALID_ARG_TYPE renders `Received undefined`. + let received = if jv.is_null() { + "undefined".to_string() + } else { + describe_received(mask) + }; + let message = format!( + "The \"mask\" argument must be of type number. Received {}", + received + ); + throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + + let n = if jv.is_int32() { + jv.as_int32() as f64 + } else { + jv.as_number() + }; + if !(n.is_finite() && n.fract() == 0.0) { + let message = format!( + "The value of \"mask\" is out of range. It must be an integer. Received {}", + format_out_of_range_number(n) + ); + throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); + } + if n < 0.0 || n > u32::MAX as f64 { + let message = format!( + "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", + format_out_of_range_number(n) + ); + throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); + } + n as u32 +} diff --git a/crates/perry-runtime/src/process/env_misc.rs b/crates/perry-runtime/src/process/env_misc.rs index 7db15a7809..339afd5b60 100644 --- a/crates/perry-runtime/src/process/env_misc.rs +++ b/crates/perry-runtime/src/process/env_misc.rs @@ -1385,567 +1385,6 @@ pub extern "C" fn js_process_memory_usage() -> f64 { f64::from_bits(JSValue::pointer(obj as *const u8).bits()) } -/// process.loadEnvFile(path?) — read a `.env`-formatted file from disk and -/// merge its `KEY=value` entries into `process.env`. Node 20.12+. With no -/// path, the default is `.env` in the current working directory. Throws a -/// Node-shaped `Error` (`code: "ENOENT"`, `syscall: "open"`) when the file -/// can't be opened. #2135 (#1399 follow-through): previously a no-op that -/// returned undefined so probe-and-call sites didn't crash; with -/// `process.env.X = v` now persisting via std::env (#1344), eager loading -/// is meaningful. -#[no_mangle] -pub extern "C" fn js_process_load_env_file(path_value: f64) { - let target = load_env_file_path(path_value); - let contents = match std::fs::read_to_string(&target) { - Ok(s) => s, - Err(err) => unsafe { - throw_load_env_file_open_error(&err, &target); - }, - }; - for (key, value) in crate::util_parse_env::parse_env(&contents) { - if std::env::var_os(&key).is_none() { - std::env::set_var(key, value); - } - } -} - -fn load_env_file_path(value: f64) -> String { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_undefined() || jv.is_null() { - return ".env".to_string(); - } - unsafe { - validate_load_env_file_url(value); - crate::fs::decode_path_value(value) - .unwrap_or_else(|| crate::fs::validate::throw_invalid_path_arg("path", value)) - } -} - -unsafe fn validate_load_env_file_url(value: f64) { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return; - } - let obj = jv.as_pointer::() as *mut crate::object::ObjectHeader; - if obj.is_null() || !crate::url::is_url_object_shape(obj) { - return; - } - let protocol = crate::url::get_string_content(crate::object::js_object_get_field_f64( - obj, - crate::url::parse::URL_PROTOCOL, - )); - if protocol != "file:" { - throw_invalid_load_env_file_url_scheme(); - } - let pathname = crate::url::get_string_content(crate::object::js_object_get_field_f64( - obj, - crate::url::parse::URL_PATHNAME, - )); - if has_encoded_forward_slash(&pathname) { - crate::fs::validate::throw_type_error_with_code( - "File URL path must not include encoded / characters", - "ERR_INVALID_FILE_URL_PATH", - ); - } -} - -fn has_encoded_forward_slash(pathname: &str) -> bool { - let bytes = pathname.as_bytes(); - let mut i = 0usize; - while i + 2 < bytes.len() { - if bytes[i] == b'%' && bytes[i + 1] == b'2' && (bytes[i + 2] | 0x20) == b'f' { - return true; - } - i += 1; - } - false -} - -fn throw_invalid_load_env_file_url_scheme() -> ! { - crate::fs::validate::throw_type_error_with_code( - "The URL must be of scheme file", - "ERR_INVALID_URL_SCHEME", - ) -} - -unsafe fn throw_load_env_file_open_error(err: &std::io::Error, target: &str) -> ! { - use std::io::ErrorKind; - let code: &'static str = match err.kind() { - ErrorKind::NotFound => "ENOENT", - ErrorKind::PermissionDenied => "EACCES", - _ => "EIO", - }; - let desc = match code { - "ENOENT" => "no such file or directory", - "EACCES" => "permission denied", - _ => "i/o error", - }; - let message = format!("{code}: {desc}, open '{target}'"); - let msg_ptr = js_string_from_bytes(message.as_ptr(), message.len() as u32); - crate::node_submodules::register_error_code_pub(msg_ptr, code); - crate::node_submodules::register_error_syscall(msg_ptr, "open"); - crate::node_submodules::register_error_path(msg_ptr, target.to_string()); - let err_ptr = crate::error::js_error_new_with_message(msg_ptr); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err_ptr as i64)); -} - -// Issue #2013 — process-arg-validation helpers shared by `js_process_chdir` -// and `js_process_hrtime`. Sited here (not os.rs) so the process surface's -// validation logic stays under the 2000-line file gate as the os.rs splits -// progress. - -/// `process.chdir(value)` entry point that takes the full NaN-boxed -/// value. Throws `TypeError [ERR_INVALID_ARG_TYPE]` for any non-string -/// (matching Node), then re-dispatches to `js_process_chdir` with the -/// extracted `StringHeader`. The codegen now emits this entry instead -/// of the bare string-only one so a `process.chdir(123)` call throws -/// the right error code instead of garbage-deref'ing to an `ENOENT` -/// based on whatever bytes the numeric value masqueraded as. -#[no_mangle] -pub unsafe extern "C" fn js_process_chdir_jsv(value: f64) { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_any_string() { - let message = format!( - "The \"directory\" argument must be of type string. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; - crate::os::js_process_chdir(ptr); -} - -fn execve_throw_invalid_arg_type(name: &str, expected: &str, value: f64) -> ! { - let message = format!( - "The \"{}\" argument must be {}. Received {}", - name, - expected, - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -fn execve_received_value(value: f64) -> String { - let jv = JSValue::from_bits(value.to_bits()); - if crate::fs::validate::is_numeric(jv) { - let n = if jv.is_int32() { - jv.as_int32() as f64 - } else { - jv.as_number() - }; - return crate::fs::validate::format_received_number(n); - } - if let Some(value) = module_value_to_string(value) { - return format!("'{}'", value); - } - crate::fs::validate::describe_received(value) -} - -fn execve_env_received(value: f64) -> String { - let Some(obj) = module_object_ptr(value) else { - return crate::fs::validate::describe_received(value); - }; - let keys = crate::object::js_object_keys(obj); - let len = crate::array::js_array_length(keys); - let mut parts = Vec::new(); - for i in 0..len.min(3) { - let key_value = crate::array::js_array_get_f64(keys, i); - let key = module_value_to_string(key_value).unwrap_or_default(); - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let field = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); - parts.push(format!("{}: {}", key, execve_received_value(field))); - } - if len > 3 { - parts.push("...".to_string()); - } - format!("{{ {} }}", parts.join(", ")) -} - -fn execve_throw_invalid_arg_value(name: &str, received: String) -> ! { - let message = format!( - "The argument '{}' must be a string without null bytes. Received {}", - name, received - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") -} - -fn execve_throw_invalid_env(value: f64) -> ! { - let message = format!( - "The argument 'env' must be an object with string keys and values without null bytes. Received {}", - execve_env_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") -} - -fn execve_parse_args(args: f64) -> Vec { - let args_value = JSValue::from_bits(args.to_bits()); - if args_value.is_undefined() { - return Vec::new(); - } - if !is_array_value(args_value) { - execve_throw_invalid_arg_type("args", "an instance of Array", args); - } - let arr = args_value.as_pointer::(); - let len = crate::array::js_array_length(arr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - let value = crate::array::js_array_get_f64(arr, i); - let Some(item) = module_value_to_string(value) else { - execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); - }; - if item.as_bytes().contains(&0) { - execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); - } - out.push(item); - } - out -} - -fn execve_parse_env(env: f64) -> Vec<(String, String)> { - let env_value = JSValue::from_bits(env.to_bits()); - if env_value.is_undefined() { - return std::env::vars().collect(); - } - let Some(obj) = module_object_ptr(env) else { - execve_throw_invalid_arg_type("env", "of type object", env); - }; - let keys = crate::object::js_object_keys(obj); - let len = crate::array::js_array_length(keys); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - let key_value = crate::array::js_array_get_f64(keys, i); - let Some(key) = module_value_to_string(key_value) else { - execve_throw_invalid_env(env); - }; - if key.as_bytes().contains(&0) { - execve_throw_invalid_env(env); - } - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let value = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); - let Some(value_string) = module_value_to_string(value) else { - execve_throw_invalid_env(env); - }; - if value_string.as_bytes().contains(&0) { - execve_throw_invalid_env(env); - } - out.push((key, value_string)); - } - out -} - -#[no_mangle] -pub extern "C" fn js_process_execve(exec_path: f64, args: f64, env: f64) -> f64 { - let Some(path) = module_value_to_string(exec_path) else { - execve_throw_invalid_arg_type("execPath", "of type string", exec_path); - }; - if path.as_bytes().contains(&0) { - execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)); - } - let argv = execve_parse_args(args); - let env_pairs = execve_parse_env(env); - - #[cfg(unix)] - { - let path_c = match std::ffi::CString::new(path.as_str()) { - Ok(path_c) => path_c, - Err(_) => execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)), - }; - let argv_c: Vec = argv - .iter() - .map(|arg| std::ffi::CString::new(arg.as_str()).unwrap()) - .collect(); - let env_c: Vec = env_pairs - .iter() - .map(|(key, value)| std::ffi::CString::new(format!("{key}={value}")).unwrap()) - .collect(); - let mut argv_ptrs: Vec<*const libc::c_char> = - argv_c.iter().map(|arg| arg.as_ptr()).collect(); - let mut env_ptrs: Vec<*const libc::c_char> = - env_c.iter().map(|entry| entry.as_ptr()).collect(); - argv_ptrs.push(std::ptr::null()); - env_ptrs.push(std::ptr::null()); - unsafe { - libc::execve(path_c.as_ptr(), argv_ptrs.as_ptr(), env_ptrs.as_ptr()); - libc::abort(); - } - } - - #[cfg(not(unix))] - { - let _ = (path, argv, env_pairs); - crate::fs::validate::throw_type_error_with_code( - "process.execve() is unavailable on this platform", - "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM", - ) - } -} - -/// process.resourceUsage() -> object with getrusage(RUSAGE_SELF) -/// counters matching Node's shape (#1376). Linux's `ru_maxrss` is in -/// kilobytes; macOS/BSD's is in bytes — Node normalizes Linux to bytes, -/// so we do too. Non-unix targets return zeroed fields. -#[no_mangle] -pub extern "C" fn js_process_resource_usage() -> f64 { - #[allow(unused_mut)] - let mut user_cpu: f64 = 0.0; - #[allow(unused_mut)] - let mut system_cpu: f64 = 0.0; - #[allow(unused_mut)] - let mut max_rss: f64 = 0.0; - #[allow(unused_mut)] - let mut shared_mem: f64 = 0.0; - #[allow(unused_mut)] - let mut unshared_data: f64 = 0.0; - #[allow(unused_mut)] - let mut unshared_stack: f64 = 0.0; - #[allow(unused_mut)] - let mut minor_faults: f64 = 0.0; - #[allow(unused_mut)] - let mut major_faults: f64 = 0.0; - #[allow(unused_mut)] - let mut swapped_out: f64 = 0.0; - #[allow(unused_mut)] - let mut fs_read: f64 = 0.0; - #[allow(unused_mut)] - let mut fs_write: f64 = 0.0; - #[allow(unused_mut)] - let mut ipc_sent: f64 = 0.0; - #[allow(unused_mut)] - let mut ipc_recv: f64 = 0.0; - #[allow(unused_mut)] - let mut signals: f64 = 0.0; - #[allow(unused_mut)] - let mut vcsw: f64 = 0.0; - #[allow(unused_mut)] - let mut ivcsw: f64 = 0.0; - - #[cfg(unix)] - { - let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; - if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 { - user_cpu = (usage.ru_utime.tv_sec as f64) * 1_000_000.0 + usage.ru_utime.tv_usec as f64; - system_cpu = - (usage.ru_stime.tv_sec as f64) * 1_000_000.0 + usage.ru_stime.tv_usec as f64; - #[cfg(target_os = "linux")] - { - max_rss = (usage.ru_maxrss as f64) * 1024.0; - } - #[cfg(not(target_os = "linux"))] - { - max_rss = usage.ru_maxrss as f64; - } - shared_mem = usage.ru_ixrss as f64; - unshared_data = usage.ru_idrss as f64; - unshared_stack = usage.ru_isrss as f64; - minor_faults = usage.ru_minflt as f64; - major_faults = usage.ru_majflt as f64; - swapped_out = usage.ru_nswap as f64; - fs_read = usage.ru_inblock as f64; - fs_write = usage.ru_oublock as f64; - ipc_sent = usage.ru_msgsnd as f64; - ipc_recv = usage.ru_msgrcv as f64; - signals = usage.ru_nsignals as f64; - vcsw = usage.ru_nvcsw as f64; - ivcsw = usage.ru_nivcsw as f64; - } - } - - let obj = crate::object::js_object_alloc(0, 16); - let set_field = |name: &str, value: f64| { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(obj, key, value); - }; - set_field("userCPUTime", user_cpu); - set_field("systemCPUTime", system_cpu); - set_field("maxRSS", max_rss); - set_field("sharedMemorySize", shared_mem); - set_field("unsharedDataSize", unshared_data); - set_field("unsharedStackSize", unshared_stack); - set_field("minorPageFault", minor_faults); - set_field("majorPageFault", major_faults); - set_field("swappedOut", swapped_out); - set_field("fsRead", fs_read); - set_field("fsWrite", fs_write); - set_field("ipcSent", ipc_sent); - set_field("ipcReceived", ipc_recv); - set_field("signalsCount", signals); - set_field("voluntaryContextSwitches", vcsw); - set_field("involuntaryContextSwitches", ivcsw); - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -/// process.title -> string. Returns the value set via the setter, or -/// falls back to argv[0]. -#[no_mangle] -pub extern "C" fn js_process_title() -> f64 { - use crate::value::JSValue; - let stored: Option = PROCESS_TITLE.with(|c| c.borrow().clone()); - let s = stored.unwrap_or_else(|| std::env::args().next().unwrap_or_default()); - let bytes = s.as_bytes(); - let ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -/// process.title = value — coerces to string and stores in the cell. -#[no_mangle] -pub extern "C" fn js_process_set_title(value: f64) { - let ptr = crate::value::js_jsvalue_to_string(value); - let s = if ptr.is_null() { - String::new() - } else { - unsafe { - let header = &*ptr; - let len = header.byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } - }; - #[cfg(target_os = "linux")] - { - let mut buf = [0i8; 16]; - let src = s.as_bytes(); - let copy_len = std::cmp::min(src.len(), 15); - for i in 0..copy_len { - buf[i] = src[i] as i8; - } - unsafe { - libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0); - } - } - PROCESS_TITLE.with(|c| *c.borrow_mut() = Some(s)); -} - -/// process.umask() -> number. Returns the current file-mode creation mask -/// without modifying it. POSIX's `umask` syscall has no read-only form, so -/// we set the mask to 0, capture the previous value, then restore it. -#[no_mangle] -pub extern "C" fn js_process_umask() -> f64 { - #[cfg(unix)] - unsafe { - let prev = libc::umask(0); - libc::umask(prev); - prev as f64 - } - #[cfg(not(unix))] - { - 0.0 - } -} - -/// process.umask(mask) -> number. Validates and parses `mask` the way Node's -/// `process.umask` (`parseMode`) does, sets the file-mode creation mask, and -/// returns the previous value (#2920). -/// -/// Node accepts either a 32-bit unsigned integer or an octal string: -/// - a non-number / non-string (`null`, object, boolean, …) throws -/// `TypeError [ERR_INVALID_ARG_TYPE]` ("must be of type number"); `null` -/// reports as `Received undefined` to match Node's `parseMode`; -/// - an octal string (`"077"`) is parsed via radix-8 `parseInt`; a string that -/// is not all-octal-digits (empty, `"abc"`, `"8"`, `"0xff"`, leading/trailing -/// whitespace) throws `TypeError [ERR_INVALID_ARG_VALUE]`; -/// - a non-integer / `NaN` / `Infinity` number throws -/// `RangeError [ERR_OUT_OF_RANGE]` ("must be an integer"); -/// - a value `< 0` or `> 4294967295` (either form) throws -/// `RangeError [ERR_OUT_OF_RANGE]` ("must be >= 0 && <= 4294967295"). -/// -/// An explicit `undefined` is handled at the call site as the read-only -/// no-argument form (so `js_process_umask` is called instead), matching Node's -/// `umask(undefined)` no-op-returns-current behavior. -#[no_mangle] -pub extern "C" fn js_process_umask_set(mask: f64) -> f64 { - // An explicit `undefined` argument is the read-only form (Node: - // `umask(undefined)` returns the current mask without changing it). - if JSValue::from_bits(mask.to_bits()).is_undefined() { - return js_process_umask(); - } - let parsed = parse_umask_mask(mask); - #[cfg(unix)] - unsafe { - libc::umask(parsed as libc::mode_t) as f64 - } - #[cfg(not(unix))] - { - let _ = parsed; - 0.0 - } -} - -/// Node's `parseMode("mask", value)` for `process.umask`. Diverges via -/// `js_throw` on an invalid value; otherwise returns the validated 32-bit -/// unsigned mask. -fn parse_umask_mask(mask: f64) -> u32 { - use crate::fs::validate::{ - describe_received, is_numeric, throw_range_error_named, throw_type_error_with_code, - }; - let jv = JSValue::from_bits(mask.to_bits()); - - if jv.is_any_string() { - let s = read_js_string_lossy(mask); - // Node parses the string with radix 8 (`parseInt(str, 8)`) but only - // after asserting the whole string is octal digits — leading/trailing - // whitespace, prefixes, empty, or non-octal chars are rejected. - let valid = !s.is_empty() && s.bytes().all(|b| (b'0'..=b'7').contains(&b)); - let parsed = if valid { - u64::from_str_radix(&s, 8).ok() - } else { - None - }; - match parsed { - Some(n) if n <= u32::MAX as u64 => return n as u32, - Some(n) => { - let message = format!( - "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", - n - ); - throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); - } - None => { - let message = format!( - "The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received '{}'", - s - ); - throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); - } - } - } - - if !is_numeric(jv) { - // Node's `parseMode` treats `null` like a missing value here, so its - // ERR_INVALID_ARG_TYPE renders `Received undefined`. - let received = if jv.is_null() { - "undefined".to_string() - } else { - describe_received(mask) - }; - let message = format!( - "The \"mask\" argument must be of type number. Received {}", - received - ); - throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - - let n = if jv.is_int32() { - jv.as_int32() as f64 - } else { - jv.as_number() - }; - if !(n.is_finite() && n.fract() == 0.0) { - let message = format!( - "The value of \"mask\" is out of range. It must be an integer. Received {}", - format_out_of_range_number(n) - ); - throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); - } - if n < 0.0 || n > u32::MAX as f64 { - let message = format!( - "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", - format_out_of_range_number(n) - ); - throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); - } - n as u32 -} - /// Render a number the way Node prints the `Received …` clause of an /// `ERR_OUT_OF_RANGE` message (no `type number (...)` wrapper). pub(crate) fn format_out_of_range_number(n: f64) -> String { @@ -1968,7 +1407,7 @@ pub(crate) fn format_out_of_range_number(n: f64) -> String { } /// Read a JS string (heap `StringHeader` or inline SSO) into a Rust `String`. -fn read_js_string_lossy(value: f64) -> String { +pub(super) fn read_js_string_lossy(value: f64) -> String { let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; if ptr.is_null() { return String::new(); diff --git a/crates/perry-runtime/src/process/exec_env.rs b/crates/perry-runtime/src/process/exec_env.rs new file mode 100644 index 0000000000..6a2fd318e8 --- /dev/null +++ b/crates/perry-runtime/src/process/exec_env.rs @@ -0,0 +1,305 @@ +//! `process.loadEnvFile()`, `process.chdir()` and `process.execve()` — the +//! entry points that read a `.env` file off disk, move the process's working +//! directory, or replace the process image (argv + environ) outright. +//! +//! Split verbatim out of the sibling [`super::env_misc`] module to keep that +//! file under the 2000-line gate. Pure code move — no behavior change. + +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +/// process.loadEnvFile(path?) — read a `.env`-formatted file from disk and +/// merge its `KEY=value` entries into `process.env`. Node 20.12+. With no +/// path, the default is `.env` in the current working directory. Throws a +/// Node-shaped `Error` (`code: "ENOENT"`, `syscall: "open"`) when the file +/// can't be opened. #2135 (#1399 follow-through): previously a no-op that +/// returned undefined so probe-and-call sites didn't crash; with +/// `process.env.X = v` now persisting via std::env (#1344), eager loading +/// is meaningful. +#[no_mangle] +pub extern "C" fn js_process_load_env_file(path_value: f64) { + let target = load_env_file_path(path_value); + let contents = match std::fs::read_to_string(&target) { + Ok(s) => s, + Err(err) => unsafe { + throw_load_env_file_open_error(&err, &target); + }, + }; + for (key, value) in crate::util_parse_env::parse_env(&contents) { + if std::env::var_os(&key).is_none() { + std::env::set_var(key, value); + } + } +} + +fn load_env_file_path(value: f64) -> String { + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_undefined() || jv.is_null() { + return ".env".to_string(); + } + unsafe { + validate_load_env_file_url(value); + crate::fs::decode_path_value(value) + .unwrap_or_else(|| crate::fs::validate::throw_invalid_path_arg("path", value)) + } +} + +unsafe fn validate_load_env_file_url(value: f64) { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return; + } + let obj = jv.as_pointer::() as *mut crate::object::ObjectHeader; + if obj.is_null() || !crate::url::is_url_object_shape(obj) { + return; + } + let protocol = crate::url::get_string_content(crate::object::js_object_get_field_f64( + obj, + crate::url::parse::URL_PROTOCOL, + )); + if protocol != "file:" { + throw_invalid_load_env_file_url_scheme(); + } + let pathname = crate::url::get_string_content(crate::object::js_object_get_field_f64( + obj, + crate::url::parse::URL_PATHNAME, + )); + if has_encoded_forward_slash(&pathname) { + crate::fs::validate::throw_type_error_with_code( + "File URL path must not include encoded / characters", + "ERR_INVALID_FILE_URL_PATH", + ); + } +} + +fn has_encoded_forward_slash(pathname: &str) -> bool { + let bytes = pathname.as_bytes(); + let mut i = 0usize; + while i + 2 < bytes.len() { + if bytes[i] == b'%' && bytes[i + 1] == b'2' && (bytes[i + 2] | 0x20) == b'f' { + return true; + } + i += 1; + } + false +} + +fn throw_invalid_load_env_file_url_scheme() -> ! { + crate::fs::validate::throw_type_error_with_code( + "The URL must be of scheme file", + "ERR_INVALID_URL_SCHEME", + ) +} + +unsafe fn throw_load_env_file_open_error(err: &std::io::Error, target: &str) -> ! { + use std::io::ErrorKind; + let code: &'static str = match err.kind() { + ErrorKind::NotFound => "ENOENT", + ErrorKind::PermissionDenied => "EACCES", + _ => "EIO", + }; + let desc = match code { + "ENOENT" => "no such file or directory", + "EACCES" => "permission denied", + _ => "i/o error", + }; + let message = format!("{code}: {desc}, open '{target}'"); + let msg_ptr = js_string_from_bytes(message.as_ptr(), message.len() as u32); + crate::node_submodules::register_error_code_pub(msg_ptr, code); + crate::node_submodules::register_error_syscall(msg_ptr, "open"); + crate::node_submodules::register_error_path(msg_ptr, target.to_string()); + let err_ptr = crate::error::js_error_new_with_message(msg_ptr); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err_ptr as i64)); +} + +// Issue #2013 — process-arg-validation helpers shared by `js_process_chdir` +// and `js_process_hrtime`. Sited here (not os.rs) so the process surface's +// validation logic stays under the 2000-line file gate as the os.rs splits +// progress. + +/// `process.chdir(value)` entry point that takes the full NaN-boxed +/// value. Throws `TypeError [ERR_INVALID_ARG_TYPE]` for any non-string +/// (matching Node), then re-dispatches to `js_process_chdir` with the +/// extracted `StringHeader`. The codegen now emits this entry instead +/// of the bare string-only one so a `process.chdir(123)` call throws +/// the right error code instead of garbage-deref'ing to an `ENOENT` +/// based on whatever bytes the numeric value masqueraded as. +#[no_mangle] +pub unsafe extern "C" fn js_process_chdir_jsv(value: f64) { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_any_string() { + let message = format!( + "The \"directory\" argument must be of type string. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; + crate::os::js_process_chdir(ptr); +} + +fn execve_throw_invalid_arg_type(name: &str, expected: &str, value: f64) -> ! { + let message = format!( + "The \"{}\" argument must be {}. Received {}", + name, + expected, + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +fn execve_received_value(value: f64) -> String { + let jv = JSValue::from_bits(value.to_bits()); + if crate::fs::validate::is_numeric(jv) { + let n = if jv.is_int32() { + jv.as_int32() as f64 + } else { + jv.as_number() + }; + return crate::fs::validate::format_received_number(n); + } + if let Some(value) = module_value_to_string(value) { + return format!("'{}'", value); + } + crate::fs::validate::describe_received(value) +} + +fn execve_env_received(value: f64) -> String { + let Some(obj) = module_object_ptr(value) else { + return crate::fs::validate::describe_received(value); + }; + let keys = crate::object::js_object_keys(obj); + let len = crate::array::js_array_length(keys); + let mut parts = Vec::new(); + for i in 0..len.min(3) { + let key_value = crate::array::js_array_get_f64(keys, i); + let key = module_value_to_string(key_value).unwrap_or_default(); + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + let field = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); + parts.push(format!("{}: {}", key, execve_received_value(field))); + } + if len > 3 { + parts.push("...".to_string()); + } + format!("{{ {} }}", parts.join(", ")) +} + +fn execve_throw_invalid_arg_value(name: &str, received: String) -> ! { + let message = format!( + "The argument '{}' must be a string without null bytes. Received {}", + name, received + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") +} + +fn execve_throw_invalid_env(value: f64) -> ! { + let message = format!( + "The argument 'env' must be an object with string keys and values without null bytes. Received {}", + execve_env_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") +} + +fn execve_parse_args(args: f64) -> Vec { + let args_value = JSValue::from_bits(args.to_bits()); + if args_value.is_undefined() { + return Vec::new(); + } + if !is_array_value(args_value) { + execve_throw_invalid_arg_type("args", "an instance of Array", args); + } + let arr = args_value.as_pointer::(); + let len = crate::array::js_array_length(arr); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let value = crate::array::js_array_get_f64(arr, i); + let Some(item) = module_value_to_string(value) else { + execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); + }; + if item.as_bytes().contains(&0) { + execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); + } + out.push(item); + } + out +} + +fn execve_parse_env(env: f64) -> Vec<(String, String)> { + let env_value = JSValue::from_bits(env.to_bits()); + if env_value.is_undefined() { + return std::env::vars().collect(); + } + let Some(obj) = module_object_ptr(env) else { + execve_throw_invalid_arg_type("env", "of type object", env); + }; + let keys = crate::object::js_object_keys(obj); + let len = crate::array::js_array_length(keys); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let key_value = crate::array::js_array_get_f64(keys, i); + let Some(key) = module_value_to_string(key_value) else { + execve_throw_invalid_env(env); + }; + if key.as_bytes().contains(&0) { + execve_throw_invalid_env(env); + } + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + let value = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); + let Some(value_string) = module_value_to_string(value) else { + execve_throw_invalid_env(env); + }; + if value_string.as_bytes().contains(&0) { + execve_throw_invalid_env(env); + } + out.push((key, value_string)); + } + out +} + +#[no_mangle] +pub extern "C" fn js_process_execve(exec_path: f64, args: f64, env: f64) -> f64 { + let Some(path) = module_value_to_string(exec_path) else { + execve_throw_invalid_arg_type("execPath", "of type string", exec_path); + }; + if path.as_bytes().contains(&0) { + execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)); + } + let argv = execve_parse_args(args); + let env_pairs = execve_parse_env(env); + + #[cfg(unix)] + { + let path_c = match std::ffi::CString::new(path.as_str()) { + Ok(path_c) => path_c, + Err(_) => execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)), + }; + let argv_c: Vec = argv + .iter() + .map(|arg| std::ffi::CString::new(arg.as_str()).unwrap()) + .collect(); + let env_c: Vec = env_pairs + .iter() + .map(|(key, value)| std::ffi::CString::new(format!("{key}={value}")).unwrap()) + .collect(); + let mut argv_ptrs: Vec<*const libc::c_char> = + argv_c.iter().map(|arg| arg.as_ptr()).collect(); + let mut env_ptrs: Vec<*const libc::c_char> = + env_c.iter().map(|entry| entry.as_ptr()).collect(); + argv_ptrs.push(std::ptr::null()); + env_ptrs.push(std::ptr::null()); + unsafe { + libc::execve(path_c.as_ptr(), argv_ptrs.as_ptr(), env_ptrs.as_ptr()); + libc::abort(); + } + } + + #[cfg(not(unix))] + { + let _ = (path, argv, env_pairs); + crate::fs::validate::throw_type_error_with_code( + "process.execve() is unavailable on this platform", + "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM", + ) + } +} diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index abc20e5161..9614d3524c 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -11,13 +11,11 @@ use std::pin::Pin; use std::sync::{Arc, Mutex, Once, OnceLock}; use std::task::{Context, Poll}; -use base64::{engine::general_purpose, Engine as _}; -use perry_runtime::array::js_array_get_f64; use perry_runtime::{ - js_array_alloc, js_array_is_array, js_array_length, js_array_push, js_closure_call0, - js_closure_call1, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, - js_object_get_field_by_name, js_object_set_field_by_name, js_string_from_bytes, ClosureHeader, - JSValue, ObjectHeader, StringHeader, + js_array_alloc, js_array_is_array, js_array_push, js_closure_call0, js_closure_call1, + js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, js_object_get_field_by_name, + js_object_set_field_by_name, js_string_from_bytes, ClosureHeader, JSValue, ObjectHeader, + StringHeader, }; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; @@ -28,92 +26,21 @@ use tokio_rustls::{rustls, server::TlsStream as ServerTlsStream, TlsAcceptor}; const TAG_UNDEFINED_BITS: u64 = 0x7FFC_0000_0000_0001; const TLS_DISPATCH_MISSING_BITS: u64 = TAG_UNDEFINED_BITS; -const DEFAULT_CIPHERS: &str = concat!( - "TLS_AES_256_GCM_SHA384:", - "TLS_CHACHA20_POLY1305_SHA256:", - "TLS_AES_128_GCM_SHA256:", - "ECDHE-RSA-AES128-GCM-SHA256:", - "ECDHE-ECDSA-AES128-GCM-SHA256:", - "ECDHE-RSA-AES256-GCM-SHA384:", - "ECDHE-ECDSA-AES256-GCM-SHA384:", - "DHE-RSA-AES128-GCM-SHA256:", - "ECDHE-RSA-AES128-SHA256:", - "DHE-RSA-AES128-SHA256:", - "ECDHE-RSA-AES256-SHA384:", - "DHE-RSA-AES256-SHA384:", - "ECDHE-RSA-AES256-SHA256:", - "DHE-RSA-AES256-SHA256:", - "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA" -); - -const NODE_TLS_CIPHERS: &[&str] = &[ - "aes128-gcm-sha256", - "aes128-sha", - "aes128-sha256", - "aes256-gcm-sha384", - "aes256-sha", - "aes256-sha256", - "dhe-psk-aes128-cbc-sha", - "dhe-psk-aes128-cbc-sha256", - "dhe-psk-aes128-gcm-sha256", - "dhe-psk-aes256-cbc-sha", - "dhe-psk-aes256-cbc-sha384", - "dhe-psk-aes256-gcm-sha384", - "dhe-psk-chacha20-poly1305", - "dhe-rsa-aes128-gcm-sha256", - "dhe-rsa-aes128-sha", - "dhe-rsa-aes128-sha256", - "dhe-rsa-aes256-gcm-sha384", - "dhe-rsa-aes256-sha", - "dhe-rsa-aes256-sha256", - "dhe-rsa-chacha20-poly1305", - "ecdhe-ecdsa-aes128-gcm-sha256", - "ecdhe-ecdsa-aes128-sha", - "ecdhe-ecdsa-aes128-sha256", - "ecdhe-ecdsa-aes256-gcm-sha384", - "ecdhe-ecdsa-aes256-sha", - "ecdhe-ecdsa-aes256-sha384", - "ecdhe-ecdsa-chacha20-poly1305", - "ecdhe-psk-aes128-cbc-sha", - "ecdhe-psk-aes128-cbc-sha256", - "ecdhe-psk-aes256-cbc-sha", - "ecdhe-psk-aes256-cbc-sha384", - "ecdhe-psk-chacha20-poly1305", - "ecdhe-rsa-aes128-gcm-sha256", - "ecdhe-rsa-aes128-sha", - "ecdhe-rsa-aes128-sha256", - "ecdhe-rsa-aes256-gcm-sha384", - "ecdhe-rsa-aes256-sha", - "ecdhe-rsa-aes256-sha384", - "ecdhe-rsa-chacha20-poly1305", - "psk-aes128-cbc-sha", - "psk-aes128-cbc-sha256", - "psk-aes128-gcm-sha256", - "psk-aes256-cbc-sha", - "psk-aes256-cbc-sha384", - "psk-aes256-gcm-sha384", - "psk-chacha20-poly1305", - "rsa-psk-aes128-cbc-sha", - "rsa-psk-aes128-cbc-sha256", - "rsa-psk-aes128-gcm-sha256", - "rsa-psk-aes256-cbc-sha", - "rsa-psk-aes256-cbc-sha384", - "rsa-psk-aes256-gcm-sha384", - "rsa-psk-chacha20-poly1305", - "srp-aes-128-cbc-sha", - "srp-aes-256-cbc-sha", - "srp-rsa-aes-128-cbc-sha", - "srp-rsa-aes-256-cbc-sha", - "tls_aes_128_ccm_8_sha256", - "tls_aes_128_ccm_sha256", - "tls_aes_128_gcm_sha256", - "tls_aes_256_gcm_sha384", - "tls_chacha20_poly1305_sha256", -]; - -static ROOT_CERTIFICATES: OnceLock> = OnceLock::new(); -static DEFAULT_CA_CERTIFICATES: OnceLock>>> = OnceLock::new(); -static NEXT_SECURE_CONTEXT_ID: OnceLock> = OnceLock::new(); +mod dispatch; +mod module_api; +mod secure_context; + +// Re-export the handle-dispatch and module-level entry points so +// `crate::tls::…` (and the `pub use tls::*` glob in `lib.rs`) keep resolving +// them exactly as before the split. +pub use dispatch::{dispatch_tls_handle, dispatch_tls_property, should_dispatch_tls_handle}; +pub use module_api::{ + js_tls_check_server_identity, js_tls_convert_alpn_protocols, js_tls_create_secure_context, + js_tls_get_ca_certificates, js_tls_get_ciphers, js_tls_native_dispatch, + js_tls_root_certificates, js_tls_secure_context_constructor, + js_tls_set_default_ca_certificates, +}; + static NEXT_TLS_HANDLE_ID: OnceLock> = OnceLock::new(); static TLS_SERVERS: OnceLock>> = OnceLock::new(); static TLS_SOCKETS: OnceLock>> = OnceLock::new(); @@ -124,13 +51,6 @@ static TLS_PENDING_EVENTS: OnceLock>> = OnceLock::new static TLS_GC_REGISTERED: Once = Once::new(); static RUSTLS_PROVIDER_INSTALLED: Once = Once::new(); -#[derive(Default)] -struct PemScan { - valid: usize, - had_pem_boundary: bool, - had_parse_error: bool, -} - struct TlsServerState { shutdown_tx: Option>, bound_port: u16, @@ -307,30 +227,6 @@ fn throw_error(message: &str, code: &'static str) -> ! { } } -fn der_to_pem(der: &[u8]) -> String { - let encoded = general_purpose::STANDARD.encode(der); - let mut pem = String::from("-----BEGIN CERTIFICATE-----\n"); - for chunk in encoded.as_bytes().chunks(64) { - pem.push_str(std::str::from_utf8(chunk).unwrap_or("")); - pem.push('\n'); - } - pem.push_str("-----END CERTIFICATE-----\n"); - pem -} - -fn load_native_certificates() -> Vec { - let native = rustls_native_certs::load_native_certs(); - let mut out = Vec::with_capacity(native.certs.len()); - for cert in native.certs { - out.push(der_to_pem(cert.as_ref())); - } - out -} - -fn root_certificates() -> &'static Vec { - ROOT_CERTIFICATES.get_or_init(load_native_certificates) -} - unsafe fn string_array(items: &[String]) -> *mut perry_runtime::ArrayHeader { let mut arr = js_array_alloc(items.len() as u32); for item in items { @@ -349,129 +245,6 @@ unsafe fn static_string_array(items: &[&str]) -> *mut perry_runtime::ArrayHeader arr } -fn ca_store() -> &'static Mutex>> { - DEFAULT_CA_CERTIFICATES.get_or_init(|| Mutex::new(None)) -} - -unsafe fn cert_list_from_array_value(value: f64) -> Result, ()> { - if !is_array_value(value) { - return Err(()); - } - let Some(addr) = pointer_addr(value) else { - return Err(()); - }; - let arr = addr as *const perry_runtime::ArrayHeader; - let len = js_array_length(arr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - let item = js_array_get_f64(arr, i); - let Some(s) = value_to_string(item) else { - return Err(()); - }; - out.push(s); - } - Ok(out) -} - -fn scan_pem_certificates(pems: &[String]) -> PemScan { - let mut scan = PemScan::default(); - for pem in pems { - if pem.contains("-----BEGIN CERTIFICATE-----") { - scan.had_pem_boundary = true; - } - let mut cursor = Cursor::new(pem.as_bytes()); - for cert in rustls_pemfile::certs(&mut cursor) { - match cert { - Ok(_) => scan.valid += 1, - Err(_) => scan.had_parse_error = true, - } - } - } - scan -} - -fn validate_ca_list_for_set(pems: &[String]) { - if pems.is_empty() { - return; - } - let scan = scan_pem_certificates(pems); - if scan.valid > 0 { - return; - } - if scan.had_pem_boundary || scan.had_parse_error { - throw_error( - "error:0488000D:PEM routines::ASN1 lib", - "ERR_OSSL_PEM_ASN1_LIB", - ); - } - throw_error( - "No valid certificates found in the provided array", - "ERR_CRYPTO_OPERATION_FAILED", - ); -} - -fn validate_ca_list_for_context(pems: &[String]) { - if pems.is_empty() { - return; - } - let scan = scan_pem_certificates(pems); - if scan.valid > 0 { - return; - } - if scan.had_pem_boundary || scan.had_parse_error { - throw_error( - "error:0488000D:PEM routines::ASN1 lib", - "ERR_OSSL_PEM_ASN1_LIB", - ); - } - throw_error( - "No valid certificates found in the provided array", - "ERR_CRYPTO_OPERATION_FAILED", - ); -} - -fn validate_tls_version(value: f64, label: &str) { - if js_is_undefined_or_null(value) { - return; - } - let text = unsafe { value_to_string(value).unwrap_or_default() }; - match text.as_str() { - "TLSv1.2" | "TLSv1.3" => {} - _ => { - let adjective = if label == "minVersion" { - "minimum" - } else { - "maximum" - }; - throw_type_error( - &format!("{text:?} is not a valid {adjective} TLS protocol version"), - "ERR_TLS_INVALID_PROTOCOL_VERSION", - ); - } - } -} - -unsafe fn ca_list_from_value(value: f64) -> Result, ()> { - if js_is_undefined_or_null(value) { - return Ok(Vec::new()); - } - if is_array_value(value) { - return cert_list_from_array_value(value); - } - let Some(s) = value_to_string(value) else { - return Err(()); - }; - Ok(vec![s]) -} - -fn next_secure_context_id() -> i64 { - let lock = NEXT_SECURE_CONTEXT_ID.get_or_init(|| Mutex::new(1)); - let mut guard = lock.lock().unwrap(); - let id = *guard; - *guard += 1; - id -} - fn servers() -> &'static Mutex> { TLS_SERVERS.get_or_init(|| Mutex::new(HashMap::new())) } @@ -788,124 +561,6 @@ unsafe fn build_server_config_from_options( Ok(Arc::new(config)) } -unsafe fn constructor_value(name: &str) -> f64 { - let module = b"tls"; - let prop = name.as_bytes(); - perry_runtime::object::js_native_module_property_by_name( - module.as_ptr(), - module.len(), - prop.as_ptr(), - prop.len(), - ) -} - -unsafe fn make_secure_context(options: f64) -> f64 { - let min_version = if js_is_undefined_or_null(options) { - undefined() - } else { - object_field(options, "minVersion") - }; - let max_version = if js_is_undefined_or_null(options) { - undefined() - } else { - object_field(options, "maxVersion") - }; - validate_tls_version(min_version, "minVersion"); - validate_tls_version(max_version, "maxVersion"); - - let ca_value = if js_is_undefined_or_null(options) { - undefined() - } else { - object_field(options, "ca") - }; - let ca = ca_list_from_value(ca_value).unwrap_or_else(|_| { - throw_type_error( - "The \"ca\" option must be a string, Buffer, or an array of those values", - "ERR_INVALID_ARG_TYPE", - ) - }); - validate_ca_list_for_context(&ca); - - let obj = js_object_alloc(0, 0); - set_field(obj, "context", next_secure_context_id() as f64); - set_field(obj, "_secureContext", next_secure_context_id() as f64); - set_str_field(obj, "minVersion", "TLSv1.2"); - set_str_field(obj, "maxVersion", "TLSv1.3"); - set_field(obj, "constructor", constructor_value("SecureContext")); - if !ca.is_empty() { - let ca_arr = string_array(&ca); - set_field(obj, "ca", js_nanbox_pointer(ca_arr as i64)); - } - js_nanbox_pointer(obj as i64) -} - -fn split_subject_alt_names(san: &str) -> Vec<(String, String)> { - let mut out = Vec::new(); - for part in san.split(',') { - let item = part.trim(); - if let Some(rest) = item.strip_prefix("DNS:") { - out.push(("DNS".to_string(), rest.trim().to_string())); - } else if let Some(rest) = item.strip_prefix("IP Address:") { - out.push(("IP".to_string(), rest.trim().to_string())); - } else if let Some(rest) = item.strip_prefix("IP:") { - out.push(("IP".to_string(), rest.trim().to_string())); - } - } - out -} - -fn hostname_is_ip(host: &str) -> bool { - host.parse::().is_ok() -} - -fn dns_matches(pattern: &str, host: &str) -> bool { - let pattern = pattern.trim_end_matches('.').to_ascii_lowercase(); - let host = host.trim_end_matches('.').to_ascii_lowercase(); - if pattern == host { - return true; - } - if let Some(suffix) = pattern.strip_prefix("*.") { - let Some(rest) = host.strip_suffix(suffix) else { - return false; - }; - return rest.ends_with('.') && rest[..rest.len().saturating_sub(1)].find('.').is_none(); - } - false -} - -unsafe fn cn_values(subject_value: f64) -> Vec { - let cn = object_field(subject_value, "CN"); - if js_is_undefined_or_null(cn) { - return Vec::new(); - } - if is_array_value(cn) { - if let Some(addr) = pointer_addr(cn) { - let arr = addr as *const perry_runtime::ArrayHeader; - let len = js_array_length(arr); - let mut out = Vec::new(); - for i in 0..len { - if let Some(s) = value_to_string(js_array_get_f64(arr, i)) { - out.push(s); - } - } - return out; - } - } - value_to_string(cn).into_iter().collect() -} - -unsafe fn make_altname_error(reason: String, host: &str, cert: f64) -> f64 { - let message = format!("Hostname/IP does not match certificate's altnames: {reason}"); - let obj = js_object_alloc(perry_runtime::error::CLASS_ID_ERROR, 0); - set_str_field(obj, "name", "Error"); - set_str_field(obj, "message", &message); - set_str_field(obj, "code", "ERR_TLS_CERT_ALTNAME_INVALID"); - set_str_field(obj, "reason", &reason); - set_str_field(obj, "host", host); - set_field(obj, "cert", cert); - js_nanbox_pointer(obj as i64) -} - unsafe fn build_error_object(message: &str) -> f64 { let obj = js_object_alloc(perry_runtime::error::CLASS_ID_ERROR, 0); set_str_field(obj, "name", "Error"); @@ -1350,311 +1005,6 @@ pub fn is_tls_socket_handle(handle: i64) -> bool { sockets().lock().unwrap().contains_key(&handle) } -fn tls_server_method_name(method: &str) -> bool { - matches!( - method, - "listen" - | "close" - | "address" - | "on" - | "addListener" - | "once" - | "off" - | "removeListener" - | "removeAllListeners" - | "listenerCount" - | "eventNames" - | "setSecureContext" - | "getTicketKeys" - | "setTicketKeys" - | "ref" - | "unref" - ) -} - -fn tls_socket_introspection_method_name(method: &str) -> bool { - matches!( - method, - "getProtocol" - | "getCipher" - | "getPeerCertificate" - | "getCertificate" - | "getSession" - | "isSessionReused" - | "exportKeyingMaterial" - | "setMaxSendFragment" - | "ref" - | "unref" - ) -} - -fn tls_socket_server_method_name(method: &str) -> bool { - tls_socket_introspection_method_name(method) - || matches!(method, |"write"| "end" - | "destroy" - | "on" - | "addListener" - | "once" - | "off" - | "removeListener" - | "removeAllListeners" - | "listenerCount" - | "eventNames") -} - -pub fn should_dispatch_tls_handle(handle: i64, method: &str) -> bool { - if is_tls_server_handle(handle) { - return tls_server_method_name(method); - } - sockets() - .lock() - .unwrap() - .get(&handle) - .map(|socket| { - if socket.server_side { - tls_socket_server_method_name(method) - } else { - tls_socket_introspection_method_name(method) - } - }) - .unwrap_or(false) -} - -unsafe fn event_arg(args: &[f64], idx: usize) -> i64 { - args.get(idx) - .copied() - .map(|value| (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64) - .unwrap_or(0) -} - -unsafe fn callback_bits(args: &[f64], idx: usize) -> i64 { - args.get(idx) - .copied() - .map(|v| v.to_bits() as i64) - .unwrap_or(TAG_UNDEFINED_BITS as i64) -} - -pub unsafe fn dispatch_tls_handle(handle: i64, method: &str, args: &[f64]) -> f64 { - if is_tls_server_handle(handle) { - match method { - "listen" => { - let port = args.first().copied().unwrap_or(0.0); - let cb = callback_bits(args, 1); - js_tls_server_listen(handle, port, cb); - return nanbox_handle(handle); - } - "close" => { - js_tls_server_close(handle, callback_bits(args, 0)); - return nanbox_handle(handle); - } - "address" => { - let ptr = js_tls_server_address(handle); - return f64::from_bits(perry_runtime::json::js_json_parse_or_null(ptr).bits()); - } - "on" | "addListener" => { - js_tls_server_on(handle, event_arg(args, 0), callback_bits(args, 1)); - return nanbox_handle(handle); - } - "once" => { - js_tls_server_once(handle, event_arg(args, 0), callback_bits(args, 1)); - return nanbox_handle(handle); - } - "off" | "removeListener" => { - js_tls_server_remove_listener(handle, event_arg(args, 0), callback_bits(args, 1)); - return nanbox_handle(handle); - } - "removeAllListeners" => { - js_tls_server_remove_all_listeners(handle, event_arg(args, 0)); - return nanbox_handle(handle); - } - "listenerCount" => return js_tls_server_listener_count(handle, event_arg(args, 0)), - "eventNames" => { - let ptr = js_tls_server_event_names(handle); - return f64::from_bits(perry_runtime::json::js_json_parse_or_null(ptr).bits()); - } - "setSecureContext" => { - js_tls_server_set_secure_context(handle, callback_bits(args, 0)); - return nanbox_handle(handle); - } - "getTicketKeys" => return js_tls_server_get_ticket_keys(handle), - "setTicketKeys" => { - js_tls_server_set_ticket_keys(handle, callback_bits(args, 0)); - return nanbox_handle(handle); - } - "ref" | "unref" => return nanbox_handle(handle), - _ => {} - } - } - - if is_tls_socket_handle(handle) { - match method { - "write" if !args.is_empty() => { - if let Some(socket) = sockets().lock().unwrap().get(&handle) { - if let Some(tx) = &socket.cmd_tx { - if let Some(bytes) = jsvalue_to_bytes(args[0]) { - let _ = tx.send(TlsSocketCommand::Write(bytes)); - } - } - } - return f64::from_bits(TAG_UNDEFINED_BITS); - } - "end" => { - if let Some(socket) = sockets().lock().unwrap().get(&handle) { - if let Some(tx) = &socket.cmd_tx { - if let Some(value) = args.first().copied() { - if let Some(bytes) = jsvalue_to_bytes(value) { - if !bytes.is_empty() { - let _ = tx.send(TlsSocketCommand::Write(bytes)); - } - } - } - let _ = tx.send(TlsSocketCommand::End); - } - } - return f64::from_bits(TAG_UNDEFINED_BITS); - } - "destroy" => { - if let Some(socket) = sockets().lock().unwrap().get(&handle) { - if let Some(tx) = &socket.cmd_tx { - let _ = tx.send(TlsSocketCommand::Destroy); - } - } - return f64::from_bits(TAG_UNDEFINED_BITS); - } - "on" | "addListener" => { - let event = string_from_header(event_arg(args, 0) as *const StringHeader); - if let Some(event) = event { - let cb = - pointer_addr(f64_from_raw_bits(callback_bits(args, 1))).unwrap_or(0) as i64; - register_listener(handle, event, cb, false); - } - return nanbox_handle(handle); - } - "once" => { - let event = string_from_header(event_arg(args, 0) as *const StringHeader); - if let Some(event) = event { - let cb = - pointer_addr(f64_from_raw_bits(callback_bits(args, 1))).unwrap_or(0) as i64; - register_listener(handle, event, cb, true); - } - return nanbox_handle(handle); - } - "off" | "removeListener" => { - if let Some(event) = string_from_header(event_arg(args, 0) as *const StringHeader) { - let cb = - pointer_addr(f64_from_raw_bits(callback_bits(args, 1))).unwrap_or(0) as i64; - remove_listener(handle, &event, cb); - } - return nanbox_handle(handle); - } - "removeAllListeners" => { - let event = string_from_header(event_arg(args, 0) as *const StringHeader); - remove_all_listeners(handle, event.as_deref()); - return nanbox_handle(handle); - } - "listenerCount" => { - return string_from_header(event_arg(args, 0) as *const StringHeader) - .map(|event| listener_count(handle, &event)) - .unwrap_or(0.0); - } - "eventNames" => return json_value_from_str(&event_names_json(handle)), - "getProtocol" => return js_tls_socket_get_protocol(handle), - "getCipher" => return js_tls_socket_get_cipher(handle), - "getPeerCertificate" => { - return js_tls_socket_get_peer_certificate( - handle, - args.first().copied().unwrap_or(undefined()), - ) - } - "getCertificate" => return js_tls_socket_get_certificate(handle), - "getSession" => return js_tls_socket_get_session(handle), - "isSessionReused" => return js_tls_socket_is_session_reused(handle), - "exportKeyingMaterial" => { - return js_tls_socket_export_keying_material( - handle, - args.first().copied().unwrap_or(0.0), - event_arg(args, 1), - ) - } - "setMaxSendFragment" => { - return js_tls_socket_set_max_send_fragment( - handle, - args.first().copied().unwrap_or(0.0), - ) - } - "ref" | "unref" => return nanbox_handle(handle), - _ => {} - } - } - undefined() -} - -pub unsafe fn dispatch_tls_property(handle: i64, property: &str) -> Option { - if is_tls_server_handle(handle) { - match property { - "listening" => { - let value = servers() - .lock() - .unwrap() - .get(&handle) - .map(|s| s.listening) - .unwrap_or(false); - return Some(f64::from_bits(JSValue::bool(value).bits())); - } - "listen" | "close" | "address" | "on" | "addListener" | "once" | "off" - | "removeListener" | "removeAllListeners" | "listenerCount" | "eventNames" - | "setSecureContext" | "getTicketKeys" | "setTicketKeys" | "ref" | "unref" => { - return Some(perry_runtime::object::js_class_method_bind( - raw_handle_value(handle), - property.as_ptr(), - property.len(), - )); - } - _ => {} - } - } - if is_tls_socket_handle(handle) { - match property { - "encrypted" => return Some(f64::from_bits(JSValue::bool(true).bits())), - "authorized" => { - let authorized = sockets() - .lock() - .unwrap() - .get(&handle) - .map(|s| s.authorized) - .unwrap_or(false); - return Some(f64::from_bits(JSValue::bool(authorized).bits())); - } - "authorizationError" => { - return Some(f64::from_bits(perry_runtime::JSValue::null().bits())) - } - "servername" => return Some(nanbox_str("localhost")), - "alpnProtocol" => return Some(f64::from_bits(perry_runtime::JSValue::null().bits())), - _ if sockets() - .lock() - .unwrap() - .get(&handle) - .map(|socket| { - if socket.server_side { - tls_socket_server_method_name(property) - } else { - tls_socket_introspection_method_name(property) - } - }) - .unwrap_or(false) => - { - return Some(perry_runtime::object::js_class_method_bind( - raw_handle_value(handle), - property.as_ptr(), - property.len(), - )); - } - _ => {} - } - } - None -} - #[no_mangle] pub unsafe extern "C" fn js_tls_process_pending() -> i32 { let mut events = { @@ -1764,256 +1114,3 @@ pub fn js_tls_has_active_handles() -> i32 { } 0 } - -pub unsafe extern "C" fn js_tls_get_ciphers() -> *mut perry_runtime::ArrayHeader { - static_string_array(NODE_TLS_CIPHERS) -} - -pub unsafe extern "C" fn js_tls_root_certificates() -> *mut perry_runtime::ArrayHeader { - string_array(root_certificates()) -} - -pub unsafe extern "C" fn js_tls_get_ca_certificates( - type_bits: i64, -) -> *mut perry_runtime::ArrayHeader { - let value = f64_from_raw_bits(type_bits); - let kind = if js_is_undefined_or_null(value) { - "default".to_string() - } else { - let jsv = JSValue::from_bits(value.to_bits()); - if !jsv.is_any_string() { - throw_type_error( - &format!( - "The \"type\" argument must be of type string. Received type {}", - type_name(value) - ), - "ERR_INVALID_ARG_TYPE", - ); - } - value_to_string(value).unwrap_or_default() - }; - - match kind.as_str() { - "default" => { - if let Some(certs) = ca_store().lock().unwrap().clone() { - string_array(&certs) - } else { - string_array(root_certificates()) - } - } - "system" | "bundled" => string_array(root_certificates()), - "extra" => string_array(&[]), - _ => throw_type_error( - &format!("The argument 'type' is invalid. Received {kind:?}"), - "ERR_INVALID_ARG_VALUE", - ), - } -} - -pub unsafe extern "C" fn js_tls_set_default_ca_certificates(certs_bits: i64) -> f64 { - let value = f64_from_raw_bits(certs_bits); - if !is_array_value(value) { - throw_type_error( - &format!( - "The \"certs\" argument must be an instance of Array. Received type {}", - type_name(value) - ), - "ERR_INVALID_ARG_TYPE", - ); - } - let certs = cert_list_from_array_value(value).unwrap_or_else(|_| { - throw_type_error( - "The \"certs\" argument must contain strings or Buffer-like values", - "ERR_INVALID_ARG_TYPE", - ) - }); - validate_ca_list_for_set(&certs); - *ca_store().lock().unwrap() = Some(certs); - undefined() -} - -pub unsafe extern "C" fn js_tls_check_server_identity(hostname_bits: i64, cert_bits: i64) -> f64 { - let hostname_value = f64_from_raw_bits(hostname_bits); - let cert = f64_from_raw_bits(cert_bits); - let host = value_to_string(hostname_value).unwrap_or_default(); - let host_is_ip = hostname_is_ip(&host); - let san = object_field_string(cert, "subjectaltname").unwrap_or_default(); - let san_entries = split_subject_alt_names(&san); - let ip_names: Vec = san_entries - .iter() - .filter(|(kind, _)| kind == "IP") - .map(|(_, value)| value.clone()) - .collect(); - let dns_names: Vec = san_entries - .iter() - .filter(|(kind, _)| kind == "DNS") - .map(|(_, value)| value.clone()) - .collect(); - - if host_is_ip { - if ip_names.iter().any(|ip| ip == &host) { - return undefined(); - } - let reason = format!( - "IP: {host} is not in the cert's list: {}", - ip_names.join(", ") - ); - return make_altname_error(reason, &host, cert); - } - - if !dns_names.is_empty() { - if dns_names.iter().any(|pattern| dns_matches(pattern, &host)) { - return undefined(); - } - let reason = format!("Host: {host}. is not in the cert's altnames: {san}"); - return make_altname_error(reason, &host, cert); - } - - let subject = object_field(cert, "subject"); - let cns = cn_values(subject); - if cns.iter().any(|cn| dns_matches(cn, &host)) { - return undefined(); - } - let reason = if cns.is_empty() { - format!("Host: {host}. is not cert's CN: ") - } else { - format!("Host: {host}. is not cert's CN: {}", cns.join(",")) - }; - make_altname_error(reason, &host, cert) -} - -pub unsafe extern "C" fn js_tls_create_secure_context(options_bits: i64) -> f64 { - make_secure_context(f64_from_raw_bits(options_bits)) -} - -pub unsafe extern "C" fn js_tls_secure_context_constructor(options_bits: i64) -> f64 { - make_secure_context(f64_from_raw_bits(options_bits)) -} - -#[no_mangle] -pub unsafe extern "C" fn js_tls_convert_alpn_protocols(protocols: f64, out: f64) -> f64 { - let mut encoded = Vec::new(); - if is_array_value(protocols) { - let array = - JSValue::from_bits(protocols.to_bits()).as_pointer::(); - let length = js_array_length(array); - for index in 0..length { - let protocol = js_array_get_f64(array, index); - if !JSValue::from_bits(protocol.to_bits()).is_any_string() { - throw_type_error( - "The \"protocols\" argument must contain only strings", - "ERR_INVALID_ARG_TYPE", - ); - } - let protocol = value_to_string(protocol).unwrap_or_default(); - if protocol.len() > u8::MAX as usize { - perry_runtime::fs::validate::throw_range_error_named( - "ALPN protocol names must not exceed 255 bytes", - "ERR_OUT_OF_RANGE", - ); - } - encoded.push(protocol.len() as u8); - encoded.extend_from_slice(protocol.as_bytes()); - } - } else if let Some(addr) = pointer_addr(protocols) { - if perry_runtime::buffer::is_registered_buffer(addr) - && !perry_runtime::buffer::is_any_array_buffer(addr) - { - let data = perry_runtime::buffer::js_native_buffer_data_ptr(protocols); - let length = perry_runtime::buffer::js_native_buffer_byte_len(protocols); - if !data.is_null() && length != 0 { - encoded.extend_from_slice(std::slice::from_raw_parts(data, length)); - } - } else if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { - let mut length = 0u32; - let data = - perry_runtime::buffer::js_value_buffer_or_typedarray_data(protocols, &mut length); - if !data.is_null() && length != 0 { - encoded.extend_from_slice(std::slice::from_raw_parts(data, length as usize)); - } - } else { - return undefined(); - } - } else { - // Node's internal helper ignores non-array/non-view values and leaves - // the target object untouched. - return undefined(); - } - - let Some(out_addr) = pointer_addr(out) else { - throw_type_error( - "The \"out\" argument must be of type object", - "ERR_INVALID_ARG_TYPE", - ); - }; - let buffer = perry_runtime::buffer::js_buffer_alloc(encoded.len() as i32, 0); - if !encoded.is_empty() { - std::ptr::copy_nonoverlapping( - encoded.as_ptr(), - perry_runtime::buffer::buffer_data_mut(buffer), - encoded.len(), - ); - } - set_field( - out_addr as *mut ObjectHeader, - "ALPNProtocols", - js_nanbox_pointer(buffer as i64), - ); - undefined() -} - -pub unsafe extern "C" fn js_tls_native_dispatch( - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - let method = - std::str::from_utf8(std::slice::from_raw_parts(method_ptr, method_len)).unwrap_or(""); - let arg = |idx: usize| -> f64 { - if idx < args_len && !args_ptr.is_null() { - *args_ptr.add(idx) - } else { - undefined() - } - }; - match method { - "getCiphers" => js_nanbox_pointer(js_tls_get_ciphers() as i64), - "rootCertificates" => js_nanbox_pointer(js_tls_root_certificates() as i64), - "getCACertificates" => { - js_nanbox_pointer(js_tls_get_ca_certificates(arg(0).to_bits() as i64) as i64) - } - "setDefaultCACertificates" => js_tls_set_default_ca_certificates(arg(0).to_bits() as i64), - "checkServerIdentity" => { - js_tls_check_server_identity(arg(0).to_bits() as i64, arg(1).to_bits() as i64) - } - "convertALPNProtocols" => js_tls_convert_alpn_protocols(arg(0), arg(1)), - "connect" => { - // Pass the args through raw — js_tls_connect resolves Node's - // `connect(options[, cb])` / `connect(port[, host][, options][, - // cb])` overloads plus the legacy positional form itself (#4971). - let handle = crate::net::js_tls_connect(arg(0), arg(1), arg(2), arg(3)); - if handle == 0 { - // Unresolvable args (e.g. no port) — undefined beats a - // NaN-boxed null pointer that every later method call - // trips over (#4971). - return undefined(); - } - record_tls_client_handle(handle); - nanbox_handle(handle) - } - "createServer" | "Server" => nanbox_handle(js_tls_create_server( - arg(0).to_bits() as i64, - arg(1).to_bits() as i64, - )), - "TLSSocket" => nanbox_handle(js_tls_tlssocket_constructor( - arg(0).to_bits() as i64, - arg(1).to_bits() as i64, - )), - "createSecureContext" | "SecureContext" => { - js_tls_create_secure_context(arg(0).to_bits() as i64) - } - "$DEFAULT_CIPHERS" => nanbox_str(DEFAULT_CIPHERS), - _ => f64::from_bits(TLS_DISPATCH_MISSING_BITS), - } -} diff --git a/crates/perry-stdlib/src/tls/dispatch.rs b/crates/perry-stdlib/src/tls/dispatch.rs new file mode 100644 index 0000000000..b33a41e3bb --- /dev/null +++ b/crates/perry-stdlib/src/tls/dispatch.rs @@ -0,0 +1,329 @@ +//! Handle-based method and property dispatch for the `node:tls` server and +//! socket surfaces. +//! +//! Split out of `tls.rs` to keep that file under the 2000-line lint cap +//! (`scripts/check_file_size.sh`). Items are moved verbatim; the three `pub` +//! entry points are re-exported from the parent module so +//! `crate::tls::should_dispatch_tls_handle` / `dispatch_tls_handle` / +//! `dispatch_tls_property` keep resolving unchanged. + +use perry_runtime::{JSValue, StringHeader}; + +use super::{ + event_names_json, f64_from_raw_bits, is_tls_server_handle, is_tls_socket_handle, + js_tls_server_address, js_tls_server_close, js_tls_server_event_names, + js_tls_server_get_ticket_keys, js_tls_server_listen, js_tls_server_listener_count, + js_tls_server_on, js_tls_server_once, js_tls_server_remove_all_listeners, + js_tls_server_remove_listener, js_tls_server_set_secure_context, js_tls_server_set_ticket_keys, + js_tls_socket_export_keying_material, js_tls_socket_get_certificate, js_tls_socket_get_cipher, + js_tls_socket_get_peer_certificate, js_tls_socket_get_protocol, js_tls_socket_get_session, + js_tls_socket_is_session_reused, js_tls_socket_set_max_send_fragment, json_value_from_str, + jsvalue_to_bytes, listener_count, nanbox_handle, nanbox_str, pointer_addr, raw_handle_value, + register_listener, remove_all_listeners, remove_listener, servers, sockets, string_from_header, + undefined, TlsSocketCommand, TAG_UNDEFINED_BITS, +}; + +fn tls_server_method_name(method: &str) -> bool { + matches!( + method, + "listen" + | "close" + | "address" + | "on" + | "addListener" + | "once" + | "off" + | "removeListener" + | "removeAllListeners" + | "listenerCount" + | "eventNames" + | "setSecureContext" + | "getTicketKeys" + | "setTicketKeys" + | "ref" + | "unref" + ) +} + +fn tls_socket_introspection_method_name(method: &str) -> bool { + matches!( + method, + "getProtocol" + | "getCipher" + | "getPeerCertificate" + | "getCertificate" + | "getSession" + | "isSessionReused" + | "exportKeyingMaterial" + | "setMaxSendFragment" + | "ref" + | "unref" + ) +} + +fn tls_socket_server_method_name(method: &str) -> bool { + tls_socket_introspection_method_name(method) + || matches!(method, |"write"| "end" + | "destroy" + | "on" + | "addListener" + | "once" + | "off" + | "removeListener" + | "removeAllListeners" + | "listenerCount" + | "eventNames") +} + +pub fn should_dispatch_tls_handle(handle: i64, method: &str) -> bool { + if is_tls_server_handle(handle) { + return tls_server_method_name(method); + } + sockets() + .lock() + .unwrap() + .get(&handle) + .map(|socket| { + if socket.server_side { + tls_socket_server_method_name(method) + } else { + tls_socket_introspection_method_name(method) + } + }) + .unwrap_or(false) +} + +unsafe fn event_arg(args: &[f64], idx: usize) -> i64 { + args.get(idx) + .copied() + .map(|value| (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64) + .unwrap_or(0) +} + +unsafe fn callback_bits(args: &[f64], idx: usize) -> i64 { + args.get(idx) + .copied() + .map(|v| v.to_bits() as i64) + .unwrap_or(TAG_UNDEFINED_BITS as i64) +} + +pub unsafe fn dispatch_tls_handle(handle: i64, method: &str, args: &[f64]) -> f64 { + if is_tls_server_handle(handle) { + match method { + "listen" => { + let port = args.first().copied().unwrap_or(0.0); + let cb = callback_bits(args, 1); + js_tls_server_listen(handle, port, cb); + return nanbox_handle(handle); + } + "close" => { + js_tls_server_close(handle, callback_bits(args, 0)); + return nanbox_handle(handle); + } + "address" => { + let ptr = js_tls_server_address(handle); + return f64::from_bits(perry_runtime::json::js_json_parse_or_null(ptr).bits()); + } + "on" | "addListener" => { + js_tls_server_on(handle, event_arg(args, 0), callback_bits(args, 1)); + return nanbox_handle(handle); + } + "once" => { + js_tls_server_once(handle, event_arg(args, 0), callback_bits(args, 1)); + return nanbox_handle(handle); + } + "off" | "removeListener" => { + js_tls_server_remove_listener(handle, event_arg(args, 0), callback_bits(args, 1)); + return nanbox_handle(handle); + } + "removeAllListeners" => { + js_tls_server_remove_all_listeners(handle, event_arg(args, 0)); + return nanbox_handle(handle); + } + "listenerCount" => return js_tls_server_listener_count(handle, event_arg(args, 0)), + "eventNames" => { + let ptr = js_tls_server_event_names(handle); + return f64::from_bits(perry_runtime::json::js_json_parse_or_null(ptr).bits()); + } + "setSecureContext" => { + js_tls_server_set_secure_context(handle, callback_bits(args, 0)); + return nanbox_handle(handle); + } + "getTicketKeys" => return js_tls_server_get_ticket_keys(handle), + "setTicketKeys" => { + js_tls_server_set_ticket_keys(handle, callback_bits(args, 0)); + return nanbox_handle(handle); + } + "ref" | "unref" => return nanbox_handle(handle), + _ => {} + } + } + + if is_tls_socket_handle(handle) { + match method { + "write" if !args.is_empty() => { + if let Some(socket) = sockets().lock().unwrap().get(&handle) { + if let Some(tx) = &socket.cmd_tx { + if let Some(bytes) = jsvalue_to_bytes(args[0]) { + let _ = tx.send(TlsSocketCommand::Write(bytes)); + } + } + } + return f64::from_bits(TAG_UNDEFINED_BITS); + } + "end" => { + if let Some(socket) = sockets().lock().unwrap().get(&handle) { + if let Some(tx) = &socket.cmd_tx { + if let Some(value) = args.first().copied() { + if let Some(bytes) = jsvalue_to_bytes(value) { + if !bytes.is_empty() { + let _ = tx.send(TlsSocketCommand::Write(bytes)); + } + } + } + let _ = tx.send(TlsSocketCommand::End); + } + } + return f64::from_bits(TAG_UNDEFINED_BITS); + } + "destroy" => { + if let Some(socket) = sockets().lock().unwrap().get(&handle) { + if let Some(tx) = &socket.cmd_tx { + let _ = tx.send(TlsSocketCommand::Destroy); + } + } + return f64::from_bits(TAG_UNDEFINED_BITS); + } + "on" | "addListener" => { + let event = string_from_header(event_arg(args, 0) as *const StringHeader); + if let Some(event) = event { + let cb = + pointer_addr(f64_from_raw_bits(callback_bits(args, 1))).unwrap_or(0) as i64; + register_listener(handle, event, cb, false); + } + return nanbox_handle(handle); + } + "once" => { + let event = string_from_header(event_arg(args, 0) as *const StringHeader); + if let Some(event) = event { + let cb = + pointer_addr(f64_from_raw_bits(callback_bits(args, 1))).unwrap_or(0) as i64; + register_listener(handle, event, cb, true); + } + return nanbox_handle(handle); + } + "off" | "removeListener" => { + if let Some(event) = string_from_header(event_arg(args, 0) as *const StringHeader) { + let cb = + pointer_addr(f64_from_raw_bits(callback_bits(args, 1))).unwrap_or(0) as i64; + remove_listener(handle, &event, cb); + } + return nanbox_handle(handle); + } + "removeAllListeners" => { + let event = string_from_header(event_arg(args, 0) as *const StringHeader); + remove_all_listeners(handle, event.as_deref()); + return nanbox_handle(handle); + } + "listenerCount" => { + return string_from_header(event_arg(args, 0) as *const StringHeader) + .map(|event| listener_count(handle, &event)) + .unwrap_or(0.0); + } + "eventNames" => return json_value_from_str(&event_names_json(handle)), + "getProtocol" => return js_tls_socket_get_protocol(handle), + "getCipher" => return js_tls_socket_get_cipher(handle), + "getPeerCertificate" => { + return js_tls_socket_get_peer_certificate( + handle, + args.first().copied().unwrap_or(undefined()), + ) + } + "getCertificate" => return js_tls_socket_get_certificate(handle), + "getSession" => return js_tls_socket_get_session(handle), + "isSessionReused" => return js_tls_socket_is_session_reused(handle), + "exportKeyingMaterial" => { + return js_tls_socket_export_keying_material( + handle, + args.first().copied().unwrap_or(0.0), + event_arg(args, 1), + ) + } + "setMaxSendFragment" => { + return js_tls_socket_set_max_send_fragment( + handle, + args.first().copied().unwrap_or(0.0), + ) + } + "ref" | "unref" => return nanbox_handle(handle), + _ => {} + } + } + undefined() +} + +pub unsafe fn dispatch_tls_property(handle: i64, property: &str) -> Option { + if is_tls_server_handle(handle) { + match property { + "listening" => { + let value = servers() + .lock() + .unwrap() + .get(&handle) + .map(|s| s.listening) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(value).bits())); + } + "listen" | "close" | "address" | "on" | "addListener" | "once" | "off" + | "removeListener" | "removeAllListeners" | "listenerCount" | "eventNames" + | "setSecureContext" | "getTicketKeys" | "setTicketKeys" | "ref" | "unref" => { + return Some(perry_runtime::object::js_class_method_bind( + raw_handle_value(handle), + property.as_ptr(), + property.len(), + )); + } + _ => {} + } + } + if is_tls_socket_handle(handle) { + match property { + "encrypted" => return Some(f64::from_bits(JSValue::bool(true).bits())), + "authorized" => { + let authorized = sockets() + .lock() + .unwrap() + .get(&handle) + .map(|s| s.authorized) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(authorized).bits())); + } + "authorizationError" => { + return Some(f64::from_bits(perry_runtime::JSValue::null().bits())) + } + "servername" => return Some(nanbox_str("localhost")), + "alpnProtocol" => return Some(f64::from_bits(perry_runtime::JSValue::null().bits())), + _ if sockets() + .lock() + .unwrap() + .get(&handle) + .map(|socket| { + if socket.server_side { + tls_socket_server_method_name(property) + } else { + tls_socket_introspection_method_name(property) + } + }) + .unwrap_or(false) => + { + return Some(perry_runtime::object::js_class_method_bind( + raw_handle_value(handle), + property.as_ptr(), + property.len(), + )); + } + _ => {} + } + } + None +} diff --git a/crates/perry-stdlib/src/tls/module_api.rs b/crates/perry-stdlib/src/tls/module_api.rs new file mode 100644 index 0000000000..16ab08c9b8 --- /dev/null +++ b/crates/perry-stdlib/src/tls/module_api.rs @@ -0,0 +1,426 @@ +//! Module-level `node:tls` surface: the cipher catalogs, `checkServerIdentity`, +//! the CA-certificate accessors, `convertALPNProtocols` and the `tls.*` native +//! dispatch table. +//! +//! Split out of `tls.rs` to keep that file under the 2000-line lint cap +//! (`scripts/check_file_size.sh`). Items are moved verbatim; the `pub` entry +//! points are re-exported from the parent module so `crate::tls::…` (and the +//! `pub use tls::*` glob in `lib.rs`) keep resolving them unchanged. + +use perry_runtime::array::js_array_get_f64; +use perry_runtime::{js_array_length, js_nanbox_pointer, js_object_alloc, JSValue, ObjectHeader}; + +use super::secure_context::{ + ca_store, cert_list_from_array_value, make_secure_context, root_certificates, + validate_ca_list_for_set, +}; +use super::{ + f64_from_raw_bits, is_array_value, js_is_undefined_or_null, js_tls_create_server, + js_tls_tlssocket_constructor, nanbox_handle, nanbox_str, object_field, object_field_string, + pointer_addr, record_tls_client_handle, set_field, set_str_field, static_string_array, + string_array, throw_type_error, type_name, undefined, value_to_string, + TLS_DISPATCH_MISSING_BITS, +}; + +const DEFAULT_CIPHERS: &str = concat!( + "TLS_AES_256_GCM_SHA384:", + "TLS_CHACHA20_POLY1305_SHA256:", + "TLS_AES_128_GCM_SHA256:", + "ECDHE-RSA-AES128-GCM-SHA256:", + "ECDHE-ECDSA-AES128-GCM-SHA256:", + "ECDHE-RSA-AES256-GCM-SHA384:", + "ECDHE-ECDSA-AES256-GCM-SHA384:", + "DHE-RSA-AES128-GCM-SHA256:", + "ECDHE-RSA-AES128-SHA256:", + "DHE-RSA-AES128-SHA256:", + "ECDHE-RSA-AES256-SHA384:", + "DHE-RSA-AES256-SHA384:", + "ECDHE-RSA-AES256-SHA256:", + "DHE-RSA-AES256-SHA256:", + "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA" +); + +const NODE_TLS_CIPHERS: &[&str] = &[ + "aes128-gcm-sha256", + "aes128-sha", + "aes128-sha256", + "aes256-gcm-sha384", + "aes256-sha", + "aes256-sha256", + "dhe-psk-aes128-cbc-sha", + "dhe-psk-aes128-cbc-sha256", + "dhe-psk-aes128-gcm-sha256", + "dhe-psk-aes256-cbc-sha", + "dhe-psk-aes256-cbc-sha384", + "dhe-psk-aes256-gcm-sha384", + "dhe-psk-chacha20-poly1305", + "dhe-rsa-aes128-gcm-sha256", + "dhe-rsa-aes128-sha", + "dhe-rsa-aes128-sha256", + "dhe-rsa-aes256-gcm-sha384", + "dhe-rsa-aes256-sha", + "dhe-rsa-aes256-sha256", + "dhe-rsa-chacha20-poly1305", + "ecdhe-ecdsa-aes128-gcm-sha256", + "ecdhe-ecdsa-aes128-sha", + "ecdhe-ecdsa-aes128-sha256", + "ecdhe-ecdsa-aes256-gcm-sha384", + "ecdhe-ecdsa-aes256-sha", + "ecdhe-ecdsa-aes256-sha384", + "ecdhe-ecdsa-chacha20-poly1305", + "ecdhe-psk-aes128-cbc-sha", + "ecdhe-psk-aes128-cbc-sha256", + "ecdhe-psk-aes256-cbc-sha", + "ecdhe-psk-aes256-cbc-sha384", + "ecdhe-psk-chacha20-poly1305", + "ecdhe-rsa-aes128-gcm-sha256", + "ecdhe-rsa-aes128-sha", + "ecdhe-rsa-aes128-sha256", + "ecdhe-rsa-aes256-gcm-sha384", + "ecdhe-rsa-aes256-sha", + "ecdhe-rsa-aes256-sha384", + "ecdhe-rsa-chacha20-poly1305", + "psk-aes128-cbc-sha", + "psk-aes128-cbc-sha256", + "psk-aes128-gcm-sha256", + "psk-aes256-cbc-sha", + "psk-aes256-cbc-sha384", + "psk-aes256-gcm-sha384", + "psk-chacha20-poly1305", + "rsa-psk-aes128-cbc-sha", + "rsa-psk-aes128-cbc-sha256", + "rsa-psk-aes128-gcm-sha256", + "rsa-psk-aes256-cbc-sha", + "rsa-psk-aes256-cbc-sha384", + "rsa-psk-aes256-gcm-sha384", + "rsa-psk-chacha20-poly1305", + "srp-aes-128-cbc-sha", + "srp-aes-256-cbc-sha", + "srp-rsa-aes-128-cbc-sha", + "srp-rsa-aes-256-cbc-sha", + "tls_aes_128_ccm_8_sha256", + "tls_aes_128_ccm_sha256", + "tls_aes_128_gcm_sha256", + "tls_aes_256_gcm_sha384", + "tls_chacha20_poly1305_sha256", +]; + +fn split_subject_alt_names(san: &str) -> Vec<(String, String)> { + let mut out = Vec::new(); + for part in san.split(',') { + let item = part.trim(); + if let Some(rest) = item.strip_prefix("DNS:") { + out.push(("DNS".to_string(), rest.trim().to_string())); + } else if let Some(rest) = item.strip_prefix("IP Address:") { + out.push(("IP".to_string(), rest.trim().to_string())); + } else if let Some(rest) = item.strip_prefix("IP:") { + out.push(("IP".to_string(), rest.trim().to_string())); + } + } + out +} + +fn hostname_is_ip(host: &str) -> bool { + host.parse::().is_ok() +} + +fn dns_matches(pattern: &str, host: &str) -> bool { + let pattern = pattern.trim_end_matches('.').to_ascii_lowercase(); + let host = host.trim_end_matches('.').to_ascii_lowercase(); + if pattern == host { + return true; + } + if let Some(suffix) = pattern.strip_prefix("*.") { + let Some(rest) = host.strip_suffix(suffix) else { + return false; + }; + return rest.ends_with('.') && rest[..rest.len().saturating_sub(1)].find('.').is_none(); + } + false +} + +unsafe fn cn_values(subject_value: f64) -> Vec { + let cn = object_field(subject_value, "CN"); + if js_is_undefined_or_null(cn) { + return Vec::new(); + } + if is_array_value(cn) { + if let Some(addr) = pointer_addr(cn) { + let arr = addr as *const perry_runtime::ArrayHeader; + let len = js_array_length(arr); + let mut out = Vec::new(); + for i in 0..len { + if let Some(s) = value_to_string(js_array_get_f64(arr, i)) { + out.push(s); + } + } + return out; + } + } + value_to_string(cn).into_iter().collect() +} + +unsafe fn make_altname_error(reason: String, host: &str, cert: f64) -> f64 { + let message = format!("Hostname/IP does not match certificate's altnames: {reason}"); + let obj = js_object_alloc(perry_runtime::error::CLASS_ID_ERROR, 0); + set_str_field(obj, "name", "Error"); + set_str_field(obj, "message", &message); + set_str_field(obj, "code", "ERR_TLS_CERT_ALTNAME_INVALID"); + set_str_field(obj, "reason", &reason); + set_str_field(obj, "host", host); + set_field(obj, "cert", cert); + js_nanbox_pointer(obj as i64) +} + +pub unsafe extern "C" fn js_tls_get_ciphers() -> *mut perry_runtime::ArrayHeader { + static_string_array(NODE_TLS_CIPHERS) +} + +pub unsafe extern "C" fn js_tls_root_certificates() -> *mut perry_runtime::ArrayHeader { + string_array(root_certificates()) +} + +pub unsafe extern "C" fn js_tls_get_ca_certificates( + type_bits: i64, +) -> *mut perry_runtime::ArrayHeader { + let value = f64_from_raw_bits(type_bits); + let kind = if js_is_undefined_or_null(value) { + "default".to_string() + } else { + let jsv = JSValue::from_bits(value.to_bits()); + if !jsv.is_any_string() { + throw_type_error( + &format!( + "The \"type\" argument must be of type string. Received type {}", + type_name(value) + ), + "ERR_INVALID_ARG_TYPE", + ); + } + value_to_string(value).unwrap_or_default() + }; + + match kind.as_str() { + "default" => { + if let Some(certs) = ca_store().lock().unwrap().clone() { + string_array(&certs) + } else { + string_array(root_certificates()) + } + } + "system" | "bundled" => string_array(root_certificates()), + "extra" => string_array(&[]), + _ => throw_type_error( + &format!("The argument 'type' is invalid. Received {kind:?}"), + "ERR_INVALID_ARG_VALUE", + ), + } +} + +pub unsafe extern "C" fn js_tls_set_default_ca_certificates(certs_bits: i64) -> f64 { + let value = f64_from_raw_bits(certs_bits); + if !is_array_value(value) { + throw_type_error( + &format!( + "The \"certs\" argument must be an instance of Array. Received type {}", + type_name(value) + ), + "ERR_INVALID_ARG_TYPE", + ); + } + let certs = cert_list_from_array_value(value).unwrap_or_else(|_| { + throw_type_error( + "The \"certs\" argument must contain strings or Buffer-like values", + "ERR_INVALID_ARG_TYPE", + ) + }); + validate_ca_list_for_set(&certs); + *ca_store().lock().unwrap() = Some(certs); + undefined() +} + +pub unsafe extern "C" fn js_tls_check_server_identity(hostname_bits: i64, cert_bits: i64) -> f64 { + let hostname_value = f64_from_raw_bits(hostname_bits); + let cert = f64_from_raw_bits(cert_bits); + let host = value_to_string(hostname_value).unwrap_or_default(); + let host_is_ip = hostname_is_ip(&host); + let san = object_field_string(cert, "subjectaltname").unwrap_or_default(); + let san_entries = split_subject_alt_names(&san); + let ip_names: Vec = san_entries + .iter() + .filter(|(kind, _)| kind == "IP") + .map(|(_, value)| value.clone()) + .collect(); + let dns_names: Vec = san_entries + .iter() + .filter(|(kind, _)| kind == "DNS") + .map(|(_, value)| value.clone()) + .collect(); + + if host_is_ip { + if ip_names.iter().any(|ip| ip == &host) { + return undefined(); + } + let reason = format!( + "IP: {host} is not in the cert's list: {}", + ip_names.join(", ") + ); + return make_altname_error(reason, &host, cert); + } + + if !dns_names.is_empty() { + if dns_names.iter().any(|pattern| dns_matches(pattern, &host)) { + return undefined(); + } + let reason = format!("Host: {host}. is not in the cert's altnames: {san}"); + return make_altname_error(reason, &host, cert); + } + + let subject = object_field(cert, "subject"); + let cns = cn_values(subject); + if cns.iter().any(|cn| dns_matches(cn, &host)) { + return undefined(); + } + let reason = if cns.is_empty() { + format!("Host: {host}. is not cert's CN: ") + } else { + format!("Host: {host}. is not cert's CN: {}", cns.join(",")) + }; + make_altname_error(reason, &host, cert) +} + +pub unsafe extern "C" fn js_tls_create_secure_context(options_bits: i64) -> f64 { + make_secure_context(f64_from_raw_bits(options_bits)) +} + +pub unsafe extern "C" fn js_tls_secure_context_constructor(options_bits: i64) -> f64 { + make_secure_context(f64_from_raw_bits(options_bits)) +} + +#[no_mangle] +pub unsafe extern "C" fn js_tls_convert_alpn_protocols(protocols: f64, out: f64) -> f64 { + let mut encoded = Vec::new(); + if is_array_value(protocols) { + let array = + JSValue::from_bits(protocols.to_bits()).as_pointer::(); + let length = js_array_length(array); + for index in 0..length { + let protocol = js_array_get_f64(array, index); + if !JSValue::from_bits(protocol.to_bits()).is_any_string() { + throw_type_error( + "The \"protocols\" argument must contain only strings", + "ERR_INVALID_ARG_TYPE", + ); + } + let protocol = value_to_string(protocol).unwrap_or_default(); + if protocol.len() > u8::MAX as usize { + perry_runtime::fs::validate::throw_range_error_named( + "ALPN protocol names must not exceed 255 bytes", + "ERR_OUT_OF_RANGE", + ); + } + encoded.push(protocol.len() as u8); + encoded.extend_from_slice(protocol.as_bytes()); + } + } else if let Some(addr) = pointer_addr(protocols) { + if perry_runtime::buffer::is_registered_buffer(addr) + && !perry_runtime::buffer::is_any_array_buffer(addr) + { + let data = perry_runtime::buffer::js_native_buffer_data_ptr(protocols); + let length = perry_runtime::buffer::js_native_buffer_byte_len(protocols); + if !data.is_null() && length != 0 { + encoded.extend_from_slice(std::slice::from_raw_parts(data, length)); + } + } else if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { + let mut length = 0u32; + let data = + perry_runtime::buffer::js_value_buffer_or_typedarray_data(protocols, &mut length); + if !data.is_null() && length != 0 { + encoded.extend_from_slice(std::slice::from_raw_parts(data, length as usize)); + } + } else { + return undefined(); + } + } else { + // Node's internal helper ignores non-array/non-view values and leaves + // the target object untouched. + return undefined(); + } + + let Some(out_addr) = pointer_addr(out) else { + throw_type_error( + "The \"out\" argument must be of type object", + "ERR_INVALID_ARG_TYPE", + ); + }; + let buffer = perry_runtime::buffer::js_buffer_alloc(encoded.len() as i32, 0); + if !encoded.is_empty() { + std::ptr::copy_nonoverlapping( + encoded.as_ptr(), + perry_runtime::buffer::buffer_data_mut(buffer), + encoded.len(), + ); + } + set_field( + out_addr as *mut ObjectHeader, + "ALPNProtocols", + js_nanbox_pointer(buffer as i64), + ); + undefined() +} + +pub unsafe extern "C" fn js_tls_native_dispatch( + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let method = + std::str::from_utf8(std::slice::from_raw_parts(method_ptr, method_len)).unwrap_or(""); + let arg = |idx: usize| -> f64 { + if idx < args_len && !args_ptr.is_null() { + *args_ptr.add(idx) + } else { + undefined() + } + }; + match method { + "getCiphers" => js_nanbox_pointer(js_tls_get_ciphers() as i64), + "rootCertificates" => js_nanbox_pointer(js_tls_root_certificates() as i64), + "getCACertificates" => { + js_nanbox_pointer(js_tls_get_ca_certificates(arg(0).to_bits() as i64) as i64) + } + "setDefaultCACertificates" => js_tls_set_default_ca_certificates(arg(0).to_bits() as i64), + "checkServerIdentity" => { + js_tls_check_server_identity(arg(0).to_bits() as i64, arg(1).to_bits() as i64) + } + "convertALPNProtocols" => js_tls_convert_alpn_protocols(arg(0), arg(1)), + "connect" => { + // Pass the args through raw — js_tls_connect resolves Node's + // `connect(options[, cb])` / `connect(port[, host][, options][, + // cb])` overloads plus the legacy positional form itself (#4971). + let handle = crate::net::js_tls_connect(arg(0), arg(1), arg(2), arg(3)); + if handle == 0 { + // Unresolvable args (e.g. no port) — undefined beats a + // NaN-boxed null pointer that every later method call + // trips over (#4971). + return undefined(); + } + record_tls_client_handle(handle); + nanbox_handle(handle) + } + "createServer" | "Server" => nanbox_handle(js_tls_create_server( + arg(0).to_bits() as i64, + arg(1).to_bits() as i64, + )), + "TLSSocket" => nanbox_handle(js_tls_tlssocket_constructor( + arg(0).to_bits() as i64, + arg(1).to_bits() as i64, + )), + "createSecureContext" | "SecureContext" => { + js_tls_create_secure_context(arg(0).to_bits() as i64) + } + "$DEFAULT_CIPHERS" => nanbox_str(DEFAULT_CIPHERS), + _ => f64::from_bits(TLS_DISPATCH_MISSING_BITS), + } +} diff --git a/crates/perry-stdlib/src/tls/secure_context.rs b/crates/perry-stdlib/src/tls/secure_context.rs new file mode 100644 index 0000000000..d03a9fa45c --- /dev/null +++ b/crates/perry-stdlib/src/tls/secure_context.rs @@ -0,0 +1,227 @@ +//! SecureContext construction plus the CA / PEM catalog helpers for `node:tls`. +//! +//! Split out of `tls.rs` to keep that file under the 2000-line lint cap +//! (`scripts/check_file_size.sh`). Items are moved verbatim; the entry points +//! consumed by the parent module and by `tls::module_api` are widened to +//! `pub(super)` so the original call sites keep resolving. + +use std::io::Cursor; +use std::sync::{Mutex, OnceLock}; + +use base64::{engine::general_purpose, Engine as _}; +use perry_runtime::array::js_array_get_f64; +use perry_runtime::{js_array_length, js_nanbox_pointer, js_object_alloc}; + +use super::{ + is_array_value, js_is_undefined_or_null, object_field, pointer_addr, set_field, set_str_field, + string_array, throw_error, throw_type_error, undefined, value_to_string, +}; + +static ROOT_CERTIFICATES: OnceLock> = OnceLock::new(); +static DEFAULT_CA_CERTIFICATES: OnceLock>>> = OnceLock::new(); +static NEXT_SECURE_CONTEXT_ID: OnceLock> = OnceLock::new(); + +#[derive(Default)] +struct PemScan { + valid: usize, + had_pem_boundary: bool, + had_parse_error: bool, +} + +fn der_to_pem(der: &[u8]) -> String { + let encoded = general_purpose::STANDARD.encode(der); + let mut pem = String::from("-----BEGIN CERTIFICATE-----\n"); + for chunk in encoded.as_bytes().chunks(64) { + pem.push_str(std::str::from_utf8(chunk).unwrap_or("")); + pem.push('\n'); + } + pem.push_str("-----END CERTIFICATE-----\n"); + pem +} + +fn load_native_certificates() -> Vec { + let native = rustls_native_certs::load_native_certs(); + let mut out = Vec::with_capacity(native.certs.len()); + for cert in native.certs { + out.push(der_to_pem(cert.as_ref())); + } + out +} + +pub(super) fn root_certificates() -> &'static Vec { + ROOT_CERTIFICATES.get_or_init(load_native_certificates) +} + +pub(super) fn ca_store() -> &'static Mutex>> { + DEFAULT_CA_CERTIFICATES.get_or_init(|| Mutex::new(None)) +} + +pub(super) unsafe fn cert_list_from_array_value(value: f64) -> Result, ()> { + if !is_array_value(value) { + return Err(()); + } + let Some(addr) = pointer_addr(value) else { + return Err(()); + }; + let arr = addr as *const perry_runtime::ArrayHeader; + let len = js_array_length(arr); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let item = js_array_get_f64(arr, i); + let Some(s) = value_to_string(item) else { + return Err(()); + }; + out.push(s); + } + Ok(out) +} + +fn scan_pem_certificates(pems: &[String]) -> PemScan { + let mut scan = PemScan::default(); + for pem in pems { + if pem.contains("-----BEGIN CERTIFICATE-----") { + scan.had_pem_boundary = true; + } + let mut cursor = Cursor::new(pem.as_bytes()); + for cert in rustls_pemfile::certs(&mut cursor) { + match cert { + Ok(_) => scan.valid += 1, + Err(_) => scan.had_parse_error = true, + } + } + } + scan +} + +pub(super) fn validate_ca_list_for_set(pems: &[String]) { + if pems.is_empty() { + return; + } + let scan = scan_pem_certificates(pems); + if scan.valid > 0 { + return; + } + if scan.had_pem_boundary || scan.had_parse_error { + throw_error( + "error:0488000D:PEM routines::ASN1 lib", + "ERR_OSSL_PEM_ASN1_LIB", + ); + } + throw_error( + "No valid certificates found in the provided array", + "ERR_CRYPTO_OPERATION_FAILED", + ); +} + +fn validate_ca_list_for_context(pems: &[String]) { + if pems.is_empty() { + return; + } + let scan = scan_pem_certificates(pems); + if scan.valid > 0 { + return; + } + if scan.had_pem_boundary || scan.had_parse_error { + throw_error( + "error:0488000D:PEM routines::ASN1 lib", + "ERR_OSSL_PEM_ASN1_LIB", + ); + } + throw_error( + "No valid certificates found in the provided array", + "ERR_CRYPTO_OPERATION_FAILED", + ); +} + +fn validate_tls_version(value: f64, label: &str) { + if js_is_undefined_or_null(value) { + return; + } + let text = unsafe { value_to_string(value).unwrap_or_default() }; + match text.as_str() { + "TLSv1.2" | "TLSv1.3" => {} + _ => { + let adjective = if label == "minVersion" { + "minimum" + } else { + "maximum" + }; + throw_type_error( + &format!("{text:?} is not a valid {adjective} TLS protocol version"), + "ERR_TLS_INVALID_PROTOCOL_VERSION", + ); + } + } +} + +unsafe fn ca_list_from_value(value: f64) -> Result, ()> { + if js_is_undefined_or_null(value) { + return Ok(Vec::new()); + } + if is_array_value(value) { + return cert_list_from_array_value(value); + } + let Some(s) = value_to_string(value) else { + return Err(()); + }; + Ok(vec![s]) +} + +fn next_secure_context_id() -> i64 { + let lock = NEXT_SECURE_CONTEXT_ID.get_or_init(|| Mutex::new(1)); + let mut guard = lock.lock().unwrap(); + let id = *guard; + *guard += 1; + id +} + +unsafe fn constructor_value(name: &str) -> f64 { + let module = b"tls"; + let prop = name.as_bytes(); + perry_runtime::object::js_native_module_property_by_name( + module.as_ptr(), + module.len(), + prop.as_ptr(), + prop.len(), + ) +} + +pub(super) unsafe fn make_secure_context(options: f64) -> f64 { + let min_version = if js_is_undefined_or_null(options) { + undefined() + } else { + object_field(options, "minVersion") + }; + let max_version = if js_is_undefined_or_null(options) { + undefined() + } else { + object_field(options, "maxVersion") + }; + validate_tls_version(min_version, "minVersion"); + validate_tls_version(max_version, "maxVersion"); + + let ca_value = if js_is_undefined_or_null(options) { + undefined() + } else { + object_field(options, "ca") + }; + let ca = ca_list_from_value(ca_value).unwrap_or_else(|_| { + throw_type_error( + "The \"ca\" option must be a string, Buffer, or an array of those values", + "ERR_INVALID_ARG_TYPE", + ) + }); + validate_ca_list_for_context(&ca); + + let obj = js_object_alloc(0, 0); + set_field(obj, "context", next_secure_context_id() as f64); + set_field(obj, "_secureContext", next_secure_context_id() as f64); + set_str_field(obj, "minVersion", "TLSv1.2"); + set_str_field(obj, "maxVersion", "TLSv1.3"); + set_field(obj, "constructor", constructor_value("SecureContext")); + if !ca.is_empty() { + let ca_arr = string_array(&ca); + set_field(obj, "ca", js_nanbox_pointer(ca_arr as i64)); + } + js_nanbox_pointer(obj as i64) +} diff --git a/crates/perry-stdlib/src/worker_threads.rs b/crates/perry-stdlib/src/worker_threads.rs index cf85d9cd33..02c82bcb37 100644 --- a/crates/perry-stdlib/src/worker_threads.rs +++ b/crates/perry-stdlib/src/worker_threads.rs @@ -21,10 +21,13 @@ use perry_runtime::thread::{ }; use perry_runtime::value::JSValue; +mod broadcast_channel; mod channel_pump; mod direct_message; +mod message_port; mod parent_port; mod worker_options; +mod worker_pump; mod worker_surface; // Re-export the channel-pump entry points so `crate::worker_threads::*` @@ -32,6 +35,14 @@ mod worker_surface; pub use channel_pump::{ js_worker_threads_channels_has_pending, js_worker_threads_channels_process_pending, }; +// Same for the BroadcastChannel constructor and the main-thread pump entry +// points, which moved into sibling modules to keep this file under the +// 2000-line lint cap. +pub use broadcast_channel::js_worker_threads_broadcast_channel_new; +pub use worker_pump::{js_worker_threads_has_pending, js_worker_threads_process_pending}; + +use message_port::message_port_object; +use worker_pump::{deliver_parent_port_message, start_stdin_reader}; use worker_options::{apply_worker_env, restore_worker_env, WorkerOptions, WorkerResourceLimits}; use worker_surface::{ @@ -740,268 +751,6 @@ fn event_object(event_type: &str, target_bits: u64, data: Option) -> f64 { object_value(obj) } -/// Build a MessagePort JS object for a same-process channel. The id is also -/// stored on the object (hidden `__perryPortId` field) so `receiveMessageOnPort` -/// can recover it from the object reference. -fn message_port_object(port_id: u64) -> *mut perry_runtime::object::ObjectHeader { - let obj = perry_runtime::object::js_object_alloc(0, 0); - set_object_prototype(obj, constructor_prototype("MessagePort")); - let object_bits = object_value(obj).to_bits(); - set_object_field(obj, "constructor", get_global_constructor("MessagePort")); - set_object_field( - obj, - "postMessage", - port_bound_closure(port_post_message as *const u8, 2, port_id), - ); - set_object_field( - obj, - "on", - port_bound_closure(port_on as *const u8, 2, port_id), - ); - set_object_field( - obj, - "addListener", - port_bound_closure(port_on as *const u8, 2, port_id), - ); - set_object_field( - obj, - "once", - port_bound_closure(port_on as *const u8, 2, port_id), - ); - set_object_field( - obj, - "off", - port_bound_closure(port_off as *const u8, 2, port_id), - ); - set_object_field( - obj, - "removeListener", - port_bound_closure(port_off as *const u8, 2, port_id), - ); - set_object_field( - obj, - "addEventListener", - port_bound_closure(port_add_event_listener as *const u8, 2, port_id), - ); - set_object_field( - obj, - "removeEventListener", - port_bound_closure(port_remove_event_listener as *const u8, 2, port_id), - ); - set_object_field( - obj, - "close", - port_bound_closure(port_close as *const u8, 0, port_id), - ); - set_object_field( - obj, - "start", - port_bound_closure(port_start as *const u8, 0, port_id), - ); - set_object_field( - obj, - "ref", - closure_value(worker_threads_noop0 as *const u8, 0), - ); - set_object_field( - obj, - "unref", - closure_value(worker_threads_noop0 as *const u8, 0), - ); - set_object_field( - obj, - "hasRef", - closure_value(worker_threads_has_ref as *const u8, 0), - ); - set_object_field(obj, "__perryPortId", f64::from_bits(port_id)); - set_object_field(obj, "onmessage", js_null()); - set_object_field(obj, "onmessageerror", js_null()); - MESSAGE_PORTS.with(|ports| { - if let Some(state) = ports.borrow_mut().get_mut(&port_id) { - state.object_bits = object_bits; - } - }); - obj -} - -/// port.postMessage(value) — deliver to the peer port's inbox (#3157). -extern "C" fn port_post_message(closure: *const ClosureHeader, value: f64, _transfer: f64) -> f64 { - let port_id = port_id_from_closure(closure); - if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { - return js_worker_threads_post_message(value); - } - // Validate the full submitted graph: a marked object nested inside an - // otherwise cloneable container rejects the whole message. - if message_value_is_uncloneable(value, &mut HashSet::new()) { - throw_data_clone_error("object could not be cloned."); - } - let serialized = serialize_message(value); - MESSAGE_PORTS.with(|ports| { - let peer = { - let ports = ports.borrow(); - match ports.get(&port_id) { - Some(state) if !state.closed => state.peer, - _ => return, - } - }; - if let Some(peer_state) = ports.borrow_mut().get_mut(&peer) { - if !peer_state.closed { - peer_state.inbox.push_back(serialized); - } - } - }); - perry_runtime::event_pump::js_notify_main_thread(); - js_undefined() -} - -/// port.on(event, callback) / addListener / once (#3157). -extern "C" fn port_on(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { - let port_id = port_id_from_closure(closure); - let event_name = string_value_to_string(event).unwrap_or_default(); - if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { - let callback_ptr = perry_runtime::value::js_nanbox_get_pointer(callback) as i64; - return js_worker_threads_on(event.to_bits() as i64, callback_ptr); - } - let cb_bits = callback.to_bits(); - // A program that only uses MessageChannel never calls spawn_for_promise, so - // the runtime pump would otherwise never be registered and `main` would - // return before any queued `message` is delivered. Register it here (mirrors - // readline #347), so the event loop ticks and drains the inboxes. - crate::common::async_bridge::ensure_pump_registered(); - MESSAGE_PORTS.with(|ports| { - if let Some(state) = ports.borrow_mut().get_mut(&port_id) { - match event_name.as_str() { - "message" => { - state.message_cb = Some(cb_bits); - // Attaching a `message` listener implicitly starts the port. - state.started = true; - } - "close" => state.close_cb = Some(cb_bits), - _ => {} - } - } - }); - js_undefined() -} - -/// port.off(event) / removeListener (#3157). -extern "C" fn port_off(closure: *const ClosureHeader, event: f64, _callback: f64) -> f64 { - let port_id = port_id_from_closure(closure); - let event_name = string_value_to_string(event).unwrap_or_default(); - if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { - match event_name.as_str() { - "message" => MESSAGE_CALLBACK.with(|cb| *cb.borrow_mut() = None), - "close" => CLOSE_CALLBACK.with(|cb| *cb.borrow_mut() = None), - _ => {} - } - return js_undefined(); - } - MESSAGE_PORTS.with(|ports| { - if let Some(state) = ports.borrow_mut().get_mut(&port_id) { - match event_name.as_str() { - "message" => state.message_cb = None, - "close" => state.close_cb = None, - _ => {} - } - } - }); - js_undefined() -} - -/// port.addEventListener(event, callback) (#3598). -extern "C" fn port_add_event_listener( - closure: *const ClosureHeader, - event: f64, - callback: f64, -) -> f64 { - let port_id = port_id_from_closure(closure); - let event_name = string_value_to_string(event).unwrap_or_default(); - let Some(cb_bits) = callback_bits_from_value(callback) else { - return js_undefined(); - }; - crate::common::async_bridge::ensure_pump_registered(); - MESSAGE_PORTS.with(|ports| { - if let Some(state) = ports.borrow_mut().get_mut(&port_id) { - match event_name.as_str() { - "message" => { - state.started = true; - if !state.message_event_cbs.contains(&cb_bits) { - state.message_event_cbs.push(cb_bits); - } - } - "close" => { - if !state.close_event_cbs.contains(&cb_bits) { - state.close_event_cbs.push(cb_bits); - } - } - _ => {} - } - } - }); - js_undefined() -} - -/// port.removeEventListener(event, callback) (#3598). -extern "C" fn port_remove_event_listener( - closure: *const ClosureHeader, - event: f64, - callback: f64, -) -> f64 { - let port_id = port_id_from_closure(closure); - let event_name = string_value_to_string(event).unwrap_or_default(); - let Some(cb_bits) = callback_bits_from_value(callback) else { - return js_undefined(); - }; - MESSAGE_PORTS.with(|ports| { - if let Some(state) = ports.borrow_mut().get_mut(&port_id) { - match event_name.as_str() { - "message" => state.message_event_cbs.retain(|cb| *cb != cb_bits), - "close" => state.close_event_cbs.retain(|cb| *cb != cb_bits), - _ => {} - } - } - }); - js_undefined() -} - -/// port.start() — enable delivery of queued messages to the listener (#3157). -extern "C" fn port_start(closure: *const ClosureHeader) -> f64 { - let port_id = port_id_from_closure(closure); - MESSAGE_PORTS.with(|ports| { - if let Some(state) = ports.borrow_mut().get_mut(&port_id) { - state.started = true; - } - }); - js_undefined() -} - -/// port.close() — mark closed and queue `close` events on both ends (#3157). -extern "C" fn port_close(closure: *const ClosureHeader) -> f64 { - let port_id = port_id_from_closure(closure); - let peer_id = MESSAGE_PORTS.with(|ports| ports.borrow().get(&port_id).map(|state| state.peer)); - MESSAGE_PORTS.with(|ports| { - let mut ports = ports.borrow_mut(); - if let Some(state) = ports.get_mut(&port_id) { - if !state.closed { - state.close_pending = true; - } - state.closed = true; - state.inbox.clear(); - } - if let Some(peer_id) = peer_id { - if let Some(peer) = ports.get_mut(&peer_id) { - if !peer.closed { - peer.close_pending = true; - } - peer.closed = true; - peer.inbox.clear(); - } - } - }); - js_worker_threads_channels_process_pending(); - js_undefined() -} - /// worker_threads DataCloneError: matches Node's message for postMessage /// rejections of marked-uncloneable / marked-untransferable values (#3159). fn throw_data_clone_error(detail: &str) -> ! { @@ -1384,161 +1133,6 @@ pub extern "C" fn js_worker_threads_message_channel_new() -> f64 { object_value(obj) } -extern "C" fn broadcast_post_message(closure: *const ClosureHeader, value: f64) -> f64 { - let channel_id = port_id_from_closure(closure); - let channel_name = BROADCAST_CHANNELS.with(|channels| { - channels - .borrow() - .get(&channel_id) - .and_then(|state| (!state.closed).then(|| state.name.clone())) - }); - let Some(channel_name) = channel_name else { - throw_invalid_state_error("BroadcastChannel is closed"); - }; - if message_value_is_uncloneable(value, &mut HashSet::new()) { - throw_data_clone_error("object could not be cloned."); - } - let serialized = serialize_message(value); - BROADCAST_CHANNELS.with(|channels| { - for (id, state) in channels.borrow_mut().iter_mut() { - if *id != channel_id && !state.closed && state.name == channel_name { - state.inbox.push_back(serialized.clone()); - } - } - }); - queue_worker_threads_microtask(); - js_undefined() -} - -extern "C" fn broadcast_close(closure: *const ClosureHeader) -> f64 { - let channel_id = port_id_from_closure(closure); - BROADCAST_CHANNELS.with(|channels| { - if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { - state.closed = true; - state.inbox.clear(); - state.message_event_cbs.clear(); - } - }); - js_undefined() -} - -extern "C" fn broadcast_ref_or_unref(closure: *const ClosureHeader) -> f64 { - let channel_id = port_id_from_closure(closure); - BROADCAST_CHANNELS.with(|channels| match channels.borrow().get(&channel_id) { - Some(state) => f64::from_bits(state.object_bits), - None => js_undefined(), - }) -} - -extern "C" fn broadcast_add_event_listener( - closure: *const ClosureHeader, - event: f64, - callback: f64, -) -> f64 { - let channel_id = port_id_from_closure(closure); - let event_name = string_value_to_string(event).unwrap_or_default(); - let Some(cb_bits) = callback_bits_from_value(callback) else { - return js_undefined(); - }; - crate::common::async_bridge::ensure_pump_registered(); - BROADCAST_CHANNELS.with(|channels| { - if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { - if event_name == "message" && !state.message_event_cbs.contains(&cb_bits) { - state.message_event_cbs.push(cb_bits); - } - } - }); - js_undefined() -} - -extern "C" fn broadcast_remove_event_listener( - closure: *const ClosureHeader, - event: f64, - callback: f64, -) -> f64 { - let channel_id = port_id_from_closure(closure); - let event_name = string_value_to_string(event).unwrap_or_default(); - let Some(cb_bits) = callback_bits_from_value(callback) else { - return js_undefined(); - }; - BROADCAST_CHANNELS.with(|channels| { - if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { - if event_name == "message" { - state.message_event_cbs.retain(|cb| *cb != cb_bits); - } - } - }); - js_undefined() -} - -/// new worker_threads.BroadcastChannel(name) -#[no_mangle] -pub extern "C" fn js_worker_threads_broadcast_channel_new(name: f64) -> f64 { - ensure_environment_data_gc_scanner(); - crate::common::async_bridge::ensure_pump_registered(); - let id = NEXT_BROADCAST_ID.with(|n| { - let mut n = n.borrow_mut(); - let id = *n; - *n += 1; - id - }); - let name_value = string_coerce(name); - let name_string = string_value_to_string(name_value).unwrap_or_default(); - let obj = perry_runtime::object::js_object_alloc(0, 0); - set_object_prototype(obj, constructor_prototype("BroadcastChannel")); - let object_bits = object_value(obj).to_bits(); - set_object_field( - obj, - "constructor", - get_global_constructor("BroadcastChannel"), - ); - set_object_field( - obj, - "postMessage", - port_bound_closure(broadcast_post_message as *const u8, 1, id), - ); - set_object_field( - obj, - "close", - port_bound_closure(broadcast_close as *const u8, 0, id), - ); - set_object_field( - obj, - "ref", - port_bound_closure(broadcast_ref_or_unref as *const u8, 0, id), - ); - set_object_field( - obj, - "unref", - port_bound_closure(broadcast_ref_or_unref as *const u8, 0, id), - ); - set_object_field( - obj, - "addEventListener", - port_bound_closure(broadcast_add_event_listener as *const u8, 2, id), - ); - set_object_field( - obj, - "removeEventListener", - port_bound_closure(broadcast_remove_event_listener as *const u8, 2, id), - ); - set_object_field(obj, "onmessage", js_null()); - set_object_field(obj, "onmessageerror", js_null()); - set_object_field(obj, "name", name_value); - set_object_field(obj, "__perryBroadcastChannelId", f64::from_bits(id)); - BROADCAST_CHANNELS.with(|channels| { - channels.borrow_mut().insert( - id, - BroadcastChannelState { - name: name_string, - object_bits, - ..Default::default() - }, - ); - }); - object_value(obj) -} - #[no_mangle] pub extern "C" fn js_worker_threads_worker_new(entry_ptr: i64, options: f64) -> f64 { ensure_worker_gc_scanner(); @@ -1786,46 +1380,6 @@ pub extern "C" fn js_worker_threads_on(event_ptr: i64, callback: i64) -> f64 { js_undefined() } -/// Deliver one main→worker message to the in-worker `parentPort` listeners. -/// Fires the Node-style `MESSAGE_CALLBACK` with the raw payload AND any -/// Web-style `addEventListener("message", fn)` listeners with a `MessageEvent`. -/// Runs on the worker's own thread (its arena), so the value is deserialized -/// here and any event wrapper is allocated in this thread's arena. -fn deliver_parent_port_message(message: &SerializedValue) { - let bits = unsafe { deserialize_nanbox_on_current_thread(message) }; - let value = f64::from_bits(bits); - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let value_h = scope.root_nanbox_f64(value); - - if let Some(callback_ptr) = MESSAGE_CALLBACK.with(|cb| *cb.borrow()) { - let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call1(closure, value_h.get_nanbox_f64()); - } - - // Root the listener closures BEFORE allocating the `MessageEvent`: that - // allocation can trigger a moving GC, which rewrites the canonical - // `MESSAGE_EVENT_CALLBACKS` storage via the registered root scanner but - // would leave a plain `clone()` of the raw pointers stale. - let event_cbs = MESSAGE_EVENT_CALLBACKS.with(|cbs| { - cbs.borrow() - .iter() - .map(|&callback_ptr| { - scope.root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(callback_ptr)) - }) - .collect::>() - }); - if !event_cbs.is_empty() { - let event = event_object("message", 0, Some(value_h.get_nanbox_f64())); - let event_h = scope.root_nanbox_f64(event); - for callback_h in event_cbs { - let callback_ptr = - perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); - let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call1(closure, event_h.get_nanbox_f64()); - } - } -} - /// `parentPort.addEventListener("message", fn)` / `removeEventListener`. /// Web-style registration on the in-worker parent port. `add` adds the /// listener; otherwise removes it. @@ -1914,209 +1468,6 @@ fn scan_parent_port_event_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRoot }); } -/// Start the background stdin reader thread -fn start_stdin_reader() { - let already_started = STDIN_READER_STARTED.with(|s| { - let was = *s.borrow(); - *s.borrow_mut() = true; - was - }); - if already_started { - return; - } - - // Spawn a thread to read lines from stdin - // We use a regular thread (not tokio) because stdin reading is blocking - std::thread::spawn(move || { - let stdin = io::stdin(); - let reader = stdin.lock(); - for line in reader.lines() { - match line { - Ok(line) => { - if line.is_empty() { - continue; - } - // Queue the message for main thread processing - PENDING_MESSAGES.with(|q| { - q.borrow_mut().push(line); - }); - } - Err(_) => break, - } - } - // stdin EOF - STDIN_EOF.with(|eof| { - *eof.borrow_mut() = true; - }); - }); -} - -/// Process pending messages - called from main thread event loop -/// Returns number of messages processed -#[no_mangle] -pub extern "C" fn js_worker_threads_process_pending() -> i32 { - let mut processed = 0; - - let events: Vec = { - let mut q = PARENT_EVENTS.lock().unwrap(); - q.drain(..).collect() - }; - for event in events { - match event { - WorkerEvent::Online(worker_id) => { - dispatch_worker_event(worker_id, "online", None); - processed += 1; - } - WorkerEvent::Message(worker_id, message) => { - let bits = unsafe { deserialize_nanbox_on_current_thread(&message) }; - dispatch_worker_event(worker_id, "message", Some(f64::from_bits(bits))); - processed += 1; - } - WorkerEvent::Error(worker_id) => { - dispatch_worker_event(worker_id, "error", None); - processed += 1; - } - WorkerEvent::Exit(worker_id, code) => { - let terminate_promise = - if let Some(worker) = WORKERS.lock().unwrap().get_mut(&worker_id) { - worker.alive = false; - worker.terminate_promise.take() - } else { - None - }; - dispatch_worker_event(worker_id, "exit", Some(code as f64)); - if let Some(promise) = terminate_promise { - crate::common::async_bridge::queue_promise_resolution( - promise, - true, - (code as f64).to_bits(), - ); - } - processed += 1; - } - } - } - - // Collect messages to process - let messages: Vec = PENDING_MESSAGES.with(|q| { - let mut q = q.borrow_mut(); - q.drain(..).collect() - }); - - let callback = MESSAGE_CALLBACK.with(|cb| *cb.borrow()); - - if let Some(callback_ptr) = callback { - for msg in messages { - // JSON-parse the message string - let str_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let bits = unsafe { js_json_parse(str_ptr) }; - let parsed = f64::from_bits(bits); - - // Call the message callback with the parsed value - let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call1(closure, parsed); - processed += 1; - } - } - - // Check for EOF and fire close callback - let is_eof = STDIN_EOF.with(|eof| *eof.borrow()); - if is_eof { - let close_cb = CLOSE_CALLBACK.with(|cb| cb.borrow_mut().take()); - if let Some(callback_ptr) = close_cb { - let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call0(closure); - } - } - - processed -} - -/// Check if worker_threads has pending work (stdin reader active) -#[no_mangle] -pub extern "C" fn js_worker_threads_has_pending() -> i32 { - let started = STDIN_READER_STARTED.with(|s| *s.borrow()); - let eof = STDIN_EOF.with(|eof| *eof.borrow()); - let has_messages = PENDING_MESSAGES.with(|q| !q.borrow().is_empty()); - let has_worker_events = !PARENT_EVENTS.lock().unwrap().is_empty(); - let has_live_refed_worker = WORKERS - .lock() - .unwrap() - .values() - .any(|worker| worker.alive && worker.refed); - - if has_messages || has_worker_events || has_live_refed_worker || (started && !eof) { - 1 - } else { - 0 - } -} - -fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { - // Collect (callback, web_event) pairs, then invoke OUTSIDE the WORKERS lock — - // a listener may re-enter postMessage / terminate, which needs the lock again. - let callbacks: Vec<(u64, bool)> = { - let mut workers = WORKERS.lock().unwrap(); - let Some(worker) = workers.get_mut(&worker_id) else { - return; - }; - let Some(listeners) = worker.listeners.get_mut(event) else { - return; - }; - let callbacks = listeners - .iter() - .map(|listener| (listener.callback_bits, listener.web_event)) - .collect::>(); - listeners.retain(|listener| !listener.once); - callbacks - }; - - // Web-style `addEventListener` listeners receive a `MessageEvent` wrapper - // (with `.data`) for "message" events; Node-style `on` listeners receive the - // raw payload. Lazily build the event object only if a web listener exists. - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - // Root the callbacks BEFORE allocating the `MessageEvent` (or any value the - // listeners are called with): the allocation can trigger a moving GC, which - // rewrites the canonical `WorkerListener.callback_bits` via the worker root - // scanner but would leave this snapshot's raw bits stale. - let callbacks = callbacks - .into_iter() - .map(|(callback_bits, web_event)| { - ( - scope.root_nanbox_f64(f64::from_bits(callback_bits)), - web_event, - ) - }) - .collect::>(); - let arg_handle = arg.map(|a| scope.root_nanbox_f64(a)); - let needs_event = event == "message" && callbacks.iter().any(|(_, web)| *web); - let event_handle = if needs_event { - let data = arg_handle.as_ref().map(|h| h.get_nanbox_f64()); - let ev = event_object("message", 0, data); - Some(scope.root_nanbox_f64(ev)) - } else { - None - }; - - for (callback_h, web_event) in callbacks { - let closure_ptr = perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); - if closure_ptr == 0 { - continue; - } - let closure = closure_ptr as *const ClosureHeader; - let call_arg = if web_event && event == "message" { - event_handle.as_ref().map(|h| h.get_nanbox_f64()) - } else { - arg_handle.as_ref().map(|h| h.get_nanbox_f64()) - }; - if let Some(arg) = call_arg { - perry_runtime::closure::js_closure_call1(closure, arg); - } else { - perry_runtime::closure::js_closure_call0(closure); - } - } -} - // `#[used]` keepalive anchors (#3157/#3159) — these `#[no_mangle]` entry points // are emitted by codegen (native-table dispatch) and called only from generated // `.o`. The auto-optimize whole-program-LLVM rebuild internalizes + dead-strips diff --git a/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs b/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs new file mode 100644 index 0000000000..3f083c2397 --- /dev/null +++ b/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs @@ -0,0 +1,164 @@ +//! Same-process `BroadcastChannel` surface. +//! +//! Split out of `worker_threads.rs` to keep that file under the 2000-line lint +//! cap (`scripts/check_file_size.sh`). Items are moved verbatim; the +//! `#[no_mangle]` constructor is re-exported from the parent module so +//! `crate::worker_threads::js_worker_threads_broadcast_channel_new` (and the +//! `pub use worker_threads::*` glob in `lib.rs`) keeps resolving it. + +use super::*; + +extern "C" fn broadcast_post_message(closure: *const ClosureHeader, value: f64) -> f64 { + let channel_id = port_id_from_closure(closure); + let channel_name = BROADCAST_CHANNELS.with(|channels| { + channels + .borrow() + .get(&channel_id) + .and_then(|state| (!state.closed).then(|| state.name.clone())) + }); + let Some(channel_name) = channel_name else { + throw_invalid_state_error("BroadcastChannel is closed"); + }; + if message_value_is_uncloneable(value, &mut HashSet::new()) { + throw_data_clone_error("object could not be cloned."); + } + let serialized = serialize_message(value); + BROADCAST_CHANNELS.with(|channels| { + for (id, state) in channels.borrow_mut().iter_mut() { + if *id != channel_id && !state.closed && state.name == channel_name { + state.inbox.push_back(serialized.clone()); + } + } + }); + queue_worker_threads_microtask(); + js_undefined() +} + +extern "C" fn broadcast_close(closure: *const ClosureHeader) -> f64 { + let channel_id = port_id_from_closure(closure); + BROADCAST_CHANNELS.with(|channels| { + if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { + state.closed = true; + state.inbox.clear(); + state.message_event_cbs.clear(); + } + }); + js_undefined() +} + +extern "C" fn broadcast_ref_or_unref(closure: *const ClosureHeader) -> f64 { + let channel_id = port_id_from_closure(closure); + BROADCAST_CHANNELS.with(|channels| match channels.borrow().get(&channel_id) { + Some(state) => f64::from_bits(state.object_bits), + None => js_undefined(), + }) +} + +extern "C" fn broadcast_add_event_listener( + closure: *const ClosureHeader, + event: f64, + callback: f64, +) -> f64 { + let channel_id = port_id_from_closure(closure); + let event_name = string_value_to_string(event).unwrap_or_default(); + let Some(cb_bits) = callback_bits_from_value(callback) else { + return js_undefined(); + }; + crate::common::async_bridge::ensure_pump_registered(); + BROADCAST_CHANNELS.with(|channels| { + if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { + if event_name == "message" && !state.message_event_cbs.contains(&cb_bits) { + state.message_event_cbs.push(cb_bits); + } + } + }); + js_undefined() +} + +extern "C" fn broadcast_remove_event_listener( + closure: *const ClosureHeader, + event: f64, + callback: f64, +) -> f64 { + let channel_id = port_id_from_closure(closure); + let event_name = string_value_to_string(event).unwrap_or_default(); + let Some(cb_bits) = callback_bits_from_value(callback) else { + return js_undefined(); + }; + BROADCAST_CHANNELS.with(|channels| { + if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { + if event_name == "message" { + state.message_event_cbs.retain(|cb| *cb != cb_bits); + } + } + }); + js_undefined() +} + +/// new worker_threads.BroadcastChannel(name) +#[no_mangle] +pub extern "C" fn js_worker_threads_broadcast_channel_new(name: f64) -> f64 { + ensure_environment_data_gc_scanner(); + crate::common::async_bridge::ensure_pump_registered(); + let id = NEXT_BROADCAST_ID.with(|n| { + let mut n = n.borrow_mut(); + let id = *n; + *n += 1; + id + }); + let name_value = string_coerce(name); + let name_string = string_value_to_string(name_value).unwrap_or_default(); + let obj = perry_runtime::object::js_object_alloc(0, 0); + set_object_prototype(obj, constructor_prototype("BroadcastChannel")); + let object_bits = object_value(obj).to_bits(); + set_object_field( + obj, + "constructor", + get_global_constructor("BroadcastChannel"), + ); + set_object_field( + obj, + "postMessage", + port_bound_closure(broadcast_post_message as *const u8, 1, id), + ); + set_object_field( + obj, + "close", + port_bound_closure(broadcast_close as *const u8, 0, id), + ); + set_object_field( + obj, + "ref", + port_bound_closure(broadcast_ref_or_unref as *const u8, 0, id), + ); + set_object_field( + obj, + "unref", + port_bound_closure(broadcast_ref_or_unref as *const u8, 0, id), + ); + set_object_field( + obj, + "addEventListener", + port_bound_closure(broadcast_add_event_listener as *const u8, 2, id), + ); + set_object_field( + obj, + "removeEventListener", + port_bound_closure(broadcast_remove_event_listener as *const u8, 2, id), + ); + set_object_field(obj, "onmessage", js_null()); + set_object_field(obj, "onmessageerror", js_null()); + set_object_field(obj, "name", name_value); + set_object_field(obj, "__perryBroadcastChannelId", f64::from_bits(id)); + BROADCAST_CHANNELS.with(|channels| { + channels.borrow_mut().insert( + id, + BroadcastChannelState { + name: name_string, + object_bits, + ..Default::default() + }, + ); + }); + object_value(obj) +} diff --git a/crates/perry-stdlib/src/worker_threads/message_port.rs b/crates/perry-stdlib/src/worker_threads/message_port.rs new file mode 100644 index 0000000000..053698dee1 --- /dev/null +++ b/crates/perry-stdlib/src/worker_threads/message_port.rs @@ -0,0 +1,270 @@ +//! Same-process `MessagePort` object surface (#3157 / #3598). +//! +//! Split out of `worker_threads.rs` to keep that file under the 2000-line lint +//! cap (`scripts/check_file_size.sh`). Items are moved verbatim; +//! `message_port_object` is widened to `pub(super)` so the parent module's +//! `MessageChannel` / `moveMessagePortToContext` call sites keep resolving. + +use super::*; + +/// Build a MessagePort JS object for a same-process channel. The id is also +/// stored on the object (hidden `__perryPortId` field) so `receiveMessageOnPort` +/// can recover it from the object reference. +pub(super) fn message_port_object(port_id: u64) -> *mut perry_runtime::object::ObjectHeader { + let obj = perry_runtime::object::js_object_alloc(0, 0); + set_object_prototype(obj, constructor_prototype("MessagePort")); + let object_bits = object_value(obj).to_bits(); + set_object_field(obj, "constructor", get_global_constructor("MessagePort")); + set_object_field( + obj, + "postMessage", + port_bound_closure(port_post_message as *const u8, 2, port_id), + ); + set_object_field( + obj, + "on", + port_bound_closure(port_on as *const u8, 2, port_id), + ); + set_object_field( + obj, + "addListener", + port_bound_closure(port_on as *const u8, 2, port_id), + ); + set_object_field( + obj, + "once", + port_bound_closure(port_on as *const u8, 2, port_id), + ); + set_object_field( + obj, + "off", + port_bound_closure(port_off as *const u8, 2, port_id), + ); + set_object_field( + obj, + "removeListener", + port_bound_closure(port_off as *const u8, 2, port_id), + ); + set_object_field( + obj, + "addEventListener", + port_bound_closure(port_add_event_listener as *const u8, 2, port_id), + ); + set_object_field( + obj, + "removeEventListener", + port_bound_closure(port_remove_event_listener as *const u8, 2, port_id), + ); + set_object_field( + obj, + "close", + port_bound_closure(port_close as *const u8, 0, port_id), + ); + set_object_field( + obj, + "start", + port_bound_closure(port_start as *const u8, 0, port_id), + ); + set_object_field( + obj, + "ref", + closure_value(worker_threads_noop0 as *const u8, 0), + ); + set_object_field( + obj, + "unref", + closure_value(worker_threads_noop0 as *const u8, 0), + ); + set_object_field( + obj, + "hasRef", + closure_value(worker_threads_has_ref as *const u8, 0), + ); + set_object_field(obj, "__perryPortId", f64::from_bits(port_id)); + set_object_field(obj, "onmessage", js_null()); + set_object_field(obj, "onmessageerror", js_null()); + MESSAGE_PORTS.with(|ports| { + if let Some(state) = ports.borrow_mut().get_mut(&port_id) { + state.object_bits = object_bits; + } + }); + obj +} + +/// port.postMessage(value) — deliver to the peer port's inbox (#3157). +extern "C" fn port_post_message(closure: *const ClosureHeader, value: f64, _transfer: f64) -> f64 { + let port_id = port_id_from_closure(closure); + if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { + return js_worker_threads_post_message(value); + } + // Validate the full submitted graph: a marked object nested inside an + // otherwise cloneable container rejects the whole message. + if message_value_is_uncloneable(value, &mut HashSet::new()) { + throw_data_clone_error("object could not be cloned."); + } + let serialized = serialize_message(value); + MESSAGE_PORTS.with(|ports| { + let peer = { + let ports = ports.borrow(); + match ports.get(&port_id) { + Some(state) if !state.closed => state.peer, + _ => return, + } + }; + if let Some(peer_state) = ports.borrow_mut().get_mut(&peer) { + if !peer_state.closed { + peer_state.inbox.push_back(serialized); + } + } + }); + perry_runtime::event_pump::js_notify_main_thread(); + js_undefined() +} + +/// port.on(event, callback) / addListener / once (#3157). +extern "C" fn port_on(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { + let port_id = port_id_from_closure(closure); + let event_name = string_value_to_string(event).unwrap_or_default(); + if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { + let callback_ptr = perry_runtime::value::js_nanbox_get_pointer(callback) as i64; + return js_worker_threads_on(event.to_bits() as i64, callback_ptr); + } + let cb_bits = callback.to_bits(); + // A program that only uses MessageChannel never calls spawn_for_promise, so + // the runtime pump would otherwise never be registered and `main` would + // return before any queued `message` is delivered. Register it here (mirrors + // readline #347), so the event loop ticks and drains the inboxes. + crate::common::async_bridge::ensure_pump_registered(); + MESSAGE_PORTS.with(|ports| { + if let Some(state) = ports.borrow_mut().get_mut(&port_id) { + match event_name.as_str() { + "message" => { + state.message_cb = Some(cb_bits); + // Attaching a `message` listener implicitly starts the port. + state.started = true; + } + "close" => state.close_cb = Some(cb_bits), + _ => {} + } + } + }); + js_undefined() +} + +/// port.off(event) / removeListener (#3157). +extern "C" fn port_off(closure: *const ClosureHeader, event: f64, _callback: f64) -> f64 { + let port_id = port_id_from_closure(closure); + let event_name = string_value_to_string(event).unwrap_or_default(); + if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { + match event_name.as_str() { + "message" => MESSAGE_CALLBACK.with(|cb| *cb.borrow_mut() = None), + "close" => CLOSE_CALLBACK.with(|cb| *cb.borrow_mut() = None), + _ => {} + } + return js_undefined(); + } + MESSAGE_PORTS.with(|ports| { + if let Some(state) = ports.borrow_mut().get_mut(&port_id) { + match event_name.as_str() { + "message" => state.message_cb = None, + "close" => state.close_cb = None, + _ => {} + } + } + }); + js_undefined() +} + +/// port.addEventListener(event, callback) (#3598). +extern "C" fn port_add_event_listener( + closure: *const ClosureHeader, + event: f64, + callback: f64, +) -> f64 { + let port_id = port_id_from_closure(closure); + let event_name = string_value_to_string(event).unwrap_or_default(); + let Some(cb_bits) = callback_bits_from_value(callback) else { + return js_undefined(); + }; + crate::common::async_bridge::ensure_pump_registered(); + MESSAGE_PORTS.with(|ports| { + if let Some(state) = ports.borrow_mut().get_mut(&port_id) { + match event_name.as_str() { + "message" => { + state.started = true; + if !state.message_event_cbs.contains(&cb_bits) { + state.message_event_cbs.push(cb_bits); + } + } + "close" => { + if !state.close_event_cbs.contains(&cb_bits) { + state.close_event_cbs.push(cb_bits); + } + } + _ => {} + } + } + }); + js_undefined() +} + +/// port.removeEventListener(event, callback) (#3598). +extern "C" fn port_remove_event_listener( + closure: *const ClosureHeader, + event: f64, + callback: f64, +) -> f64 { + let port_id = port_id_from_closure(closure); + let event_name = string_value_to_string(event).unwrap_or_default(); + let Some(cb_bits) = callback_bits_from_value(callback) else { + return js_undefined(); + }; + MESSAGE_PORTS.with(|ports| { + if let Some(state) = ports.borrow_mut().get_mut(&port_id) { + match event_name.as_str() { + "message" => state.message_event_cbs.retain(|cb| *cb != cb_bits), + "close" => state.close_event_cbs.retain(|cb| *cb != cb_bits), + _ => {} + } + } + }); + js_undefined() +} + +/// port.start() — enable delivery of queued messages to the listener (#3157). +extern "C" fn port_start(closure: *const ClosureHeader) -> f64 { + let port_id = port_id_from_closure(closure); + MESSAGE_PORTS.with(|ports| { + if let Some(state) = ports.borrow_mut().get_mut(&port_id) { + state.started = true; + } + }); + js_undefined() +} + +/// port.close() — mark closed and queue `close` events on both ends (#3157). +extern "C" fn port_close(closure: *const ClosureHeader) -> f64 { + let port_id = port_id_from_closure(closure); + let peer_id = MESSAGE_PORTS.with(|ports| ports.borrow().get(&port_id).map(|state| state.peer)); + MESSAGE_PORTS.with(|ports| { + let mut ports = ports.borrow_mut(); + if let Some(state) = ports.get_mut(&port_id) { + if !state.closed { + state.close_pending = true; + } + state.closed = true; + state.inbox.clear(); + } + if let Some(peer_id) = peer_id { + if let Some(peer) = ports.get_mut(&peer_id) { + if !peer.closed { + peer.close_pending = true; + } + peer.closed = true; + peer.inbox.clear(); + } + } + }); + js_worker_threads_channels_process_pending(); + js_undefined() +} diff --git a/crates/perry-stdlib/src/worker_threads/worker_pump.rs b/crates/perry-stdlib/src/worker_threads/worker_pump.rs new file mode 100644 index 0000000000..be64b88e5c --- /dev/null +++ b/crates/perry-stdlib/src/worker_threads/worker_pump.rs @@ -0,0 +1,253 @@ +//! Main-thread pump for worker events plus the stdin IPC bridge. +//! +//! Split out of `worker_threads.rs` to keep that file under the 2000-line lint +//! cap (`scripts/check_file_size.sh`). Items are moved verbatim; +//! `deliver_parent_port_message` / `start_stdin_reader` are widened to +//! `pub(super)` and the two `#[no_mangle]` entry points are re-exported from +//! the parent module so `crate::worker_threads::js_worker_threads_*_pending` +//! keeps resolving. + +use super::*; + +/// Deliver one main→worker message to the in-worker `parentPort` listeners. +/// Fires the Node-style `MESSAGE_CALLBACK` with the raw payload AND any +/// Web-style `addEventListener("message", fn)` listeners with a `MessageEvent`. +/// Runs on the worker's own thread (its arena), so the value is deserialized +/// here and any event wrapper is allocated in this thread's arena. +pub(super) fn deliver_parent_port_message(message: &SerializedValue) { + let bits = unsafe { deserialize_nanbox_on_current_thread(message) }; + let value = f64::from_bits(bits); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let value_h = scope.root_nanbox_f64(value); + + if let Some(callback_ptr) = MESSAGE_CALLBACK.with(|cb| *cb.borrow()) { + let closure = callback_ptr as *const ClosureHeader; + perry_runtime::closure::js_closure_call1(closure, value_h.get_nanbox_f64()); + } + + // Root the listener closures BEFORE allocating the `MessageEvent`: that + // allocation can trigger a moving GC, which rewrites the canonical + // `MESSAGE_EVENT_CALLBACKS` storage via the registered root scanner but + // would leave a plain `clone()` of the raw pointers stale. + let event_cbs = MESSAGE_EVENT_CALLBACKS.with(|cbs| { + cbs.borrow() + .iter() + .map(|&callback_ptr| { + scope.root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(callback_ptr)) + }) + .collect::>() + }); + if !event_cbs.is_empty() { + let event = event_object("message", 0, Some(value_h.get_nanbox_f64())); + let event_h = scope.root_nanbox_f64(event); + for callback_h in event_cbs { + let callback_ptr = + perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); + let closure = callback_ptr as *const ClosureHeader; + perry_runtime::closure::js_closure_call1(closure, event_h.get_nanbox_f64()); + } + } +} + +/// Start the background stdin reader thread +pub(super) fn start_stdin_reader() { + let already_started = STDIN_READER_STARTED.with(|s| { + let was = *s.borrow(); + *s.borrow_mut() = true; + was + }); + if already_started { + return; + } + + // Spawn a thread to read lines from stdin + // We use a regular thread (not tokio) because stdin reading is blocking + std::thread::spawn(move || { + let stdin = io::stdin(); + let reader = stdin.lock(); + for line in reader.lines() { + match line { + Ok(line) => { + if line.is_empty() { + continue; + } + // Queue the message for main thread processing + PENDING_MESSAGES.with(|q| { + q.borrow_mut().push(line); + }); + } + Err(_) => break, + } + } + // stdin EOF + STDIN_EOF.with(|eof| { + *eof.borrow_mut() = true; + }); + }); +} + +/// Process pending messages - called from main thread event loop +/// Returns number of messages processed +#[no_mangle] +pub extern "C" fn js_worker_threads_process_pending() -> i32 { + let mut processed = 0; + + let events: Vec = { + let mut q = PARENT_EVENTS.lock().unwrap(); + q.drain(..).collect() + }; + for event in events { + match event { + WorkerEvent::Online(worker_id) => { + dispatch_worker_event(worker_id, "online", None); + processed += 1; + } + WorkerEvent::Message(worker_id, message) => { + let bits = unsafe { deserialize_nanbox_on_current_thread(&message) }; + dispatch_worker_event(worker_id, "message", Some(f64::from_bits(bits))); + processed += 1; + } + WorkerEvent::Error(worker_id) => { + dispatch_worker_event(worker_id, "error", None); + processed += 1; + } + WorkerEvent::Exit(worker_id, code) => { + let terminate_promise = + if let Some(worker) = WORKERS.lock().unwrap().get_mut(&worker_id) { + worker.alive = false; + worker.terminate_promise.take() + } else { + None + }; + dispatch_worker_event(worker_id, "exit", Some(code as f64)); + if let Some(promise) = terminate_promise { + crate::common::async_bridge::queue_promise_resolution( + promise, + true, + (code as f64).to_bits(), + ); + } + processed += 1; + } + } + } + + // Collect messages to process + let messages: Vec = PENDING_MESSAGES.with(|q| { + let mut q = q.borrow_mut(); + q.drain(..).collect() + }); + + let callback = MESSAGE_CALLBACK.with(|cb| *cb.borrow()); + + if let Some(callback_ptr) = callback { + for msg in messages { + // JSON-parse the message string + let str_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let bits = unsafe { js_json_parse(str_ptr) }; + let parsed = f64::from_bits(bits); + + // Call the message callback with the parsed value + let closure = callback_ptr as *const ClosureHeader; + perry_runtime::closure::js_closure_call1(closure, parsed); + processed += 1; + } + } + + // Check for EOF and fire close callback + let is_eof = STDIN_EOF.with(|eof| *eof.borrow()); + if is_eof { + let close_cb = CLOSE_CALLBACK.with(|cb| cb.borrow_mut().take()); + if let Some(callback_ptr) = close_cb { + let closure = callback_ptr as *const ClosureHeader; + perry_runtime::closure::js_closure_call0(closure); + } + } + + processed +} + +/// Check if worker_threads has pending work (stdin reader active) +#[no_mangle] +pub extern "C" fn js_worker_threads_has_pending() -> i32 { + let started = STDIN_READER_STARTED.with(|s| *s.borrow()); + let eof = STDIN_EOF.with(|eof| *eof.borrow()); + let has_messages = PENDING_MESSAGES.with(|q| !q.borrow().is_empty()); + let has_worker_events = !PARENT_EVENTS.lock().unwrap().is_empty(); + let has_live_refed_worker = WORKERS + .lock() + .unwrap() + .values() + .any(|worker| worker.alive && worker.refed); + + if has_messages || has_worker_events || has_live_refed_worker || (started && !eof) { + 1 + } else { + 0 + } +} + +fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { + // Collect (callback, web_event) pairs, then invoke OUTSIDE the WORKERS lock — + // a listener may re-enter postMessage / terminate, which needs the lock again. + let callbacks: Vec<(u64, bool)> = { + let mut workers = WORKERS.lock().unwrap(); + let Some(worker) = workers.get_mut(&worker_id) else { + return; + }; + let Some(listeners) = worker.listeners.get_mut(event) else { + return; + }; + let callbacks = listeners + .iter() + .map(|listener| (listener.callback_bits, listener.web_event)) + .collect::>(); + listeners.retain(|listener| !listener.once); + callbacks + }; + + // Web-style `addEventListener` listeners receive a `MessageEvent` wrapper + // (with `.data`) for "message" events; Node-style `on` listeners receive the + // raw payload. Lazily build the event object only if a web listener exists. + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + // Root the callbacks BEFORE allocating the `MessageEvent` (or any value the + // listeners are called with): the allocation can trigger a moving GC, which + // rewrites the canonical `WorkerListener.callback_bits` via the worker root + // scanner but would leave this snapshot's raw bits stale. + let callbacks = callbacks + .into_iter() + .map(|(callback_bits, web_event)| { + ( + scope.root_nanbox_f64(f64::from_bits(callback_bits)), + web_event, + ) + }) + .collect::>(); + let arg_handle = arg.map(|a| scope.root_nanbox_f64(a)); + let needs_event = event == "message" && callbacks.iter().any(|(_, web)| *web); + let event_handle = if needs_event { + let data = arg_handle.as_ref().map(|h| h.get_nanbox_f64()); + let ev = event_object("message", 0, data); + Some(scope.root_nanbox_f64(ev)) + } else { + None + }; + + for (callback_h, web_event) in callbacks { + let closure_ptr = perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); + if closure_ptr == 0 { + continue; + } + let closure = closure_ptr as *const ClosureHeader; + let call_arg = if web_event && event == "message" { + event_handle.as_ref().map(|h| h.get_nanbox_f64()) + } else { + arg_handle.as_ref().map(|h| h.get_nanbox_f64()) + }; + if let Some(arg) = call_arg { + perry_runtime::closure::js_closure_call1(closure, arg); + } else { + perry_runtime::closure::js_closure_call0(closure); + } + } +} diff --git a/crates/perry/src/commands/compile/library_search.rs b/crates/perry/src/commands/compile/library_search.rs index 9ecbb3e8a2..1572cfa35a 100644 --- a/crates/perry/src/commands/compile/library_search.rs +++ b/crates/perry/src/commands/compile/library_search.rs @@ -1510,46 +1510,10 @@ pub(super) fn android_cross_env(ndk_home: &Path, target: Option<&str>) -> Vec<(S } #[cfg(test)] -mod android_cross_env_tests { - use super::android_cross_env; - use std::collections::HashMap; - use std::path::Path; - - #[test] - fn x86_64_uses_matching_ndk_wrappers_and_cargo_linker() { - let env: HashMap<_, _> = android_cross_env(Path::new("/ndk"), Some("android-x86_64")) - .into_iter() - .collect(); - let cc = env - .get("CC_x86_64-linux-android") - .expect("hyphenated cc-rs target key"); - assert!(cc.contains("x86_64-linux-android24-clang"), "{cc}"); - let linker = env - .get("CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER") - .expect("Cargo target linker"); - assert!(linker.contains("x86_64-linux-android24-clang"), "{linker}"); - assert!(env.contains_key("AR_x86_64_linux_android")); - } -} +mod android_cross_env_tests; #[cfg(test)] -mod harmonyos_sdk_tests { - use super::is_usable_harmonyos_native_sdk; - - #[test] - fn native_sdk_requires_both_clang_and_sysroot() { - let sdk = tempfile::tempdir().expect("tempdir"); - let bin = sdk.path().join("llvm/bin"); - std::fs::create_dir_all(&bin).expect("create llvm bin"); - assert!(!is_usable_harmonyos_native_sdk(sdk.path())); - - std::fs::write(bin.join("clang"), b"").expect("create clang"); - assert!(!is_usable_harmonyos_native_sdk(sdk.path())); - - std::fs::create_dir(sdk.path().join("sysroot")).expect("create sysroot"); - assert!(is_usable_harmonyos_native_sdk(sdk.path())); - } -} +mod harmonyos_sdk_tests; /// Cross-compile env vars to pass to `cargo build` so `cc-rs` picks up the /// OHOS SDK's clang + musl sysroot for any C source in dependency build.rs @@ -1840,316 +1804,20 @@ pub(super) fn build_geisterhand_libs(target: Option<&str>, format: OutputFormat) } #[cfg(test)] -mod apple_lib_name_tests { - use super::apple_class_lib_name; - - #[test] - fn class_stem_device_uses_canonical_name() { - // libperry_ui_ios.a stem already carries _ios → device adds nothing. - assert_eq!( - apple_class_lib_name("libperry_ui_ios.a", "_ios", false), - "libperry_ui_ios.a" - ); - } - - #[test] - fn class_stem_sim_appends_sim_suffix() { - // Same stem, simulator variant → _sim before .a. - assert_eq!( - apple_class_lib_name("libperry_ui_ios.a", "_ios", true), - "libperry_ui_ios_sim.a" - ); - } - - #[test] - fn generic_stem_device_appends_class() { - // libperry_runtime.a → device gets _ios appended. - assert_eq!( - apple_class_lib_name("libperry_runtime.a", "_ios", false), - "libperry_runtime_ios.a" - ); - } - - #[test] - fn generic_stem_sim_appends_class_and_sim() { - // libperry_runtime.a → simulator gets _ios_sim appended. - assert_eq!( - apple_class_lib_name("libperry_runtime.a", "_ios", true), - "libperry_runtime_ios_sim.a" - ); - } - - #[test] - fn handles_other_class_suffixes() { - // Spot-check non-iOS classes to make sure the helper isn't iOS-specific. - assert_eq!( - apple_class_lib_name("libperry_ui_tvos.a", "_tvos", true), - "libperry_ui_tvos_sim.a" - ); - assert_eq!( - apple_class_lib_name("libperry_stdlib.a", "_visionos", false), - "libperry_stdlib_visionos.a" - ); - } -} +mod apple_lib_name_tests; #[cfg(test)] -mod native_lib_artifact_tests { - use super::{host_target_triple, locate_native_lib_artifact}; - use std::fs; - - /// Refs #564 — when cargo writes to `target//release/` - /// (because something pinned a default target), perry must still - /// find the artifact for a native build (no `--target` passed). - #[test] - fn locates_artifact_under_host_triple_dir_for_native_build() { - let host = match host_target_triple() { - Some(h) => h, - None => return, // rustc unavailable in this test env — skip. - }; - - let tmp = tempfile::tempdir().expect("create tmpdir"); - let target_dir = tmp.path().join("target"); - let triple_dir = target_dir.join(host).join("release"); - fs::create_dir_all(&triple_dir).expect("mkdir triple/release"); - let lib_path = triple_dir.join("libfoo.a"); - fs::write(&lib_path, b"fake archive").expect("write lib"); - - let found = locate_native_lib_artifact(&target_dir, None, "libfoo.a"); - assert_eq!(found.as_deref(), Some(lib_path.as_path())); - } - - #[test] - fn prefers_bare_release_dir_when_present() { - let tmp = tempfile::tempdir().expect("create tmpdir"); - let target_dir = tmp.path().join("target"); - let release_dir = target_dir.join("release"); - fs::create_dir_all(&release_dir).expect("mkdir release"); - let lib_path = release_dir.join("libfoo.a"); - fs::write(&lib_path, b"fake archive").expect("write lib"); - - let found = locate_native_lib_artifact(&target_dir, None, "libfoo.a"); - assert_eq!(found.as_deref(), Some(lib_path.as_path())); - } - - /// Refs #792 — wrappers that supply only the cargo crate name - /// (e.g. `perry_ext_foo`) instead of the full filename should - /// still resolve to `libperry_ext_foo.a` on the host platform. - #[test] - fn locates_artifact_from_bare_crate_name() { - let tmp = tempfile::tempdir().expect("create tmpdir"); - let target_dir = tmp.path().join("target"); - let release_dir = target_dir.join("release"); - fs::create_dir_all(&release_dir).expect("mkdir release"); - let lib_name = if cfg!(target_os = "windows") { - "perry_ext_foo.lib" - } else { - "libperry_ext_foo.a" - }; - let lib_path = release_dir.join(lib_name); - fs::write(&lib_path, b"fake archive").expect("write lib"); - - let found = locate_native_lib_artifact(&target_dir, None, "perry_ext_foo"); - assert_eq!(found.as_deref(), Some(lib_path.as_path())); - } - - /// On MSVC, cargo emits `{crate_name}.lib` literally — there is no - /// automatic `lib` prefix. So a crate whose name actually starts - /// with `lib` (e.g. `libfoo`) produces `libfoo.lib`, and Perry's - /// variant logic must NOT strip the `lib` prefix in that case. - #[test] - #[cfg(target_os = "windows")] - fn windows_preserves_lib_prefix_when_crate_name_starts_with_lib() { - let tmp = tempfile::tempdir().expect("create tmpdir"); - let target_dir = tmp.path().join("target"); - let release_dir = target_dir.join("release"); - fs::create_dir_all(&release_dir).expect("mkdir release"); - let lib_path = release_dir.join("libfoo.lib"); - fs::write(&lib_path, b"fake archive").expect("write lib"); - - let found = locate_native_lib_artifact(&target_dir, None, "libfoo"); - assert_eq!(found.as_deref(), Some(lib_path.as_path())); - } - - /// Cross-platform check on the variant set itself so non-Windows - /// CI also covers the `lib`-prefix preservation logic. - #[test] - fn variant_set_for_windows_target_keeps_lib_prefix() { - let variants = super::lib_name_variants("libfoo", Some("windows")); - assert!( - variants.iter().any(|v| v == "libfoo.lib"), - "expected libfoo.lib in {:?}", - variants - ); - } - - /// Refs #5812 — a wrapper that hard-codes the Unix static-lib - /// filename (`libperry_ext_webgpu.a`) in its manifest must still - /// resolve when targeting Windows, where cargo emits - /// `perry_ext_webgpu.lib` (MSVC drops the `lib` prefix and the - /// extension is `.lib`). The literal `.a` name is still tried first - /// so Unix builds keep working unchanged. - #[test] - fn variant_set_translates_unix_static_lib_to_msvc_on_windows() { - let variants = super::lib_name_variants("libperry_ext_webgpu.a", Some("windows")); - assert_eq!( - variants.first().map(String::as_str), - Some("libperry_ext_webgpu.a") - ); - assert!( - variants.iter().any(|v| v == "perry_ext_webgpu.lib"), - "expected perry_ext_webgpu.lib in {:?}", - variants - ); - } - - /// The `.a` → `.lib` translation is Windows-only; a Unix target must - /// not start probing for `.lib` files. - #[test] - fn variant_set_does_not_translate_static_lib_on_unix() { - let variants = super::lib_name_variants("libperry_ext_webgpu.a", Some("linux")); - assert_eq!(variants, vec!["libperry_ext_webgpu.a".to_string()]); - } - - /// End-to-end: a manifest declaring `libperry_ext_webgpu.a` resolves - /// the on-disk `perry_ext_webgpu.lib` that cargo actually produced on - /// Windows. Refs #5812. - #[test] - #[cfg(target_os = "windows")] - fn locates_msvc_artifact_from_unix_manifest_name() { - let tmp = tempfile::tempdir().expect("create tmpdir"); - let target_dir = tmp.path().join("target"); - let release_dir = target_dir.join("release"); - fs::create_dir_all(&release_dir).expect("mkdir release"); - let lib_path = release_dir.join("perry_ext_webgpu.lib"); - fs::write(&lib_path, b"fake archive").expect("write lib"); - - let found = locate_native_lib_artifact(&target_dir, None, "libperry_ext_webgpu.a"); - assert_eq!(found.as_deref(), Some(lib_path.as_path())); - } - - #[test] - fn returns_none_when_artifact_missing() { - let tmp = tempfile::tempdir().expect("create tmpdir"); - let target_dir = tmp.path().join("target"); - fs::create_dir_all(&target_dir).expect("mkdir target"); - let found = locate_native_lib_artifact(&target_dir, None, "libfoo.a"); - assert!(found.is_none()); - } -} +mod native_lib_artifact_tests; #[cfg(test)] -mod llvm_tool_discovery_tests { - use super::versioned_llvm_bin_dirs; - use std::path::PathBuf; - - // #5779: the well-known-LLVM-dir fallback must return versioned kegs - // newest-first, so bitcode inspection uses an LLVM at least as new as the - // Rust toolchain's. Ordering is the only non-trivial part, and it is pure, - // so exercise it directly without touching the filesystem. - #[test] - fn versioned_llvm_dirs_are_newest_first() { - let names = vec![ - "llvm@15".to_string(), - "llvm@18".to_string(), - "llvm@9".to_string(), - "llvm".to_string(), // unversioned — ignored here - "node@20".to_string(), // wrong prefix — ignored - ]; - let dirs = versioned_llvm_bin_dirs("/opt/homebrew/opt", "llvm@", &names); - assert_eq!( - dirs, - vec![ - PathBuf::from("/opt/homebrew/opt/llvm@18/bin"), - PathBuf::from("/opt/homebrew/opt/llvm@15/bin"), - PathBuf::from("/opt/homebrew/opt/llvm@9/bin"), - ] - ); - } - - #[test] - fn versioned_llvm_dirs_handles_debian_prefix_and_ignores_nonmatching() { - let names = vec![ - "llvm-17".to_string(), - "llvm-16.0".to_string(), - "gcc-12".to_string(), - "llvm-".to_string(), // no numeric version — ignored - ]; - let dirs = versioned_llvm_bin_dirs("/usr/lib", "llvm-", &names); - assert_eq!( - dirs, - vec![ - PathBuf::from("/usr/lib/llvm-17/bin"), - PathBuf::from("/usr/lib/llvm-16.0/bin"), - ] - ); - } -} +mod llvm_tool_discovery_tests; // #6716: `--target macos` on a macOS host is host-native — the candidate list // must probe the bare `target/{release,debug}/` dirs (where a plain // `cargo build -p perry-ui-macos` lands), while a genuine cross target keeps the // conservative triple-only search. #[cfg(all(test, target_os = "macos"))] -mod macos_host_candidate_tests { - use super::collect_library_candidates; - use std::path::PathBuf; - - #[test] - fn macos_target_probes_bare_target_dirs() { - let cands = collect_library_candidates("libperry_ui_macos.a", Some("macos")); - // Triple-qualified dir is still searched — derive the triple from the - // same mapping the production code uses rather than hardcoding it, so - // this stays in sync if the `macos` → triple mapping ever changes. - let triple = super::rust_target_triple(Some("macos")) - .expect("macos target should resolve to a triple"); - assert!( - cands.contains(&PathBuf::from(format!( - "target/{triple}/release/libperry_ui_macos.a" - ))), - "missing triple dir in {cands:?}" - ); - // ...and so is the bare host-native dir the suggested build command writes to. - assert!( - cands.contains(&PathBuf::from("target/release/libperry_ui_macos.a")), - "missing bare target/release in {cands:?}" - ); - assert!( - cands.contains(&PathBuf::from("target/debug/libperry_ui_macos.a")), - "missing bare target/debug in {cands:?}" - ); - } - - #[test] - fn cross_target_keeps_conservative_search() { - // A real cross-compile (iOS) must NOT fall back to the host's bare - // target/release — that would risk linking a macOS lib into an iOS app. - let cands = collect_library_candidates("libperry_ui_ios.a", Some("ios")); - assert!( - !cands.contains(&PathBuf::from("target/release/libperry_ui_ios.a")), - "cross target unexpectedly probed bare target/release in {cands:?}" - ); - } -} +mod macos_host_candidate_tests; #[cfg(all(test, target_os = "windows"))] -mod windows_toolchain_tests { - use super::msvc_vswhere_installation_path_args; - - #[test] - fn vswhere_query_requires_msvc_tools_component() { - assert_eq!( - msvc_vswhere_installation_path_args(), - [ - "-products", - "*", - "-requires", - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", - "-latest", - "-property", - "installationPath", - "-nologo", - ] - ); - } -} +mod windows_toolchain_tests; diff --git a/crates/perry/src/commands/compile/library_search/android_cross_env_tests.rs b/crates/perry/src/commands/compile/library_search/android_cross_env_tests.rs new file mode 100644 index 0000000000..0e7b1b6305 --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/android_cross_env_tests.rs @@ -0,0 +1,19 @@ +use super::android_cross_env; +use std::collections::HashMap; +use std::path::Path; + +#[test] +fn x86_64_uses_matching_ndk_wrappers_and_cargo_linker() { + let env: HashMap<_, _> = android_cross_env(Path::new("/ndk"), Some("android-x86_64")) + .into_iter() + .collect(); + let cc = env + .get("CC_x86_64-linux-android") + .expect("hyphenated cc-rs target key"); + assert!(cc.contains("x86_64-linux-android24-clang"), "{cc}"); + let linker = env + .get("CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER") + .expect("Cargo target linker"); + assert!(linker.contains("x86_64-linux-android24-clang"), "{linker}"); + assert!(env.contains_key("AR_x86_64_linux_android")); +} diff --git a/crates/perry/src/commands/compile/library_search/apple_lib_name_tests.rs b/crates/perry/src/commands/compile/library_search/apple_lib_name_tests.rs new file mode 100644 index 0000000000..be8948a25a --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/apple_lib_name_tests.rs @@ -0,0 +1,50 @@ +use super::apple_class_lib_name; + +#[test] +fn class_stem_device_uses_canonical_name() { + // libperry_ui_ios.a stem already carries _ios → device adds nothing. + assert_eq!( + apple_class_lib_name("libperry_ui_ios.a", "_ios", false), + "libperry_ui_ios.a" + ); +} + +#[test] +fn class_stem_sim_appends_sim_suffix() { + // Same stem, simulator variant → _sim before .a. + assert_eq!( + apple_class_lib_name("libperry_ui_ios.a", "_ios", true), + "libperry_ui_ios_sim.a" + ); +} + +#[test] +fn generic_stem_device_appends_class() { + // libperry_runtime.a → device gets _ios appended. + assert_eq!( + apple_class_lib_name("libperry_runtime.a", "_ios", false), + "libperry_runtime_ios.a" + ); +} + +#[test] +fn generic_stem_sim_appends_class_and_sim() { + // libperry_runtime.a → simulator gets _ios_sim appended. + assert_eq!( + apple_class_lib_name("libperry_runtime.a", "_ios", true), + "libperry_runtime_ios_sim.a" + ); +} + +#[test] +fn handles_other_class_suffixes() { + // Spot-check non-iOS classes to make sure the helper isn't iOS-specific. + assert_eq!( + apple_class_lib_name("libperry_ui_tvos.a", "_tvos", true), + "libperry_ui_tvos_sim.a" + ); + assert_eq!( + apple_class_lib_name("libperry_stdlib.a", "_visionos", false), + "libperry_stdlib_visionos.a" + ); +} diff --git a/crates/perry/src/commands/compile/library_search/harmonyos_sdk_tests.rs b/crates/perry/src/commands/compile/library_search/harmonyos_sdk_tests.rs new file mode 100644 index 0000000000..0d49f25253 --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/harmonyos_sdk_tests.rs @@ -0,0 +1,15 @@ +use super::is_usable_harmonyos_native_sdk; + +#[test] +fn native_sdk_requires_both_clang_and_sysroot() { + let sdk = tempfile::tempdir().expect("tempdir"); + let bin = sdk.path().join("llvm/bin"); + std::fs::create_dir_all(&bin).expect("create llvm bin"); + assert!(!is_usable_harmonyos_native_sdk(sdk.path())); + + std::fs::write(bin.join("clang"), b"").expect("create clang"); + assert!(!is_usable_harmonyos_native_sdk(sdk.path())); + + std::fs::create_dir(sdk.path().join("sysroot")).expect("create sysroot"); + assert!(is_usable_harmonyos_native_sdk(sdk.path())); +} diff --git a/crates/perry/src/commands/compile/library_search/llvm_tool_discovery_tests.rs b/crates/perry/src/commands/compile/library_search/llvm_tool_discovery_tests.rs new file mode 100644 index 0000000000..1f630af5a8 --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/llvm_tool_discovery_tests.rs @@ -0,0 +1,44 @@ +use super::versioned_llvm_bin_dirs; +use std::path::PathBuf; + +// #5779: the well-known-LLVM-dir fallback must return versioned kegs +// newest-first, so bitcode inspection uses an LLVM at least as new as the +// Rust toolchain's. Ordering is the only non-trivial part, and it is pure, +// so exercise it directly without touching the filesystem. +#[test] +fn versioned_llvm_dirs_are_newest_first() { + let names = vec![ + "llvm@15".to_string(), + "llvm@18".to_string(), + "llvm@9".to_string(), + "llvm".to_string(), // unversioned — ignored here + "node@20".to_string(), // wrong prefix — ignored + ]; + let dirs = versioned_llvm_bin_dirs("/opt/homebrew/opt", "llvm@", &names); + assert_eq!( + dirs, + vec![ + PathBuf::from("/opt/homebrew/opt/llvm@18/bin"), + PathBuf::from("/opt/homebrew/opt/llvm@15/bin"), + PathBuf::from("/opt/homebrew/opt/llvm@9/bin"), + ] + ); +} + +#[test] +fn versioned_llvm_dirs_handles_debian_prefix_and_ignores_nonmatching() { + let names = vec![ + "llvm-17".to_string(), + "llvm-16.0".to_string(), + "gcc-12".to_string(), + "llvm-".to_string(), // no numeric version — ignored + ]; + let dirs = versioned_llvm_bin_dirs("/usr/lib", "llvm-", &names); + assert_eq!( + dirs, + vec![ + PathBuf::from("/usr/lib/llvm-17/bin"), + PathBuf::from("/usr/lib/llvm-16.0/bin"), + ] + ); +} diff --git a/crates/perry/src/commands/compile/library_search/macos_host_candidate_tests.rs b/crates/perry/src/commands/compile/library_search/macos_host_candidate_tests.rs new file mode 100644 index 0000000000..fdd6ba1098 --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/macos_host_candidate_tests.rs @@ -0,0 +1,38 @@ +use super::collect_library_candidates; +use std::path::PathBuf; + +#[test] +fn macos_target_probes_bare_target_dirs() { + let cands = collect_library_candidates("libperry_ui_macos.a", Some("macos")); + // Triple-qualified dir is still searched — derive the triple from the + // same mapping the production code uses rather than hardcoding it, so + // this stays in sync if the `macos` → triple mapping ever changes. + let triple = + super::rust_target_triple(Some("macos")).expect("macos target should resolve to a triple"); + assert!( + cands.contains(&PathBuf::from(format!( + "target/{triple}/release/libperry_ui_macos.a" + ))), + "missing triple dir in {cands:?}" + ); + // ...and so is the bare host-native dir the suggested build command writes to. + assert!( + cands.contains(&PathBuf::from("target/release/libperry_ui_macos.a")), + "missing bare target/release in {cands:?}" + ); + assert!( + cands.contains(&PathBuf::from("target/debug/libperry_ui_macos.a")), + "missing bare target/debug in {cands:?}" + ); +} + +#[test] +fn cross_target_keeps_conservative_search() { + // A real cross-compile (iOS) must NOT fall back to the host's bare + // target/release — that would risk linking a macOS lib into an iOS app. + let cands = collect_library_candidates("libperry_ui_ios.a", Some("ios")); + assert!( + !cands.contains(&PathBuf::from("target/release/libperry_ui_ios.a")), + "cross target unexpectedly probed bare target/release in {cands:?}" + ); +} diff --git a/crates/perry/src/commands/compile/library_search/native_lib_artifact_tests.rs b/crates/perry/src/commands/compile/library_search/native_lib_artifact_tests.rs new file mode 100644 index 0000000000..69b5e9467c --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/native_lib_artifact_tests.rs @@ -0,0 +1,141 @@ +use super::{host_target_triple, locate_native_lib_artifact}; +use std::fs; + +/// Refs #564 — when cargo writes to `target//release/` +/// (because something pinned a default target), perry must still +/// find the artifact for a native build (no `--target` passed). +#[test] +fn locates_artifact_under_host_triple_dir_for_native_build() { + let host = match host_target_triple() { + Some(h) => h, + None => return, // rustc unavailable in this test env — skip. + }; + + let tmp = tempfile::tempdir().expect("create tmpdir"); + let target_dir = tmp.path().join("target"); + let triple_dir = target_dir.join(host).join("release"); + fs::create_dir_all(&triple_dir).expect("mkdir triple/release"); + let lib_path = triple_dir.join("libfoo.a"); + fs::write(&lib_path, b"fake archive").expect("write lib"); + + let found = locate_native_lib_artifact(&target_dir, None, "libfoo.a"); + assert_eq!(found.as_deref(), Some(lib_path.as_path())); +} + +#[test] +fn prefers_bare_release_dir_when_present() { + let tmp = tempfile::tempdir().expect("create tmpdir"); + let target_dir = tmp.path().join("target"); + let release_dir = target_dir.join("release"); + fs::create_dir_all(&release_dir).expect("mkdir release"); + let lib_path = release_dir.join("libfoo.a"); + fs::write(&lib_path, b"fake archive").expect("write lib"); + + let found = locate_native_lib_artifact(&target_dir, None, "libfoo.a"); + assert_eq!(found.as_deref(), Some(lib_path.as_path())); +} + +/// Refs #792 — wrappers that supply only the cargo crate name +/// (e.g. `perry_ext_foo`) instead of the full filename should +/// still resolve to `libperry_ext_foo.a` on the host platform. +#[test] +fn locates_artifact_from_bare_crate_name() { + let tmp = tempfile::tempdir().expect("create tmpdir"); + let target_dir = tmp.path().join("target"); + let release_dir = target_dir.join("release"); + fs::create_dir_all(&release_dir).expect("mkdir release"); + let lib_name = if cfg!(target_os = "windows") { + "perry_ext_foo.lib" + } else { + "libperry_ext_foo.a" + }; + let lib_path = release_dir.join(lib_name); + fs::write(&lib_path, b"fake archive").expect("write lib"); + + let found = locate_native_lib_artifact(&target_dir, None, "perry_ext_foo"); + assert_eq!(found.as_deref(), Some(lib_path.as_path())); +} + +/// On MSVC, cargo emits `{crate_name}.lib` literally — there is no +/// automatic `lib` prefix. So a crate whose name actually starts +/// with `lib` (e.g. `libfoo`) produces `libfoo.lib`, and Perry's +/// variant logic must NOT strip the `lib` prefix in that case. +#[test] +#[cfg(target_os = "windows")] +fn windows_preserves_lib_prefix_when_crate_name_starts_with_lib() { + let tmp = tempfile::tempdir().expect("create tmpdir"); + let target_dir = tmp.path().join("target"); + let release_dir = target_dir.join("release"); + fs::create_dir_all(&release_dir).expect("mkdir release"); + let lib_path = release_dir.join("libfoo.lib"); + fs::write(&lib_path, b"fake archive").expect("write lib"); + + let found = locate_native_lib_artifact(&target_dir, None, "libfoo"); + assert_eq!(found.as_deref(), Some(lib_path.as_path())); +} + +/// Cross-platform check on the variant set itself so non-Windows +/// CI also covers the `lib`-prefix preservation logic. +#[test] +fn variant_set_for_windows_target_keeps_lib_prefix() { + let variants = super::lib_name_variants("libfoo", Some("windows")); + assert!( + variants.iter().any(|v| v == "libfoo.lib"), + "expected libfoo.lib in {:?}", + variants + ); +} + +/// Refs #5812 — a wrapper that hard-codes the Unix static-lib +/// filename (`libperry_ext_webgpu.a`) in its manifest must still +/// resolve when targeting Windows, where cargo emits +/// `perry_ext_webgpu.lib` (MSVC drops the `lib` prefix and the +/// extension is `.lib`). The literal `.a` name is still tried first +/// so Unix builds keep working unchanged. +#[test] +fn variant_set_translates_unix_static_lib_to_msvc_on_windows() { + let variants = super::lib_name_variants("libperry_ext_webgpu.a", Some("windows")); + assert_eq!( + variants.first().map(String::as_str), + Some("libperry_ext_webgpu.a") + ); + assert!( + variants.iter().any(|v| v == "perry_ext_webgpu.lib"), + "expected perry_ext_webgpu.lib in {:?}", + variants + ); +} + +/// The `.a` → `.lib` translation is Windows-only; a Unix target must +/// not start probing for `.lib` files. +#[test] +fn variant_set_does_not_translate_static_lib_on_unix() { + let variants = super::lib_name_variants("libperry_ext_webgpu.a", Some("linux")); + assert_eq!(variants, vec!["libperry_ext_webgpu.a".to_string()]); +} + +/// End-to-end: a manifest declaring `libperry_ext_webgpu.a` resolves +/// the on-disk `perry_ext_webgpu.lib` that cargo actually produced on +/// Windows. Refs #5812. +#[test] +#[cfg(target_os = "windows")] +fn locates_msvc_artifact_from_unix_manifest_name() { + let tmp = tempfile::tempdir().expect("create tmpdir"); + let target_dir = tmp.path().join("target"); + let release_dir = target_dir.join("release"); + fs::create_dir_all(&release_dir).expect("mkdir release"); + let lib_path = release_dir.join("perry_ext_webgpu.lib"); + fs::write(&lib_path, b"fake archive").expect("write lib"); + + let found = locate_native_lib_artifact(&target_dir, None, "libperry_ext_webgpu.a"); + assert_eq!(found.as_deref(), Some(lib_path.as_path())); +} + +#[test] +fn returns_none_when_artifact_missing() { + let tmp = tempfile::tempdir().expect("create tmpdir"); + let target_dir = tmp.path().join("target"); + fs::create_dir_all(&target_dir).expect("mkdir target"); + let found = locate_native_lib_artifact(&target_dir, None, "libfoo.a"); + assert!(found.is_none()); +} diff --git a/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs b/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs new file mode 100644 index 0000000000..eab4b141c7 --- /dev/null +++ b/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs @@ -0,0 +1,18 @@ +use super::msvc_vswhere_installation_path_args; + +#[test] +fn vswhere_query_requires_msvc_tools_component() { + assert_eq!( + msvc_vswhere_installation_path_args(), + [ + "-products", + "*", + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-latest", + "-property", + "installationPath", + "-nologo", + ] + ); +}