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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "1.1"
stacker = "0.1.24"

# JWT — jsonwebtoken 10.x requires explicitly selecting a crypto backend
# (`rust_crypto` or `aws_lc_rs`) and opting into PEM parsing for
Expand Down
25 changes: 15 additions & 10 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3777,18 +3777,23 @@ pub static API_MANIFEST: &[ApiEntry] = &[
property("cluster", "schedulingPolicy"),
property("cluster", "SCHED_RR"),
property("cluster", "SCHED_NONE"),
// `cluster.on` / `cluster.addListener` exist as EventEmitter
// prototype methods on the cluster module ITSELF in Node, but
// `import * as cluster from "node:cluster"` reads them as named
// exports — and there is no `on` / `addListener` named export.
// Node's parity fixture prints "undefined" for both. Register them
// as properties so the #463 strict gate doesn't bail out at compile
// time; `get_native_module_constant` returns `undefined` at
// runtime.
// #3687: the EventEmitter method surface. On the `import * as` namespace
// these all read `undefined` (they are not named exports); on the default
// import they resolve to bound methods. Registered so the #463 strict gate
// accepts reads/calls at compile time.
// import they resolve to bound methods through `NATIVE_MODULE_TABLE`.
internal_method("cluster", "on", false, None),
internal_method("cluster", "addListener", false, None),
internal_method("cluster", "once", false, None),
internal_method("cluster", "prependListener", false, None),
internal_method("cluster", "prependOnceListener", false, None),
internal_method("cluster", "emit", false, None),
internal_method("cluster", "eventNames", false, None),
internal_method("cluster", "listenerCount", false, None),
internal_method("cluster", "removeListener", false, None),
internal_method("cluster", "off", false, None),
internal_method("cluster", "removeAllListeners", false, None),
// Keep property reads registered so the #463 strict gate accepts the
// namespace-export shape; `get_native_module_constant` returns undefined
// for these names at runtime.
internal_property("cluster", "on"),
internal_property("cluster", "addListener"),
internal_property("cluster", "once"),
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen-arkts/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ fn state_method_call(state_id: u32, method: &str, args: Vec<Expr>) -> Expr {
}),
args,
type_args: vec![],
byte_offset: 0,
}
}

Expand Down Expand Up @@ -2531,6 +2532,7 @@ fn issue_410_serialize_condition_fallback_has_no_block_comment_close() {
callee: Box::new(Expr::LocalGet(99)),
args: vec![],
type_args: vec![],
byte_offset: 0,
};
let s = serialize_condition(&unrecognized, &bindings, &consts);
assert!(
Expand Down Expand Up @@ -2762,6 +2764,7 @@ fn issue_410_conditional_modifier_chain_has_no_nested_block_comments() {
callee: Box::new(Expr::LocalGet(999)),
args: vec![],
type_args: vec![],
byte_offset: 0,
},
));
m.init.push(let_widget(
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ fn full_phase2_app_emits_canonical_arkui() {
}),
args: vec![],
type_args: vec![],
byte_offset: 0,
};

// --- Phase 2 v6 + v5: Button with state.set + inline style ---
Expand All @@ -160,6 +161,7 @@ fn full_phase2_app_emits_canonical_arkui() {
}),
args: vec![Expr::Number(1.0)],
type_args: vec![],
byte_offset: 0,
})];
let inc_button = nmc(
"Button",
Expand Down Expand Up @@ -523,6 +525,7 @@ fn minimal_counter_app_emits_clean_page() {
}),
args: vec![],
type_args: vec![],
byte_offset: 0,
},
nmc(
"Button",
Expand Down
59 changes: 55 additions & 4 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,50 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
class_ids.insert(ic.name.clone(), class_id);
}

let imported_getters: Vec<perry_hir::Function> = ic
.getter_names
.iter()
.map(|prop| perry_hir::Function {
id: 0,
name: format!("get_{}", prop),
type_params: Vec::new(),
params: Vec::new(),
return_type: perry_types::Type::Any,
body: Vec::new(),
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
})
.collect();
let imported_setters: Vec<perry_hir::Function> = ic
.setter_names
.iter()
.map(|prop| perry_hir::Function {
id: 0,
name: format!("set_{}", prop),
type_params: Vec::new(),
params: Vec::new(),
return_type: perry_types::Type::Any,
body: Vec::new(),
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
})
.collect();

