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 changelog.d/8765-mysql2-request-scoped-statements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed mysql2 prepared statements and pool transactions leaking state across requests. Each SQL string and parameter vector now lives in one owned request, a parameterless `query()` uses the text protocol, and prepared statements are request-scoped so cached metadata cannot cross requests. Registry-backed mutable connection references are replaced with serialized owned handles, with safe close/release behaviour around in-flight work, and checked-out pool connections route through `query`, `execute`, `beginTransaction`, `commit`, `rollback` and `release`.
1 change: 1 addition & 0 deletions changelog.d/8767-forwarded-arrays-versioned-loops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Version-stable indexed loops now admit arrays reached through one validated forwarding edge, canonicalizing the compiler-private local to the live array after the full header/fingerprint check. Per-iteration fingerprint guards are retained, so callback-driven growth or a GC still side-exits before the next effect, and invalid targets or longer forwarding chains fail closed to the generic loop.
4 changes: 4 additions & 0 deletions changelog.d/8768-runtime-parent-function-prototype.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed subclasses of runtime-valued ordinary functions failing during prototype
materialization when the parent function's lazy `.prototype` had not already
been read. `class Child extends Parent` now observes and links the same
prototype object as an ordinary `Parent.prototype` read.
1 change: 1 addition & 0 deletions changelog.d/8769-http-ws-reactor-scheduling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed shared HTTP/WS servers by scheduling HTTP and HTTPS accept loops through Perry's explicit reactor-owned async bridge, using the same path for Unix round-robin file-descriptor injection. Adds an end-to-end regression covering a `WebSocketServer` attached to a `node:http` server followed by a plain `fetch`.
53 changes: 53 additions & 0 deletions crates/perry-codegen/src/codegen/index_method_clone_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,59 @@ fn checked_reader_callback_loop_versions_to_fast_and_resumable_slow_bodies() {
);
}

#[test]
fn versioned_checked_reader_admission_canonicalizes_one_forwarding_edge() {
let ir = emit_versioned_checked_reader_loop();
let iterate = function_body(
&ir,
"@perry_method_versioned_checked_reader_loop_ts__Reader__iterate(",
);
let source_guard = iterate
.split("\nversioned_index.array.source_deref.")
.nth(1)
.and_then(|body| body.split("\nversioned_index.array.live_deref.").next())
.unwrap_or_else(|| panic!("loop has no forwarding-source guard:\n{iterate}"));
let live_handle = source_guard
.lines()
.find(|line| line.contains(" = select i1") && line.contains(", i64 "))
.and_then(|line| line.trim().split_once(" = ").map(|(name, _)| name))
.unwrap_or_else(|| panic!("source guard has no selected live handle:\n{source_guard}"));
assert!(
source_guard.contains("and i8")
&& source_guard.contains(", 128")
&& source_guard.contains("load i64")
&& source_guard.contains("label %versioned_index.array.live_deref.")
&& source_guard.contains("label %versioned_index.loop.slow.preheader")
&& !source_guard.contains(&format!("sub i64 {live_handle}, 8")),
"admission must select one forwarding target and validate its address before \
reading its header:\n{source_guard}"
);
let live_guard = iterate
.split("\nversioned_index.array.live_deref.")
.nth(1)
.and_then(|body| body.split("\nversioned_index.array.canonicalize.").next())
.unwrap_or_else(|| panic!("loop has no selected-target header guard:\n{iterate}"));
assert!(
live_guard.contains(&format!("sub i64 {live_handle}, 8"))
&& live_guard.contains("label %versioned_index.array.canonicalize.")
&& live_guard.contains("label %versioned_index.loop.slow.preheader"),
"the selected target must be fully re-branded before admission:\n{live_guard}"
);
let canonicalize = iterate
.split("\nversioned_index.array.canonicalize.")
.nth(1)
.and_then(|body| body.split("\nversioned_index.array.source_deref.").next())
.unwrap_or_else(|| panic!("loop has no canonicalization block:\n{iterate}"));
assert!(
canonicalize.contains(&format!(
"or i64 {live_handle}, {}",
crate::nanbox::POINTER_TAG_I64
)) && canonicalize.contains("store ptr addrspace(1)"),
"the uncaptured array local must be rewritten to the admitted live target so \
iteration guards do not revisit an identity stub:\n{canonicalize}"
);
}

