diff --git a/changelog.d/8979-run-directory-entry.md b/changelog.d/8979-run-directory-entry.md
new file mode 100644
index 0000000000..bf426955b6
--- /dev/null
+++ b/changelog.d/8979-run-directory-entry.md
@@ -0,0 +1,5 @@
+`perry run
` now resolves the directory to its project entry instead of passing the directory itself into module collection.
+
+An explicit directory input is treated as a project root: its `perry.toml` entry is read relative to that directory, falling back to `/src/main.ts` then `/main.ts`. Previously `perry run .` read `perry.toml` from the current working directory and handed the directory straight to module collection, which fails on Windows.
+
+Fixes #8908.
diff --git a/changelog.d/8980-private-guard-call-site.md b/changelog.d/8980-private-guard-call-site.md
new file mode 100644
index 0000000000..5791531d9e
--- /dev/null
+++ b/changelog.d/8980-private-guard-call-site.md
@@ -0,0 +1,22 @@
+The private-member guard moved to its call sites, taking a call off every
+ordinary property read and write.
+
+#8970 made the private-member name test cheap but left the CALL. In a pure
+property-read loop (`o[k]` with pre-built keys, no concat) that showed up as
+`private_member_get_by_name` 11.4% plus `private_member_storage_name` 5.4% —
+**16.8% of the loop, the largest single item** — essentially all of it call
+overhead for keys that are rejected on their length before doing anything.
+
+The guard is now invoked at the three call sites (the generic read entry, the
+class-field read miss, and the generic write), so an ordinary property
+operation makes no call into the private-member path at all. Keys that pass
+the guard take exactly the original path.
+
+Interleaved A/B, min-of-21 (under heavy co-tenant load, so read the ratios
+rather than the absolutes): pure property read 26 → 22 ms (−15%), computed-key
+read 51 → 45 ms (−12%), combined overwrite 50 → 46 ms (−8%), write unchanged.
+
+Output on a private-member exercise — instance fields, `static #instances`,
+private methods, private getters, `#x in obj`, subclassing, and an ordinary key
+literally named `#` — is byte-identical to before the
+change. Computed-key differential vs node is byte-identical. Suite 2779 passed.
diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs
index ebe7768f7e..695c2eef4d 100644
--- a/crates/perry-runtime/src/object/field_get_set.rs
+++ b/crates/perry-runtime/src/object/field_get_set.rs
@@ -284,13 +284,13 @@ pub use has_property::{js_in_operator, js_object_has_property};
#[cfg(test)]
pub(crate) use ic_miss::primitive_proto_method_name_static;
pub(crate) use ic_miss::{
- bind_primitive_proto_method_static, is_array_method_value_name, private_evaluation_brand_value,
- private_lexical_brand_pop, private_lexical_brand_push, private_lexical_brand_stack_restore,
- private_lexical_brand_stack_savepoint, private_member_access_hints_restore,
- private_member_access_hints_savepoint, private_member_call_by_name, private_member_get_by_name,
- private_member_set_by_name, scan_private_lexical_brand_roots_mut, set_method_value_name,
- stamp_private_evaluation_brand, take_private_method_call_hint, take_private_method_owner_hint,
- timer_handle_method_name_static,
+ bind_primitive_proto_method_static, cannot_be_private_member_name, is_array_method_value_name,
+ private_evaluation_brand_value, private_lexical_brand_pop, private_lexical_brand_push,
+ private_lexical_brand_stack_restore, private_lexical_brand_stack_savepoint,
+ private_member_access_hints_restore, private_member_access_hints_savepoint,
+ private_member_call_by_name, private_member_get_by_name, private_member_set_by_name,
+ scan_private_lexical_brand_roots_mut, set_method_value_name, stamp_private_evaluation_brand,
+ take_private_method_call_hint, take_private_method_owner_hint, timer_handle_method_name_static,
};
pub use ic_miss::{
js_class_field_add, js_object_get_field_by_name_f64, js_object_get_field_by_property_id_f64,
diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
index d9fdf0e043..2e77c64d7d 100644
--- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
+++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
@@ -27,8 +27,13 @@ pub extern "C" fn js_object_get_field_by_name(
obj: *const ObjectHeader,
key: *const crate::StringHeader,
) -> JSValue {
- if let Some(value) = super::private_member_get_by_name(obj, key) {
- return JSValue::from_bits(value.to_bits());
+ // Guard hoisted to the call site: an ordinary key is rejected on a length
+ // compare and one byte here, so the overwhelmingly common property read
+ // makes no call into the private-member path at all.
+ if !super::cannot_be_private_member_name(key) {
+ if let Some(value) = super::private_member_get_by_name(obj, key) {
+ return JSValue::from_bits(value.to_bits());
+ }
}
// An elements-backed Array-subclass instance answers its indices and
// `length` from its store; an absent index falls through to the ordinary
diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs
index dd0ead0dbd..c6210d12cd 100644
--- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs
+++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs
@@ -10,8 +10,10 @@ pub extern "C" fn js_object_get_field_by_name_f64(
obj: *const ObjectHeader,
key: *const crate::StringHeader,
) -> f64 {
- if let Some(value) = private_member_get_by_name(obj, key) {
- return value;
+ if !cannot_be_private_member_name(key) {
+ if let Some(value) = private_member_get_by_name(obj, key) {
+ return value;
+ }
}
if (obj as usize) > 0 && (obj as usize) < 0x10000 && !key.is_null() {
if let Some(name) = unsafe { super::super::has_own_helpers::str_from_string_header(key) } {
diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs
index 5569f67205..44849f6265 100644
--- a/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs
+++ b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs
@@ -46,6 +46,13 @@ const PRIVATE_MEMBER_PREFIX: &str = "# bool {
+pub(crate) fn cannot_be_private_member_name(key: *const crate::StringHeader) -> bool {
if key.is_null() {
return true;
}
diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs
index 4447c63345..fa6dcaec64 100644
--- a/crates/perry-runtime/src/object/field_set_by_name.rs
+++ b/crates/perry-runtime/src/object/field_set_by_name.rs
@@ -44,7 +44,11 @@ pub extern "C" fn js_object_set_field_by_name(
key: *const crate::StringHeader,
value: f64,
) {
- if super::private_member_set_by_name(obj, key, value) {
+ // Guard hoisted to the call site (see `cannot_be_private_member_name`):
+ // an ordinary key never calls into the private-member path.
+ if !super::cannot_be_private_member_name(key)
+ && super::private_member_set_by_name(obj, key, value)
+ {
return;
}
// A heap class value is an exotic constructor object. Its own
diff --git a/crates/perry/src/commands/run/entry.rs b/crates/perry/src/commands/run/entry.rs
index e6daef034f..406422a319 100644
--- a/crates/perry/src/commands/run/entry.rs
+++ b/crates/perry/src/commands/run/entry.rs
@@ -41,15 +41,17 @@ pub fn rust_target_triple(target: Option<&str>) -> Option<&'static str> {
/// Resolve the entry TypeScript file
pub fn resolve_entry_file(input: Option<&Path>) -> Result {
- if let Some(path) = input {
- if path.exists() {
- return Ok(path.to_path_buf());
+ let project_dir = match input {
+ Some(path) if !path.exists() => {
+ return Err(anyhow!("File not found: {}", path.display()));
}
- return Err(anyhow!("File not found: {}", path.display()));
- }
+ Some(path) if !path.is_dir() => return Ok(path.to_path_buf()),
+ Some(path) => path,
+ None => Path::new("."),
+ };
// Try perry.toml
- if let Some(entry) = read_perry_toml_entry() {
+ if let Some(entry) = read_perry_toml_entry(project_dir) {
if entry.exists() {
return Ok(entry);
}
@@ -57,7 +59,7 @@ pub fn resolve_entry_file(input: Option<&Path>) -> Result {
// Fallback: src/main.ts, then main.ts
for candidate in &["src/main.ts", "main.ts"] {
- let path = PathBuf::from(candidate);
+ let path = project_dir.join(candidate);
if path.exists() {
return Ok(path);
}
@@ -71,14 +73,14 @@ pub fn resolve_entry_file(input: Option<&Path>) -> Result {
}
/// Read entry point from perry.toml if present
-pub fn read_perry_toml_entry() -> Option {
- let toml_str = std::fs::read_to_string("perry.toml").ok()?;
+fn read_perry_toml_entry(project_dir: &Path) -> Option {
+ let toml_str = std::fs::read_to_string(project_dir.join("perry.toml")).ok()?;
for line in toml_str.lines() {
let trimmed = line.trim();
if trimmed.starts_with("entry") {
if let Some(eq_pos) = trimmed.find('=') {
let value = trimmed[eq_pos + 1..].trim().trim_matches('"');
- return Some(PathBuf::from(value));
+ return Some(project_dir.join(value));
}
}
}
@@ -272,7 +274,32 @@ pub fn resolve_target(
#[cfg(test)]
mod tests {
- use super::rust_target_triple;
+ use super::{resolve_entry_file, rust_target_triple};
+
+ #[test]
+ fn directory_input_resolves_default_entry() {
+ let project = tempfile::tempdir().unwrap();
+ let entry = project.path().join("src/main.ts");
+ std::fs::create_dir_all(entry.parent().unwrap()).unwrap();
+ std::fs::write(&entry, "console.log('hello');").unwrap();
+
+ assert_eq!(resolve_entry_file(Some(project.path())).unwrap(), entry);
+ }
+
+ #[test]
+ fn directory_input_resolves_perry_toml_entry() {
+ let project = tempfile::tempdir().unwrap();
+ let entry = project.path().join("app/index.ts");
+ std::fs::create_dir_all(entry.parent().unwrap()).unwrap();
+ std::fs::write(&entry, "console.log('hello');").unwrap();
+ std::fs::write(
+ project.path().join("perry.toml"),
+ "entry = \"app/index.ts\"\n",
+ )
+ .unwrap();
+
+ assert_eq!(resolve_entry_file(Some(project.path())).unwrap(), entry);
+ }
#[test]
fn android_x86_64_uses_its_cross_runtime() {