Skip to content

Commit ee0cbf2

Browse files
committed
rusty OuterVariables
1 parent c48cde4 commit ee0cbf2

5 files changed

Lines changed: 70 additions & 39 deletions

File tree

zjit.c

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM);
3030

3131
enum zjit_struct_offsets {
32-
ISEQ_BODY_OFFSET_PARAM = offsetof(struct rb_iseq_constant_body, param)
32+
ISEQ_BODY_OFFSET_PARAM = offsetof(struct rb_iseq_constant_body, param),
33+
ISEQ_BODY_OFFSET_OUTER_VARIABLES = offsetof(struct rb_iseq_constant_body, outer_variables)
3334
};
3435

3536
// Special JITFrame used by all C method calls. We don't control the native
@@ -220,23 +221,6 @@ rb_zjit_local_id(const rb_iseq_t *iseq, unsigned idx)
220221
return ISEQ_BODY(iseq)->local_table[idx];
221222
}
222223

223-
// True if `blockiseq` (or any iseq nested within it) has bytecode that assigns
224-
// to the outer local variable named `id`.
225-
// The same table backs Ractor.shareable_proc's isolation checks.
226-
bool
227-
rb_zjit_iseq_writes_outer_local_p(const rb_iseq_t *blockiseq, ID id)
228-
{
229-
struct rb_id_table *ovs = ISEQ_BODY(blockiseq)->outer_variables;
230-
if (ovs == NULL) return false;
231-
VALUE write = Qfalse;
232-
if (rb_id_table_lookup(ovs, id, &write)) {
233-
// Table entry precense means local is referenced.
234-
// Truth entry means it's referenced through a setlocal.
235-
return RTEST(write);
236-
}
237-
return false;
238-
}
239-
240224
bool rb_zjit_cme_is_cfunc(const rb_callable_method_entry_t *me, const void *func);
241225

242226
const struct rb_callable_method_entry_struct *