#[test]
fn guarded_read_can_follow_one_forwarding_edge_but_rechecks_the_live_header() {
let ir = emit();
Expand Down
56 changes: 50 additions & 6 deletions crates/perry-codegen/src/stmt/versioned_indexed_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,12 @@ fn emit_array_admission(
slow_label: &str,
) -> Option<(String, String)> {
let local_slot = ctx.locals.get(&local_id)?.clone();
let deref_idx = ctx.new_block("versioned_index.array.deref");
let deref_label = ctx.block_label(deref_idx);
let source_deref_idx = ctx.new_block("versioned_index.array.source_deref");
let source_deref_label = ctx.block_label(source_deref_idx);
let live_deref_idx = ctx.new_block("versioned_index.array.live_deref");
let live_deref_label = ctx.block_label(live_deref_idx);
let canonicalize_idx = ctx.new_block("versioned_index.array.canonicalize");
let canonicalize_label = ctx.block_label(canonicalize_idx);
let heap_floor =
crate::target_layout::heap_addr_lower_bound_inclusive(ctx.target_triple).to_string();
let heap_ceiling =
Expand All @@ -262,10 +266,40 @@ fn emit_array_admission(
let below_ceiling = ctx.block().icmp_ult(I64, &array_handle, &heap_ceiling);
let in_heap = ctx.block().and(I1, &above_floor, &below_ceiling);
let safe = ctx.block().and(I1, &is_pointer, &in_heap);
ctx.block().cond_br(&safe, &deref_label, slow_label);
ctx.block().cond_br(&safe, &source_deref_label, slow_label);

ctx.current_block = deref_idx;
let fingerprint_addr = ctx.block().sub(I64, &array_handle, "8");
// Array growth leaves a forwarding stub at the identity-bearing address.
// Mirror the ordinary indexed-read guard: follow at most one edge, then
// validate the selected address before touching its header. A longer chain
// remains fail-closed and resumes the generic loop.
ctx.current_block = source_deref_idx;
let source_gc_type_addr = ctx.block().sub(I64, &array_handle, "8");
let source_gc_type_ptr = ctx.block().inttoptr(I64, &source_gc_type_addr);
let source_gc_type = ctx.block().load(I8, &source_gc_type_ptr);
let source_is_array = ctx.block().icmp_eq(I8, &source_gc_type, "1");
let source_flags_addr = ctx.block().sub(I64, &array_handle, "7");
let source_flags_ptr = ctx.block().inttoptr(I64, &source_flags_addr);
let source_flags = ctx.block().load(I8, &source_flags_ptr);
let source_forwarded_bits = ctx.block().and(I8, &source_flags, "128");
let source_is_forwarded = ctx.block().icmp_ne(I8, &source_forwarded_bits, "0");
let source_ptr = ctx.block().inttoptr(I64, &array_handle);
let forwarding_target = ctx.block().load(I64, &source_ptr);
let follow_forwarding = ctx.block().and(I1, &source_is_array, &source_is_forwarded);
let live_handle = ctx.block().select(
I1,
&follow_forwarding,
I64,
&forwarding_target,
&array_handle,
);
let live_above_floor = ctx.block().icmp_uge(I64, &live_handle, &heap_floor);
let live_below_ceiling = ctx.block().icmp_ult(I64, &live_handle, &heap_ceiling);
let live_in_heap = ctx.block().and(I1, &live_above_floor, &live_below_ceiling);
ctx.block()
.cond_br(&live_in_heap, &live_deref_label, slow_label);

ctx.current_block = live_deref_idx;
let fingerprint_addr = ctx.block().sub(I64, &live_handle, "8");
let fingerprint_ptr = ctx.block().inttoptr(I64, &fingerprint_addr);
let fingerprint = ctx.block().load_aligned(I128, &fingerprint_ptr, 8);
let gc_header = ctx.block().trunc(I128, &fingerprint, I64);
Expand Down Expand Up @@ -298,7 +332,17 @@ fn emit_array_admission(
pass = ctx.block().and(I1, &pass, &length_sane);
pass = ctx.block().and(I1, &pass, &capacity_sane);
pass = ctx.block().and(I1, &pass, &length_within_capacity);
ctx.block().cond_br(&pass, success_label, slow_label);
ctx.block().cond_br(&pass, &canonicalize_label, slow_label);

// Candidate analysis excludes rebinding and closure capture of this local,
// so replacing its internal root with the live address is unobservable.
// It also makes the existing per-iteration fingerprint guard O(1): a later
// growth/GC move turns this live address into a stub and side-exits before
// any effect, instead of re-walking an already-stale identity stub forever.
ctx.current_block = canonicalize_idx;
let live_box = crate::expr::nanbox_pointer_inline(ctx.block(), &live_handle);
ctx.block().store(DOUBLE, &live_box, &local_slot);
ctx.block().br(success_label);
Some((local_slot, fingerprint))
}

Expand Down
Loading
Loading