// Build a stub Class with the minimum fields the codegen needs.
// Most fields are empty — only name, extends_name, and methods
// are consulted by dispatch.
// Imported accessor bodies execute from the source module; carrying
// their names here keeps dispatch and field inference conservative.
let stub = perry_hir::Class {
id: 0, // imported — no local ClassId
name: effective_name.to_string(),
Expand Down Expand Up @@ -362,8 +403,18 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
decorators: Vec::new(),
})
.collect(),
getters: Vec::new(),
setters: Vec::new(),
getters: ic
.getter_names
.iter()
.cloned()
.zip(imported_getters)
.collect(),
setters: ic
.setter_names
.iter()
.cloned()
.zip(imported_setters)
.collect(),
static_accessor_names: Vec::new(),
static_accessor_fn_ids: Vec::new(),
static_fields: Vec::new(),
Expand Down
99 changes: 99 additions & 0 deletions crates/perry-codegen/src/lower_call/native_table/node_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,105 @@ pub(super) const NODE_MISC_ROWS: &[NativeModSig] = &[
args: &[NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "on",
class_filter: None,
runtime: "js_cluster_on",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "addListener",
class_filter: None,
runtime: "js_cluster_on",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "once",
class_filter: None,
runtime: "js_cluster_once",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "prependListener",
class_filter: None,
runtime: "js_cluster_prepend_listener",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "prependOnceListener",
class_filter: None,
runtime: "js_cluster_prepend_once_listener",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "emit",
class_filter: None,
runtime: "js_cluster_emit",
args: &[NA_F64, NA_VARARGS],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "eventNames",
class_filter: None,
runtime: "js_cluster_event_names",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "listenerCount",
class_filter: None,
runtime: "js_cluster_listener_count",
args: &[NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "removeListener",
class_filter: None,
runtime: "js_cluster_remove_listener",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "off",
class_filter: None,
runtime: "js_cluster_remove_listener",
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "cluster",
has_receiver: false,
method: "removeAllListeners",
class_filter: None,
runtime: "js_cluster_remove_all_listeners",
args: &[NA_F64],
ret: NR_F64,
},
// ========== node:vm ==========
// Minimal contextification surface for APIs that require a vm context
// object but do not execute code inside it yet.
Expand Down
35 changes: 28 additions & 7 deletions crates/perry-codegen/src/type_analysis_class_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,34 @@ pub(crate) fn class_field_global_index(
fn count_keyable(fields: &[perry_hir::ClassField]) -> u32 {
fields.iter().filter(|f| f.key_expr.is_none()).count() as u32
}
fn accessor_in_chain(ctx: &FnCtx<'_>, class_name: &str, property: &str) -> bool {
let mut current = Some(class_name.to_string());
let mut seen_class_names: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut depth = 0usize;
while let Some(name) = current {
depth += 1;
if depth > 64 || !seen_class_names.insert(name.clone()) {
return true;
}
let Some(class) = ctx.classes.get(&name) else {
return true;
};
if class.getters.iter().any(|(n, _)| n == property)
|| class.setters.iter().any(|(n, _)| n == property)
{
return true;
}
current = class.extends_name.clone();
}
false
}
// A getter/setter anywhere on the prototype chain owns this property name
// for normal JS semantics. Do not emit direct packed-field access even if
// HIR inferred a same-named field on a subclass assignment.
if accessor_in_chain(ctx, class_name, property) {
return None;
}
fn walk(
ctx: &FnCtx<'_>,
class_name: &str,
Expand All @@ -96,13 +124,6 @@ pub(crate) fn class_field_global_index(
return None;
}
let class = ctx.classes.get(class_name)?;
// Bail if a getter/setter shadows the field — those need real
// method dispatch, not a direct memory access.
if class.getters.iter().any(|(n, _)| n == property)
|| class.setters.iter().any(|(n, _)| n == property)
{
return None;
}
// Compute the byte-offset contribution from this class's parent.
let parent_count = if let Some(parent_name) = class.extends_name.as_deref() {
let mut p_count = 0u32;
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ thiserror.workspace = true
anyhow.workspace = true
serde = { workspace = true }
serde_json = { workspace = true }
stacker.workspace = true
44 changes: 44 additions & 0 deletions crates/perry-hir/src/class_accessors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ClassAccessorNames {
pub getter_names: Vec<String>,
pub setter_names: Vec<String>,
}

impl ClassAccessorNames {
pub fn is_empty(&self) -> bool {
self.getter_names.is_empty() && self.setter_names.is_empty()
}

pub fn contains_any(&self, name: &str) -> bool {
self.getter_names.iter().any(|n| n == name) || self.setter_names.iter().any(|n| n == name)
}

pub fn insert_getter(&mut self, name: String) -> bool {
if self.getter_names.iter().any(|n| n == &name) {
false
} else {
self.getter_names.push(name);
true
}
}

pub fn insert_setter(&mut self, name: String) -> bool {
if self.setter_names.iter().any(|n| n == &name) {
false
} else {
self.setter_names.push(name);
true
}
}

pub fn extend_from(&mut self, other: &Self) -> bool {
let mut changed = false;
for name in &other.getter_names {
changed |= self.insert_getter(name.clone());
}
for name in &other.setter_names {
changed |= self.insert_setter(name.clone());
}
changed
}
}
2 changes: 2 additions & 0 deletions crates/perry-hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
pub mod analysis;
pub mod audit;
pub mod capability;
mod class_accessors;
pub mod deferral;
pub(crate) mod destructuring;
pub mod dynamic_import;
Expand All @@ -28,6 +29,7 @@ pub mod walker;
pub use analysis::{collect_local_refs_expr, collect_local_refs_stmt};
pub use audit::{audit_module, AuditManifest, ModuleAudit};
pub use capability::{audit_module_capabilities, CapabilityPolicy, CapabilityViolation};
pub use class_accessors::ClassAccessorNames;
pub use deferral::{arm_deferral_sink, disarm_deferral_sink, try_defer_refusal, DeferredRefusal};
pub use dynamic_import::{
collect_dynamic_import_local_candidate_literals, collect_dynamic_import_param_literals,
Expand Down
Loading
Loading