zjit/bindgen/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ fn main() {
297297
.allowlist_function("rb_zjit_iseq_inspect")
298298
.allowlist_function("rb_zjit_iseq_insn_set")
299299
.allowlist_function("rb_zjit_local_id")
300-
.allowlist_function("rb_zjit_iseq_writes_outer_local_p")
300+
.allowlist_function("rb_id_table_lookup")
301301
.allowlist_function("rb_set_cfp_(pc|sp)")
302302
.allowlist_function("rb_c_method_tracing_currently_enabled")
303303
.allowlist_function("rb_zjit_method_tracing_currently_enabled")
@@ -451,6 +451,9 @@ fn main() {
451451
.blocklist_type("ID")
452452
.blocklist_type("rb_iseq_constant_body")
453453

454+
// We only need id_table as an opaque pointer to pass to its APIs
455+
.opaque_type("rb_id_table")
456+
454457
// Avoid binding to stuff we don't use
455458
.blocklist_item("rb_thread_struct.*")
456459
.opaque_type("rb_thread_struct.*")

zjit/src/cruby.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ use std::ffi::{c_void, CString, CStr};
9393
use std::fmt::{Debug, Display, Formatter};
9494
use std::os::raw::{c_char, c_int, c_long, c_uint};
9595
use std::panic::{catch_unwind, UnwindSafe};
96+
use std::ptr::NonNull;
9697

9798
use crate::cast::IntoUsize as _;
9899

@@ -749,9 +750,42 @@ impl VALUE {
749750

750751
pub type IseqParameters = rb_iseq_constant_body_rb_iseq_parameters;
751752

753+
/// How a block iseq refers to a variable in an enclosing scope, as recorded in
754+
/// `ISEQ_BODY(blockiseq)->outer_variables`. `compile.c` aggregates accesses from
755+
/// nested blocks up the chain, and the same table backs `Ractor.shareable_proc`'s
756+
/// isolation checks.
757+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
758+
pub enum OuterLocalAccess {
759+
/// The variable is read but never assigned to.
760+
ReadOnly,
761+
/// The variable is assigned to and maybe also read.
762+
ReadWrite,
763+
}
764+
765+
/// Wrapper over an iseq's `outer_variables` table, which describes
766+
/// how a block iseq refers to a variable in an enclosing scope.
767+
#[derive(Clone, Copy)]
768+
pub struct OuterVariables(Option<NonNull<rb_id_table>>);
769+
770+
impl OuterVariables {
771+
/// Look up how the enclosing-scope local `id` is accessed by the iseq (or any
772+
/// iseq nested within it). Returns `None` when the variable isn't referenced.
773+
pub fn local_access(self, id: ID) -> Option<OuterLocalAccess> {
774+
let table = self.0?;
775+
let mut write = Qfalse;
776+
// Non-zero return means there's a table entry, i.e. the variable is referenced.
777+
if unsafe { rb_id_table_lookup(table.as_ptr(), id, &mut write) } == 0 {
778+
return None;
779+
}
780+
// Truthy means write
781+
Some(if write.test() { OuterLocalAccess::ReadWrite } else { OuterLocalAccess::ReadOnly })
782+
}
783+
}
784+
752785
/// Extension trait to enable method calls on [`IseqPtr`]
753786
pub trait IseqAccess {
754787
unsafe fn params<'a>(self) -> &'a IseqParameters;
788+
unsafe fn outer_variables(self) -> OuterVariables;
755789
}
756790

757791
impl IseqAccess for IseqPtr {
@@ -760,6 +794,13 @@ impl IseqAccess for IseqPtr {
760794
use crate::cast::IntoUsize;
761795
unsafe { &*((*self).body.byte_add(ISEQ_BODY_OFFSET_PARAM.to_usize()) as *const IseqParameters) }
762796
}
797+
798+
/// The iseq's `outer_variables` table. See [`OuterVariables`].
799+
unsafe fn outer_variables(self) -> OuterVariables {
800+
use crate::cast::IntoUsize;
801+
let field = unsafe { (*self).body.byte_add(ISEQ_BODY_OFFSET_OUTER_VARIABLES.to_usize()) } as *const *mut rb_id_table;
802+
OuterVariables(NonNull::new(unsafe { *field }))
803+
}
763804
}
764805

765806
impl IseqParameters {

zjit/src/cruby_bindings.inc.rs

Lines changed: 8 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zjit/src/hir.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5058,21 +5058,26 @@ impl Function {
50585058
&mut (0..state.locals.len())
50595059
} else {
50605060
let params = unsafe { iseq.params() };
5061-
let block_param_local_idx = if params.flags.has_block() != 0 {
5062-
Some(params.block_start)
5061+
let block_param_local_idx: Option<usize> = if params.flags.has_block() != 0 {
5062+
params.block_start.try_into().ok()
50635063
} else {
50645064
None
50655065
};
5066-
// When not escaped, only reload the ones syntactically written to
5066+
let outer_variables = unsafe { blockiseq.outer_variables() };
5067+
// When not escaped, only reload the locals the block can have modified.
50675068
&mut (0..state.locals.len()).filter(move |&local_idx| {
5068-
if block_param_local_idx.and_then(|idx| idx.try_into().ok()).is_some_and(|idx: usize| idx == local_idx) {
5069-
// An ostensibly read of the the block param, through `getblockparam` can
5070-
// write to the local slot for it. TODO(alan): no reload when outer blocker
5071-
// param not referenced in block.
5072-
return true;
5073-
}
50745069
let id = unsafe { rb_zjit_local_id(iseq, local_idx.try_into().unwrap()) };
5075-
unsafe { rb_zjit_iseq_writes_outer_local_p(blockiseq, id) }
5070+
let access = outer_variables.local_access(id);
5071+
if block_param_local_idx == Some(local_idx) {
5072+
// The block param slot is special: `getblockparam` is recorded as a
5073+
// read, but it materializes the captured block into this slot. So
5074+
// reload it whenever the block references it at all (read or write),
5075+
// not just on a setlocal. When the block never references it, the
5076+
// slot can't have changed, so skip the reload.
5077+
access.is_some()
5078+
} else {
5079+
access == Some(OuterLocalAccess::ReadWrite)
5080+
}
50765081
})
50775082
};
50785083
let mut base: Option<InsnId> = None;

0 commit comments

Comments
 (0)