use crate::bytecode::{CodeUnit, Opcode};
use crate::heap::arena::Arena;
use crate::heap::object::{NONE, ObjectHeader, ObjectKind, flags};
use crate::heap::string::StringRecord;
use crate::heap::values::ValueHeap;
use crate::shape::{STATIC_CELL, ValidityCell};
use crate::value::{ShapeId, ValidityCellId, Value};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum IcKind {
Get,
Set,
Global,
Construct,
}
impl IcKind {
pub const fn of(op: Opcode) -> Option<IcKind> {
match op {
Opcode::GetField => Some(IcKind::Get),
Opcode::SetField => Some(IcKind::Set),
Opcode::GetGlobal | Opcode::GetGlobalOrUndefined | Opcode::SetGlobal => {
Some(IcKind::Global)
}
Opcode::New => Some(IcKind::Construct),
_ => None,
}
}
pub const fn name(self) -> &'static str {
match self {
IcKind::Get => "get",
IcKind::Set => "set",
IcKind::Global => "global",
IcKind::Construct => "construct",
}
}
}
pub const FIELD_NO_SITE: u8 = u8::MAX;
pub const GLOBAL_NO_SITE: u32 = u16::MAX as u32;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IcEntry {
pub w0: u32,
pub w1: u32,
pub w2: u32,
pub w3: u32,
}
const _: () = assert!(size_of::<IcEntry>() == 16);
pub const ENTRY_BYTES: usize = size_of::<IcEntry>() + size_of::<IcKind>();
pub const EMPTY_TAG: u32 = NONE;
pub const STRING_RECEIVER_TAG: u32 = NONE - 1;
pub const POLY_TAG: u32 = NONE - 2;
pub const MEGA_TAG: u32 = NONE - 3;
pub const LENGTH_TAG: u32 = NONE - 4;
pub const POLY: usize = 4;
pub const MEGA_ENTRIES: usize = 4096;
impl IcEntry {
pub const EMPTY: IcEntry = IcEntry {
w0: EMPTY_TAG,
w1: NONE,
w2: NONE,
w3: NONE,
};
#[inline]
pub fn is_empty(&self) -> bool {
self.w0 == EMPTY_TAG
}
#[inline]
pub fn is_filled(&self) -> bool {
self.w0 < LENGTH_TAG || self.w0 == STRING_RECEIVER_TAG || self.w0 == LENGTH_TAG
}
pub fn cell(&self) -> ValidityCellId {
ValidityCellId(self.w3)
}
}
#[inline(always)]
pub(crate) fn get_hit(
objects: &Arena<ObjectHeader>,
cells: &Arena<ValidityCell>,
values: &ValueHeap,
strings: &Arena<StringRecord>,
entry: IcEntry,
receiver: Value,
) -> Option<Value> {
let holder = if let Some(object) = receiver.as_object() {
let header = objects.get(object.0);
if header.shape.0 != entry.w0 {
if entry.w0 == LENGTH_TAG && header.kind == ObjectKind::Array {
return Some(length_value(header.payload as usize));
}
return None;
}
if entry.w1 == NONE {
return Some(values.get_at(header.slots_start + entry.w2));
}
entry.w1
} else {
let string = receiver.as_string()?;
if entry.w0 == LENGTH_TAG {
return Some(length_value(strings.get(string.index()).len()));
}
if entry.w0 != STRING_RECEIVER_TAG {
return None;
}
entry.w1
};
if !cells.get(entry.w3).valid {
return None;
}
Some(values.get_at(objects.get(holder).slots_start + entry.w2))
}
#[inline(always)]
fn length_value(length: usize) -> Value {
match i32::try_from(length) {
Ok(length) => Value::int32(length),
Err(_) => Value::number(length as f64),
}
}
#[inline(always)]
pub(crate) fn set_hit(
objects: &mut Arena<ObjectHeader>,
cells: &Arena<ValidityCell>,
values: &mut ValueHeap,
entry: IcEntry,
receiver: Value,
value: Value,
) -> bool {
let Some(object) = receiver.as_object() else {
return false;
};
let header = objects.get_mut(object.0);
if header.shape.0 != entry.w0 {
return false;
}
if entry.w1 != entry.w0 {
let plain = header.flags & (flags::EXTENSIBLE | flags::USED_AS_PROTOTYPE);
if entry.w2 >= header.slots().len
|| plain != flags::EXTENSIBLE
|| !cells.get(entry.w3).valid
{
return false;
}
header.shape = ShapeId(entry.w1);
}
values.set_at(header.slots_start + entry.w2, value);
true
}
#[derive(Clone, Debug, Default)]
pub struct IcStats {
pub mono_hits: [u64; 4],
pub poly_hits: [u64; 4],
pub fills: [u64; 4],
pub unfilled: [u64; 4],
pub site_misses: Vec<u64>,
pub iter_pristine: u64,
pub iter_generic: u64,
}
#[derive(Default)]
pub struct IcStore {
pub(crate) entries: Vec<IcEntry>,
pub(crate) kinds: Vec<IcKind>,
free: Vec<(u32, u32)>,
pub(crate) poly: Vec<[IcEntry; POLY]>,
poly_kinds: Vec<IcKind>,
poly_free: Vec<u32>,
mega: Vec<(u32, u32, IcEntry)>,
#[cfg(any(feature = "ic-stats", debug_assertions))]
stats: IcStats,
}
impl IcStore {
pub(crate) fn reserve(&mut self, unit: &mut CodeUnit) {
let count = unit.ic_count;
let end = self.entries.len() as u32;
let first = match self.free.iter().position(|&(_, len)| len >= count) {
_ if count == 0 => end,
Some(position) => {
let (start, len) = self.free[position];
if len == count {
self.free.remove(position);
} else {
self.free[position] = (start + count, len - count);
}
start
}
None if self
.free
.last()
.is_some_and(|&(start, len)| start + len == end) =>
{
let (start, _) = self.free.pop().expect("a free range");
start
}
None => end,
};
let len = first.checked_add(count).expect("cache store overflow") as usize;
if len > self.entries.len() {
self.entries.resize(len, IcEntry::EMPTY);
self.kinds.resize(len, IcKind::Get);
#[cfg(any(feature = "ic-stats", debug_assertions))]
self.stats.site_misses.resize(len, 0);
}
let first = first as usize;
self.kinds[first..len].fill(IcKind::Get);
for insn in unit.code.iter() {
if let (Some(kind), Some(site)) = (IcKind::of(insn.op), insn.site()) {
self.kinds[first + site as usize] = kind;
}
}
unit.ic_base = first as u32;
}
pub(crate) fn release(&mut self, unit: &CodeUnit) {
let (start, count) = (unit.ic_base, unit.ic_count);
if count == 0 {
return;
}
let range = start as usize..(start + count) as usize;
for entry in &mut self.entries[range.clone()] {
if entry.w0 == POLY_TAG {
self.poly[entry.w1 as usize] = [IcEntry::EMPTY; POLY];
self.poly_free.push(entry.w1);
}
*entry = IcEntry::EMPTY;
}
#[cfg(any(feature = "ic-stats", debug_assertions))]
self.stats.site_misses[range].fill(0);
let position = self.free.partition_point(|&(free, _)| free < start);
let after = self
.free
.get(position)
.is_some_and(|&(free, _)| free == start + count);
let before = position > 0 && {
let (free, len) = self.free[position - 1];
free + len == start
};
match (before, after) {
(true, true) => {
let (_, next) = self.free.remove(position);
self.free[position - 1].1 += count + next;
}
(true, false) => self.free[position - 1].1 += count,
(false, true) => {
let next = &mut self.free[position];
*next = (start, next.1 + count);
}
(false, false) => self.free.insert(position, (start, count)),
}
}
pub fn free_len(&self) -> usize {
self.free.iter().map(|&(_, len)| len as usize).sum()
}
pub(crate) fn verify_free(&self) {
for &(start, len) in &self.free {
let range = start as usize..(start + len) as usize;
assert!(
self.entries[range].iter().all(IcEntry::is_empty),
"gc verify: a free cache range at {start} is not empty"
);
}
for &row in &self.poly_free {
assert!(
self.poly[row as usize].iter().all(IcEntry::is_empty),
"gc verify: free polymorphic row {row} is not empty"
);
}
}
#[inline(always)]
pub fn entry(&self, index: usize) -> IcEntry {
self.entries[index]
}
pub fn kind(&self, index: usize) -> IcKind {
self.kinds[index]
}
#[inline]
pub fn poly_row(&self, index: usize) -> Option<&[IcEntry; POLY]> {
let entry = self.entries[index];
(entry.w0 == POLY_TAG).then(|| &self.poly[entry.w1 as usize])
}
pub(crate) fn record(&mut self, index: usize, fill: IcEntry) -> bool {
let kind = self.kinds[index];
let current = self.entries[index];
if !matches!(kind, IcKind::Get | IcKind::Set)
|| (!current.is_filled() && current.w0 != POLY_TAG)
{
if current.w0 == MEGA_TAG {
return false;
}
self.entries[index] = fill;
return true;
}
if current.is_filled() {
if current.w0 == fill.w0 {
self.entries[index] = fill;
} else {
let entries = [current, fill, IcEntry::EMPTY, IcEntry::EMPTY];
let row = match self.poly_free.pop() {
Some(row) => {
self.poly[row as usize] = entries;
self.poly_kinds[row as usize] = kind;
row
}
None => {
self.poly.push(entries);
self.poly_kinds.push(kind);
self.poly.len() as u32 - 1
}
};
self.entries[index] = IcEntry {
w0: POLY_TAG,
w1: row,
w2: NONE,
w3: NONE,
};
}
return true;
}
let row = &mut self.poly[current.w1 as usize];
let slot = row
.iter()
.position(|entry| entry.w0 == fill.w0)
.or_else(|| row.iter().position(IcEntry::is_empty));
match slot {
Some(slot) => {
row[slot] = fill;
true
}
None => {
let row = current.w1;
self.poly[row as usize] = [IcEntry::EMPTY; POLY];
self.poly_free.push(row);
self.entries[index] = IcEntry {
w0: MEGA_TAG,
..IcEntry::EMPTY
};
false
}
}
}
#[inline]
fn mega_index(shape: u32, key: u32) -> usize {
let hash = shape.wrapping_mul(0x9E37_79B9) ^ key.wrapping_mul(0x85EB_CA6B);
(hash >> 20) as usize & (MEGA_ENTRIES - 1)
}
#[inline]
pub(crate) fn mega_entry(&self, shape: u32, key: u32) -> Option<IcEntry> {
let &(entry_shape, entry_key, entry) = self.mega.get(Self::mega_index(shape, key))?;
(entry_shape == shape && entry_key == key).then_some(entry)
}
pub(crate) fn record_mega(&mut self, key: u32, fill: IcEntry) {
if self.mega.is_empty() {
self.mega
.resize(MEGA_ENTRIES, (EMPTY_TAG, NONE, IcEntry::EMPTY));
}
let index = Self::mega_index(fill.w0, key);
self.mega[index] = (fill.w0, key, fill);
}
#[inline]
pub(crate) fn is_megamorphic(&self, index: usize) -> bool {
self.entries[index].w0 == MEGA_TAG
}
pub(crate) fn clear_kind(&mut self, kind: IcKind) {
for (entry, &entry_kind) in self.entries.iter_mut().zip(&self.kinds) {
if entry_kind == kind {
*entry = IcEntry::EMPTY;
}
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn bytes(&self) -> usize {
self.entries.capacity() * size_of::<IcEntry>()
+ self.kinds.capacity() * size_of::<IcKind>()
+ self.free.capacity() * size_of::<(u32, u32)>()
+ self.poly.capacity() * size_of::<[IcEntry; POLY]>()
+ self.poly_kinds.capacity() * size_of::<IcKind>()
+ self.poly_free.capacity() * size_of::<u32>()
+ self.mega.capacity() * size_of::<(u32, u32, IcEntry)>()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn filled(&self) -> usize {
self.entries
.iter()
.filter(|entry| entry.is_filled())
.count()
}
pub fn site_states(&self) -> (usize, usize, usize) {
let count = |tag: u32| self.entries.iter().filter(|entry| entry.w0 == tag).count();
(self.filled(), count(POLY_TAG), count(MEGA_TAG))
}
#[inline(always)]
pub(crate) fn count_hit(&mut self, kind: IcKind) {
#[cfg(any(feature = "ic-stats", debug_assertions))]
{
self.stats.mono_hits[kind as usize] += 1;
}
let _ = kind;
}
#[inline(always)]
pub(crate) fn count_iteration(&mut self, pristine: bool) {
#[cfg(any(feature = "ic-stats", debug_assertions))]
{
if pristine {
self.stats.iter_pristine += 1;
} else {
self.stats.iter_generic += 1;
}
}
let _ = pristine;
}
#[inline(always)]
pub(crate) fn count_poly_hit(&mut self, kind: IcKind) {
#[cfg(any(feature = "ic-stats", debug_assertions))]
{
self.stats.poly_hits[kind as usize] += 1;
}
let _ = kind;
}
#[inline(always)]
pub(crate) fn count_miss(&mut self, index: usize, filled: bool) {
#[cfg(any(feature = "ic-stats", debug_assertions))]
{
let kind = self.kinds[index] as usize;
if filled {
self.stats.fills[kind] += 1;
} else {
self.stats.unfilled[kind] += 1;
}
self.stats.site_misses[index] += 1;
}
let _ = (index, filled);
}
pub fn stats(&self) -> Option<&IcStats> {
#[cfg(any(feature = "ic-stats", debug_assertions))]
return Some(&self.stats);
#[cfg(not(any(feature = "ic-stats", debug_assertions)))]
None
}
pub(crate) fn named_records(&self) -> impl Iterator<Item = Record> + '_ {
let mono = self
.entries
.iter()
.zip(&self.kinds)
.filter(|(entry, _)| entry.is_filled())
.flat_map(|(&entry, &kind)| records(kind, entry));
let poly = self
.poly
.iter()
.zip(&self.poly_kinds)
.flat_map(|(row, &kind)| {
row.iter()
.filter(|entry| entry.is_filled())
.flat_map(move |&entry| records(kind, entry))
});
let mega = self
.mega
.iter()
.filter(|(_, _, entry)| entry.is_filled())
.flat_map(|&(_, key, entry)| records(IcKind::Get, entry).chain(key_atom(key)));
mono.chain(poly).chain(mega)
}
pub(crate) fn prune(&mut self, mut live: impl FnMut(Record) -> bool) -> usize {
let mut cleared = 0;
let rows = self.poly.iter_mut().zip(&self.poly_kinds);
let poly = rows.flat_map(|(row, kind)| row.iter_mut().map(move |entry| (entry, kind)));
for (entry, &kind) in self.entries.iter_mut().zip(&self.kinds).chain(poly) {
if entry.is_filled() && !records(kind, *entry).all(&mut live) {
*entry = IcEntry::EMPTY;
cleared += 1;
}
}
for (_, key, entry) in &mut self.mega {
if entry.is_filled()
&& !records(IcKind::Get, *entry)
.chain(key_atom(*key))
.all(&mut live)
{
*entry = IcEntry::EMPTY;
cleared += 1;
}
}
cleared
}
}
fn key_atom(key: u32) -> Option<Record> {
match crate::heap::atom::PropertyKey::from_raw(key).kind() {
crate::heap::atom::KeyKind::String(id) => Some(Record::Atom(id.index())),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Record {
Shape(u32),
Object(u32),
Cell(u32),
Atom(u32),
}
fn records(kind: IcKind, entry: IcEntry) -> impl Iterator<Item = Record> {
let mut found = [None; 3];
match kind {
IcKind::Get if entry.w0 == LENGTH_TAG => {}
IcKind::Get => {
if entry.w0 != STRING_RECEIVER_TAG {
found[0] = Some(Record::Shape(entry.w0));
}
if entry.w1 != NONE {
found[1] = Some(Record::Object(entry.w1));
}
if entry.w3 != NONE && entry.w3 != STATIC_CELL.index() {
found[2] = Some(Record::Cell(entry.w3));
}
}
IcKind::Set => {
found[0] = Some(Record::Shape(entry.w0));
found[1] = Some(Record::Shape(entry.w1));
if entry.w3 != NONE && entry.w3 != STATIC_CELL.index() {
found[2] = Some(Record::Cell(entry.w3));
}
}
IcKind::Global => {}
IcKind::Construct => found[0] = Some(Record::Shape(entry.w0)),
}
found.into_iter().flatten()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::CodeBuilder;
use crate::bytecode::builder::UnitLayout;
fn unit(sites: u32) -> CodeUnit {
let mut builder = CodeBuilder::new();
builder.a(Opcode::Return, 0);
let mut unit = builder.finish(UnitLayout {
register_count: 1,
..UnitLayout::default()
});
unit.ic_count = sites;
unit
}
fn reserve(store: &mut IcStore, sites: u32) -> CodeUnit {
let mut unit = unit(sites);
store.reserve(&mut unit);
unit
}
#[test]
fn freed_ranges_merge_and_split() {
let mut store = IcStore::default();
let [a, b, c, d] = [4, 6, 3, 5].map(|sites| reserve(&mut store, sites));
assert_eq!([a.ic_base, b.ic_base, c.ic_base, d.ic_base], [0, 4, 10, 13]);
store.release(&b);
store.release(&a);
assert_eq!(store.free, [(0, 10)], "adjacent ranges merge");
store.release(&c);
assert_eq!(store.free, [(0, 13)]);
assert_eq!(store.free_len(), 13);
let e = reserve(&mut store, 8);
assert_eq!(e.ic_base, 0);
assert_eq!(store.free, [(8, 5)]);
let f = reserve(&mut store, 9);
assert_eq!(f.ic_base, 18);
store.release(&f);
let g = reserve(&mut store, 12);
assert_eq!(g.ic_base, 18);
assert_eq!(store.len(), 30);
store.verify_free();
}
#[test]
fn a_freed_unit_empties_its_entries_and_rows() {
let mut store = IcStore::default();
let a = reserve(&mut store, 2);
store.kinds[0] = IcKind::Get;
let fill = |shape: u32| IcEntry {
w0: shape,
w1: NONE,
w2: 0,
w3: STATIC_CELL.index(),
};
assert!(store.record(0, fill(1)));
assert!(store.record(0, fill(2)));
assert!(store.poly_row(0).is_some());
assert!(store.record(1, fill(3)));
store.release(&a);
assert!(store.entries[..2].iter().all(IcEntry::is_empty));
store.verify_free();
let b = reserve(&mut store, 1);
assert_eq!(b.ic_base, 0);
assert!(store.record(0, fill(4)));
assert!(store.record(0, fill(5)));
assert_eq!(store.entries[0].w1, 0, "the row is reused");
assert_eq!(store.poly.len(), 1);
}
}