Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e969427
perf: cache owning Uint32Array admissions
Aug 26, 2026
15d7673
perf: fast-path Array subclass length misses
Aug 26, 2026
bbe2b4c
perf(array): accumulated ECS optimization work through v74
Aug 27, 2026
12d4cba
perf(codegen): follow one growth-forwarding edge in the guarded array…
Aug 27, 2026
6de5299
perf(array): gate the raw-f64 downgrade note inline; typed-array pre-…
Aug 27, 2026
45be16c
perf(codegen): exact inline typeof-number compare; header-branded typ…
Aug 27, 2026
ec3b4d8
perf(array): route object receivers to the subclass fast read before …
Aug 27, 2026
4bf2c84
perf(codegen): give integer-valued dynamic keys the inline numeric re…
Aug 27, 2026
0c00774
test(codegen): update the proven-number strict-eq rooting test to the…
Aug 27, 2026
7898bf9
Merge origin/main into codex/array-subclass-tail-mutation; land CI gates
Aug 27, 2026
c41343c
ci: ratchet baselines for the file splits, census gate for cache carr…
Aug 27, 2026
9a09adf
Merge origin/main (#8878 batch) into codex/array-subclass-tail-mutation
Aug 27, 2026
a34c580
perf(runtime): array-read fallback serves object-backed Array subclas…
Aug 27, 2026
08b7e02
runtime(array): keyed index paths reload the receiver via across_* (r…
Aug 27, 2026
1dd37f9
review: keep the fused u31 push non-reentrant, bound ECS columns, lif…
Aug 27, 2026
f2e2c8d
perf(shapes): drop SipHash from ids_by_facts, the last one on the sha…
Aug 27, 2026
2c06e3f
changelog: add fragment for the ids_by_facts hasher change
Aug 27, 2026
6665d1e
Merge branch 'p8884' into HEAD
Aug 27, 2026
f4ec10f
Merge branch 'p8876' into HEAD
Aug 27, 2026
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
20 changes: 20 additions & 0 deletions changelog.d/8876-ecs-forwarded-store-inline-tiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Array and ECS performance. Guarded property-receiver element stores follow one
growth-forwarding edge inline (the read tier already did), so a field that
kept a pre-grow forwarding stub (`this.ents[id] = arch`) no longer pays the
out-of-line extend helper and allocator resolver on every store; the raw-f64
downgrade note is gated on the header word already loaded. `typeof x ===
"number"` decides the definitely-Number cases inline, deferring INT32/class
refs, raw typed-array pointers and the Web Streams id band to the classifier.
Inline dynamic typed-array reads brand off the `GC_TYPE_TYPED_ARRAY` header
instead of the evictable 64-slot kind cache. `js_array_get_f64` and the
typed-feedback array-read fallback route object-backed Array-subclass
receivers to their dense fast read before the tracked resolver and the by-name
key path, and an `Any`-typed key that is an integer array index takes the
inline numeric read tiers (`a[b[i]]`). Object-backed Array subclasses keep
validated prototype-override reads, and compact guarded specializations are
pre-statepoint inlined by lowered IR size.

wolf-ecs (noctjs/ecs-benchmark) on the Mac mini reference box, versus the
previous retained build: add/remove -18.5% (0.556 → 0.453 ms/op), entity-cycle
-23.1% (0.499 → 0.384 ms/op); each step 11/11 paired wins, semantics probes
byte-identical to Node.
20 changes: 20 additions & 0 deletions changelog.d/8882-shape-facts-hasher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Dropped SipHash from `ids_by_facts`, the last shape-table map still using it.

Profiling `claude -p` put `RandomState::hash_one` at 17 self-samples inside
`shapes::` alone (57 across the process) — pure hashing overhead on a lookup
that runs on every descriptor install and retire.

Its sibling maps already moved off SipHash (#8125). The standing comment on
this one argued only against `PtrHasher`, whose `write_*` methods OVERWRITE the
accumulator — right for a single-word key, wrong for this five-field one, which
would collapse to its last field and collide every descriptor sharing it. That
objection does not apply to `FastKeyHasher`: it implements only `write`, so the
derived `Hash`'s `write_u32` / `write_u64` calls all forward there and FOLD with
FNV-1a, reaching every field.

The key is internal shape state, never program input, so DoS-resistant hashing
buys nothing — the same rationale already applied to the descriptor side tables.

The new test pins the folding property directly: vary one field at a time and
require a distinct hash each time. Sabotage-checked against `PtrHasher`, where
it fails with "changing `keys` alone must change the hash".
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,8 @@ pub(super) fn compile_closure(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
func_names,
strings,
loop_targets: Vec::new(),
Expand Down Expand Up @@ -1211,6 +1213,7 @@ pub(super) fn compile_closure(
was_unrolled: false,
ic_site_counter: ic_base,
ic_globals: Vec::new(),
property_get_ic_override: None,
typed_parse_rodata: Vec::new(),
buffer_data_slots: HashMap::new(),
buffer_view_slots: HashMap::new(),
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,8 @@ pub(super) fn compile_module_entry(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
func_names,
strings,
loop_targets: Vec::new(),
Expand Down Expand Up @@ -982,6 +984,7 @@ pub(super) fn compile_module_entry(
was_unrolled: hir.init_was_unrolled,
ic_site_counter: ic_base,
ic_globals: Vec::new(),
property_get_ic_override: None,
typed_parse_rodata: Vec::new(),
buffer_data_slots: HashMap::new(),
buffer_view_slots: HashMap::new(),
Expand Down Expand Up @@ -1490,6 +1493,8 @@ pub(super) fn compile_module_entry(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
func_names,
strings,
loop_targets: Vec::new(),
Expand Down Expand Up @@ -1694,6 +1699,7 @@ pub(super) fn compile_module_entry(
was_unrolled: hir.init_was_unrolled,
ic_site_counter: ic_base,
ic_globals: Vec::new(),
property_get_ic_override: None,
typed_parse_rodata: Vec::new(),
buffer_data_slots: HashMap::new(),
buffer_view_slots: HashMap::new(),
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,8 @@ pub(super) fn compile_function(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
func_names,
strings,
loop_targets: Vec::new(),
Expand Down Expand Up @@ -1232,6 +1234,7 @@ pub(super) fn compile_function(
was_unrolled: f.was_unrolled,
ic_site_counter: ic_base,
ic_globals: Vec::new(),
property_get_ic_override: None,
typed_parse_rodata: Vec::new(),
buffer_data_slots: HashMap::new(),
buffer_view_slots: HashMap::new(),
Expand Down
176 changes: 176 additions & 0 deletions crates/perry-codegen/src/codegen/guarded_falsy_default_method_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
//! Guarded omitted-argument/false-field indexed method versioning.

use crate::{compile_module, CompileOptions};
use perry_hir::types::Type;
use perry_hir::{Class, ClassField, CompareOp, Expr, Function, Module, Param, Stmt};

const ROWS: u32 = 30;
const INDEX: u32 = 31;
const DEFER: u32 = 32;

fn param(id: u32, name: &str) -> Param {
Param {
id,
name: name.to_string(),
ty: Type::Any,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
}
}

fn default_get() -> Expr {
Expr::PropertyGet {
object: Box::new(Expr::This),
property: "DEFAULT_DEFER".to_string(),
byte_offset: 0,
}
}

fn candidate_method() -> Function {
let mut defer = param(DEFER, "defer");
defer.default = Some(default_get());
Function {
id: 40,
name: "update".to_string(),
type_params: Vec::new(),
params: vec![param(ROWS, "rows"), param(INDEX, "index"), defer],
return_type: Type::Any,
body: vec![
Stmt::If {
condition: Expr::Compare {
op: CompareOp::Eq,
left: Box::new(Expr::LocalGet(DEFER)),
right: Box::new(Expr::Undefined),
},
then_branch: vec![Stmt::Expr(Expr::LocalSet(DEFER, Box::new(default_get())))],
else_branch: None,
},
// Nominates the existing nonnegative-index family.
Stmt::Expr(Expr::IndexGet {
object: Box::new(Expr::LocalGet(ROWS)),
index: Box::new(Expr::LocalGet(INDEX)),
}),
Stmt::If {
condition: Expr::LocalGet(DEFER),
then_branch: vec![Stmt::Return(Some(Expr::Integer(1)))],
else_branch: Some(vec![Stmt::Return(Some(Expr::Integer(2)))]),
},
],
is_async: false,
is_generator: false,
is_strict: true,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
was_plain_async: false,
was_unrolled: false,
}
}

fn fixture(method: Function) -> Module {
let class = Class {
id: 41,
name: "Store".to_string(),
type_params: Vec::new(),
extends: None,
extends_name: None,
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
fields: vec![ClassField {
name: "DEFAULT_DEFER".to_string(),
key_expr: None,
ty: Type::Any,
init: Some(Expr::Bool(false)),
is_private: false,
is_readonly: false,
decorators: Vec::new(),
}],
constructor: None,
methods: vec![method],
getters: Vec::new(),
setters: Vec::new(),
static_accessor_names: Vec::new(),
static_accessor_fn_ids: Vec::new(),
computed_members: Vec::new(),
static_fields: Vec::new(),
static_methods: Vec::new(),
decorators: Vec::new(),
is_exported: false,
aliases: Vec::new(),
is_nested: false,
alloc_width_hint: 0,
specialized_from: None,
};
let mut module = Module::new("guarded_falsy_default_method.ts");
module.classes = vec![class];
module
}

fn emit(method: Function) -> String {
let opts = CompileOptions {
emit_ir_only: true,
output_type: "executable".to_string(),
..Default::default()
};
String::from_utf8(compile_module(&fixture(method), opts).expect("fixture compiles"))
.expect("LLVM IR is UTF-8")
}

fn function_body<'a>(ir: &'a str, marker: &str) -> &'a str {
let start = ir
.match_indices("define ")
.find(|(index, _)| {
let end = ir[*index..]
.find('\n')
.map(|offset| index + offset)
.unwrap_or(ir.len());
ir[*index..end].contains(marker)
})
.map(|(index, _)| index)
.unwrap_or_else(|| panic!("missing function containing {marker}:\n{ir}"));
let end = ir[start..]
.find("\n}")
.map(|offset| start + offset)
.expect("function terminator");
&ir[start..end]
}

#[test]
fn wrapper_proves_live_false_field_and_clone_erases_default_and_branch() {
let ir = emit(candidate_method());
let base = "perry_method_guarded_falsy_default_method_ts__Store__update";
let index = format!("{base}$idx_u31_{INDEX}");
let specialized = format!("{index}$default_false2");
let wrapper = function_body(&ir, &format!("@{base}("));
let ordinary = function_body(&ir, &format!("@{index}("));
let false_default = function_body(&ir, &format!("@{specialized}("));

assert!(wrapper.contains(&crate::nanbox::TAG_UNDEFINED_I64.to_string()));
assert!(wrapper.contains(&crate::nanbox::TAG_FALSE_I64.to_string()));
assert!(wrapper.contains("load i32, ptr @perry_class_shape_id_"));
assert!(wrapper.contains(&format!("@{specialized}(")));
assert!(wrapper.contains(&format!("@{index}(")));
assert!(ordinary.contains("@js_is_truthy("), "{ordinary}");
assert!(
!false_default.contains("@js_is_truthy("),
"the guarded clone retained the known-false condition:\n{false_default}"
);
assert!(
!false_default.contains("class_field_get"),
"the guarded clone reevaluated its already-proved default field:\n{false_default}"
);
}

#[test]
fn arbitrary_parameter_use_rejects_the_clone() {
let mut method = candidate_method();
method.body.push(Stmt::Return(Some(Expr::LocalGet(DEFER))));
let ir = emit(method);
assert!(
!ir.contains("$default_false"),
"a parameter whose actual false value remains observable was specialized"
);
}
31 changes: 31 additions & 0 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,20 @@ pub(super) fn apply_pshape_inline_policy(
}
}

/// Maximum pre-optimization LLVM IR body size admitted to the native-roots
/// pre-statepoint inliner for a guarded specialization.
///
/// HIR statement count is deliberately not used here: one source statement
/// can lower to a large property/index dispatch lattice. Sixteen KiB admits
/// compact exact-receiver and nonnegative-index leaves while rejecting bodies
/// such as mutation-heavy ECS transitions by nearly an order of magnitude.
pub(super) const GUARDED_SPECIALIZATION_PREINLINE_MAX_IR_BYTES: usize = 16 * 1024;

#[inline]
pub(super) fn guarded_specialization_fits_preinline_budget(ir_bytes: usize) -> bool {
ir_bytes <= GUARDED_SPECIALIZATION_PREINLINE_MAX_IR_BYTES
}

/// Maximum total (module-wide) direct call sites a function may have and still
/// be hinted. This is the anti-bloat backstop: the raised `-inlinehint-threshold`
/// lifts LLVM's ceiling for a hinted callee at *every* one of its call sites, so
Expand Down Expand Up @@ -1470,6 +1484,23 @@ mod sanitize_tests {
}
}

#[cfg(test)]
mod guarded_specialization_preinline_tests {
use super::{
guarded_specialization_fits_preinline_budget, GUARDED_SPECIALIZATION_PREINLINE_MAX_IR_BYTES,
};

#[test]
fn generated_ir_budget_is_inclusive_and_bounded() {
assert!(guarded_specialization_fits_preinline_budget(
GUARDED_SPECIALIZATION_PREINLINE_MAX_IR_BYTES
));
assert!(!guarded_specialization_fits_preinline_budget(
GUARDED_SPECIALIZATION_PREINLINE_MAX_IR_BYTES + 1
));
}
}

#[cfg(test)]
mod resolve_target_triple_tests {
use super::resolve_target_triple;
Expand Down
Loading
Loading