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/changelog.d/8981-dynamic-function-refusal-unwind.md b/changelog.d/8981-dynamic-function-refusal-unwind.md
new file mode 100644
index 0000000000..807a36cfcd
--- /dev/null
+++ b/changelog.d/8981-dynamic-function-refusal-unwind.md
@@ -0,0 +1,3 @@
+### Fixed
+
+- Refusing a runtime-string `Function` in a `dyn-eval`-off AOT binary now throws a catchable `TypeError` instead of aborting at the runtime FFI boundary, allowing zod v4 and other capability-probing libraries to select their non-eval fallback.
diff --git a/changelog.d/8982-hono-response-state.md b/changelog.d/8982-hono-response-state.md
new file mode 100644
index 0000000000..40194cf4ec
--- /dev/null
+++ b/changelog.d/8982-hono-response-state.md
@@ -0,0 +1,5 @@
+Fixed three defects that together broke Hono response state.
+
+- The read half of a compound or logical private-member assignment (`this.#x ||= v`) now lowers through the same brand guard and class-mangled storage key as an ordinary private read, instead of a differently-keyed path.
+- A runtime `HeadersInit` record passed through a non-literal `Response` init object is preserved rather than dropped.
+- The Fetch default `text/plain;charset=UTF-8` is installed for string bodies without overriding an explicit content type, and pending BodyInit metadata is cleared across reuse.
diff --git a/changelog.d/8983-feedback-gate-and-shape-field.md b/changelog.d/8983-feedback-gate-and-shape-field.md
new file mode 100644
index 0000000000..66a4acaecb
--- /dev/null
+++ b/changelog.d/8983-feedback-gate-and-shape-field.md
@@ -0,0 +1,38 @@
+Two dead-work removals on the property read path. The computed-key read loop
+now **matches node** (23 ms vs 23 ms on the same host), and the pure property
+read drops 21 → 17 ms.
+
+**1. Typed-feedback observation is skipped when recording is off.** Recording
+is off by default, and `guard_observe` and `record_fallback_call` both
+early-return in that mode — but the property wrappers built the whole
+`Observation` first, hashing the key and resolving the receiver's shape, purely
+to hand it to functions that discard it. `js_typed_feedback_object_get_field_by_name_f64`
+was 10% of an isolated property-read loop, nearly all of it that. The array
+index wrappers have carried #5094's gate for exactly this reason, and #8951
+gave it to the fast store path; the property get/set wrappers never got it.
+
+Behaviour is unchanged in both modes: with recording off `guard_observe`
+returns `contract_valid` unmodified and the fallback recorder is a no-op, so
+the wrapper already reduced to precisely the underlying call it now makes
+directly.
+
+**2. The slot bound stops copying a descriptor to read four bytes.**
+`shape_descriptor_by_id` returns `ShapeDescriptor` **by value**, so
+`object_live_slot_count` — consulted on essentially every property read and
+write — lifted the whole ~48-byte record and kept only
+`live_inline_slot_count`. It now reads that field through the table's record
+using the same way-cache probe and the same epoch validation.
+`shape_descriptor_by_id` was 10.1% of the same loop.
+
+Interleaved A/B, min-of-21, node on the same host in brackets:
+
+| loop | base | this PR | |
+|---|---|---|---|
+| pure property read | 21 ms (4) | **17 ms** | −19% |
+| computed-key read | 27 ms (23) | **23 ms** | −15% — now at parity with node |
+| combined overwrite | 46 ms (31) | **41 ms** | −11% |
+| write only | 21 ms (23) | 21 ms | unchanged; already faster than node |
+
+Suite 2779 passed (including the 55 typed-feedback tests). Private-member
+output is byte-identical to base; computed-key differential is byte-identical
+to node.
diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs
index 41c766ed3f..abef1bbf5f 100644
--- a/crates/perry-codegen/src/lower_call/builtin.rs
+++ b/crates/perry-codegen/src/lower_call/builtin.rs
@@ -1056,6 +1056,10 @@ pub(super) fn lower_builtin_new<'a>(
}
"Response" => {
// new Response(body?, init?) — init = { status?, statusText?, headers? }
+ // Clear BodyInit metadata before evaluating either argument: init
+ // evaluation can throw after the body was converted, in which case
+ // js_response_new never gets a chance to consume that metadata.
+ ctx.block().call(DOUBLE, "js_response_body_init_reset", &[]);
// Route the body through js_response_body_init_ptr (not the plain
// string coercion) so a ReadableStream body — e.g. Hono's
// `new Response(res.body, res)` header re-wrap — is drained to its
diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs
index c8d9417808..c3301947c6 100644
--- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs
+++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs
@@ -949,6 +949,9 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
// Normalize a Request/Response subclass object (for example NextResponse)
// to its native Fetch registry handle; bare handles pass through.
module.declare_function("js_fetch_unwrap_handle", DOUBLE, &[DOUBLE]);
+ // Response BodyInit metadata is reset before argument evaluation so a
+ // throwing init expression cannot leak it into a later construction.
+ module.declare_function("js_response_body_init_reset", DOUBLE, &[]);
// js_response_body_init_ptr(body_value_f64) -> string_ptr (i64): drains a
// ReadableStream body to bytes, else falls back to string coercion.
module.declare_function("js_response_body_init_ptr", I64, &[DOUBLE]);
diff --git a/crates/perry-hir/src/lower/mod.rs b/crates/perry-hir/src/lower/mod.rs
index 6ce65947ac..7a2cc5d62c 100644
--- a/crates/perry-hir/src/lower/mod.rs
+++ b/crates/perry-hir/src/lower/mod.rs
@@ -42,7 +42,9 @@ mod expr_call;
pub(crate) mod expr_function;
pub(crate) use expr_function::capture_function_source;
mod expr_member;
-pub(crate) use expr_member::{private_storage_property, wrap_private_guard, PRIV_OP_WRITE};
+pub(crate) use expr_member::{
+ private_storage_property, wrap_private_guard, PRIV_OP_READ, PRIV_OP_WRITE,
+};
mod expr_misc;
mod expr_new;
mod expr_new_builtins;
diff --git a/crates/perry-hir/src/lower_patterns.rs b/crates/perry-hir/src/lower_patterns.rs
index 217044e333..204f860179 100644
--- a/crates/perry-hir/src/lower_patterns.rs
+++ b/crates/perry-hir/src/lower_patterns.rs
@@ -4,7 +4,9 @@
//! parameter destructuring, and other pattern-related utilities.
use crate::ir::*;
-use crate::lower::{lower_expr, LoweringContext};
+use crate::lower::{
+ lower_expr, private_storage_property, wrap_private_guard, LoweringContext, PRIV_OP_READ,
+};
use crate::lower_types::*;
use crate::types::{LocalId, Type};
use anyhow::{anyhow, Result};
@@ -244,9 +246,14 @@ pub(crate) fn lower_assign_target_to_expr(
Ok(Expr::IndexGet { object, index })
}
ast::MemberProp::PrivateName(private) => {
- let property = format!("#{}", private.name);
+ // Compound and logical assignments lower the read and write
+ // halves separately. Match ordinary private-member reads:
+ // guard the receiver and use the class-mangled storage key.
+ let private_name = format!("#{}", private.name);
+ let object = wrap_private_guard(ctx, object, &private_name, PRIV_OP_READ);
+ let property = private_storage_property(ctx, &private_name);
Ok(Expr::PropertyGet {
- byte_offset: 0,
+ byte_offset: member.span.lo.0,
object,
property,
})
diff --git a/crates/perry-stdlib/src/fetch/dispatch.rs b/crates/perry-stdlib/src/fetch/dispatch.rs
index dbda44fbf4..be438ff768 100644
--- a/crates/perry-stdlib/src/fetch/dispatch.rs
+++ b/crates/perry-stdlib/src/fetch/dispatch.rs
@@ -18,6 +18,14 @@
use super::*;
use perry_runtime::{js_get_string_pointer_unified, js_jsvalue_to_string};
+/// Clear metadata left by an earlier Response whose init evaluation threw
+/// before `js_response_new` could consume it.
+#[no_mangle]
+pub extern "C" fn js_response_body_init_reset() -> f64 {
+ reset_pending_fetch_body_init();
+ f64::from_bits(TAG_UNDEFINED)
+}
+
/// Coerce a `Response` body-init value to a `*const StringHeader` (returned as
/// i64, mirroring `js_get_string_pointer_unified`).
///
@@ -30,6 +38,11 @@ use perry_runtime::{js_get_string_pointer_unified, js_jsvalue_to_string};
/// (`c.json`/`c.text`) are unaffected.
#[no_mangle]
pub extern "C" fn js_response_body_init_ptr(value: f64) -> i64 {
+ set_pending_fetch_body_content_type(
+ JSValue::from_bits(value.to_bits())
+ .is_any_string()
+ .then_some(BODY_CONTENT_TYPE_TEXT_PLAIN),
+ );
// A binary body — Buffer / Uint8Array / typed array / ArrayBuffer — must
// copy its RAW bytes. Such a value is a BufferHeader/TypedArrayHeader
// pointer, NOT a StringHeader; passing it straight to `string_from_header`
diff --git a/crates/perry-stdlib/src/fetch/headers.rs b/crates/perry-stdlib/src/fetch/headers.rs
index 9ef2b4cf82..792ed67c43 100644
--- a/crates/perry-stdlib/src/fetch/headers.rs
+++ b/crates/perry-stdlib/src/fetch/headers.rs
@@ -100,6 +100,20 @@ fn is_headers_init_iterable(value: f64) -> bool {
|| has_sync_iterator(value)
}
+/// Read a plain-object `HeadersInit` without changing registry ownership.
+/// Response construction uses this when codegen passes a runtime header record
+/// rather than a `Headers` registry handle.
+pub(super) fn headers_store_from_record_value(init: f64) -> Option {
+ let scope = perry_runtime::gc::RuntimeHandleScope::new();
+ let rooted = scope.root_nanbox_f64(init);
+ let entries = read_headers_record_entries(rooted.get_nanbox_f64(), &scope)?;
+ let mut store = HeadersStore::default();
+ for (key, value) in entries {
+ store.append(&key, &value);
+ }
+ Some(store)
+}
+
fn read_headers_record_entries(
value: f64,
scope: &perry_runtime::gc::RuntimeHandleScope,
diff --git a/crates/perry-stdlib/src/fetch/mod.rs b/crates/perry-stdlib/src/fetch/mod.rs
index 81bc085ce3..ef57a1fcfb 100644
--- a/crates/perry-stdlib/src/fetch/mod.rs
+++ b/crates/perry-stdlib/src/fetch/mod.rs
@@ -273,6 +273,26 @@ fn response_headers_snapshot(response: &FetchResponse) -> HeadersStore {
thread_local! {
static PENDING_FETCH_BODY_STREAM_ID: Cell = const { Cell::new(0) };
+ // Codegen coerces BodyInit to a StringHeader before js_response_new, so
+ // preserve whether the original value was a string long enough for the
+ // Response constructor to install Fetch's default Content-Type.
+ static PENDING_FETCH_BODY_CONTENT_TYPE: Cell