Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c70d8f4
chore: trace snapshot inputs in instrumented builds
Nov 6, 2026
44b66df
chore: verify snapshot boundaries and census instruction accounting
Nov 6, 2026
81a8c7f
chore: retain input coverage and replay counterexamples
Nov 6, 2026
07320d1
chore: preserve mapped-file input observations
Nov 6, 2026
d95b3fa
Merge remote-tracking branch 'origin/main' into snap22
Nov 6, 2026
ea632bc
fix: preserve production bytes around census crypto hooks
Nov 7, 2026
8da58ff
chore: retain native census shape publication diagnostics
Nov 7, 2026
2d10475
fix: stream census inputs without allocator re-entry
Nov 7, 2026
377449f
fix: restore census page policy after first-write tracking
Nov 7, 2026
3c391bf
fix: share the census writable bulk transition
Nov 7, 2026
a44cd98
feat: census funnels, per-read argv sites, clock inputs and pc checks…
Nov 7, 2026
29b6af1
Merge remote-tracking branch 'origin/main' into snapk2/census-prefix
Nov 9, 2026
34f82db
snap census K2a (WIP): host-value reads and scan, P-entry (a)(b)(c), …
Nov 9, 2026
aca3cc7
snap census K2a: fixture boundaries, SIGTRAP re-protect order, P-entr…
Nov 9, 2026
5e612dc
Merge remote-tracking branch 'origin/main' into snapk2/census-prefix
Nov 9, 2026
b690144
SNAPK2b WIP: compare_literal site, stdout_style_bit funnel, open-fram…
Nov 9, 2026
957e9da
fix(util): styleText colours by node getColorDepth (FORCE_COLOR="" co…
Nov 9, 2026
5b598e9
Merge branch fix/style-color-depth into snapk2b/census-sites
Nov 9, 2026
4afa548
snap census: K2b fixtures and checker; compare_literal compares strin…
Nov 9, 2026
3aec8ff
snap census: compare_literal hook from the compare lowering itself
Nov 9, 2026
428917a
Merge GitHub main into snapk2b/census-sites
Nov 9, 2026
71710bc
snap census: compare hook test reads the call line, not the declaration
Nov 9, 2026
5e114c7
snap census: env/argv read sites classify against their own parent
Nov 9, 2026
51ae33d
Merge main 9093179838 into snapk2b/census-sites
Nov 9, 2026
4aa757b
snap census: the walker lock opens an unwind funnel; a native read in…
Nov 10, 2026
a3e210e
snap census: a job_queue row names each pending job (kind, callback c…
Nov 10, 2026
ef3056c
Merge branch 'main' of https://github.com/PerryTS/perry into snap/cen…
Nov 11, 2026
49656e8
changelog: snap census fragments (env/argv sites, walker lock, pendin…
Nov 11, 2026
ab53399
changelog: number the PR fragments
Nov 11, 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
1 change: 1 addition & 0 deletions changelog.d/13935-snap-census-protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix instrumented startup census page protection after the first-write watcher ends in the weights run; keep bulk and late-root protection consistent with the page policy.
1 change: 1 addition & 0 deletions changelog.d/14449-snap-census-env-argv-sites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
snap census: process.env/process.argv read sites classify themselves against their own parent expression, not address-keyed site lists, so a later module can no longer read a stale site kind; this also covers literal operands on the scalar-replacement path of let.
1 change: 1 addition & 0 deletions changelog.d/14449-snap-census-pending-jobs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
snap census: a job_queue row names each pending job (task kind, queueing init and callback code), so the census says which job is still pending at every later boundary; instrumented runtime only.
1 change: 1 addition & 0 deletions changelog.d/14449-snap-census-walker-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
snap census: the walker lock opens the census unwind funnel, so a native event raised while a throw holds the unwinder lock no longer walks the stack through the same lock; the instrumented Supabase census run no longer hangs at the ws buffer-util throw.
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,7 @@ pub(super) fn compile_closure(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
snap_lowering: Default::default(),
literal_chunk_fill: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ pub(super) fn compile_module_entry(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
snap_lowering: Default::default(),
literal_chunk_fill: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/entry/module_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ pub(super) fn compile_module_init(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
snap_lowering: Default::default(),
literal_chunk_fill: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,7 @@ pub(super) fn compile_function(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
snap_lowering: Default::default(),
literal_chunk_fill: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,7 @@ pub(super) fn compile_method(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
snap_lowering: Default::default(),
literal_chunk_fill: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/method_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ pub(in crate::codegen) fn compile_static_method(
current_block: 0,
discard_expr_value: false,
discard_this_expr: false,
snap_lowering: Default::default(),
literal_chunk_fill: false,
truthy_call_result_requested: false,
pending_truthy_call_result: None,
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/expr/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
};
let l = operand_value(ctx, &left_literal);
let r = operand_value(ctx, &right_literal);
// SNAPK2b census (instrumented compiles only): the hook reads
// the two values this compare reads, here, where they are
// live. A numeric-literal operand is never a string, so that
// compare keeps no row and emits none.
if left_literal.is_none() && right_literal.is_none() {
super::snap_compare_site::emit(ctx, *op, left, right, &l, &r);
}
// Value records' check mode (DESIGN-p4l4 L4).
crate::expr::record_witness::compare(ctx, op, left, right, &l, &r);
// BigInt comparison fast path: NaN-tagged BIGINT_TAG values
Expand Down
10 changes: 6 additions & 4 deletions crates/perry-codegen/src/expr/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// What the expression does to the receiver run in flight.
let run = crate::expr::layout_read::run_transparency(expr);
crate::expr::layout_read::run_enter(ctx, &run);
let value = lower_expr_inner(ctx, expr)?;
// SNAP census frame: `expr` is the current node for the extent of its
// own lowering, restored on every return (`expr/snap_env_site.rs`).
let snap_frame = super::snap_env_site::enter(ctx, expr);
let value = lower_expr_inner(ctx, expr);
ctx.snap_lowering = snap_frame;
let value = value?;
crate::expr::layout_read::run_exit(ctx, &run);
if let Some(desc) = crate::layout_desc::array_families::birth_layout(expr) {
Ok(ctx.block().call(
Expand All @@ -51,9 +56,6 @@ fn lower_expr_inner(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// one. Handlers that care receive it as an argument, because they consult
// it after lowering their operands — by which point the field is gone.
let value_discarded = std::mem::take(&mut ctx.discard_this_expr);
// SNAP census (instrumented compiles only): an env read whose value this
// expression stores is a store site (`expr/snap_env_site.rs`).
super::snap_env_site::note_store_parent(ctx, expr);
// #9856: under `--debug-symbols`, record this expression's source line
// for the DWARF line table (`crate::debug_info`). No-op otherwise.
crate::debug_info::mark_expr_location(ctx, expr);
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-codegen/src/expr/env_clones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// -------- process.pid / process.ppid — raw f64 number --------
Expr::ProcessPid => Ok(ctx.block().call(DOUBLE, "js_process_pid", &[])),
Expr::ProcessPpid => Ok(ctx.block().call(DOUBLE, "js_process_ppid", &[])),
Expr::ProcessArgv => Ok(ctx.block().call(DOUBLE, "js_process_argv_value", &[])),
Expr::ProcessArgv => {
super::snap_argv_site::emit(ctx, expr);
Ok(ctx.block().call(DOUBLE, "js_process_argv_value", &[]))
}

// -------- structuredClone(v[, options]) — real deep copy --------
Expr::StructuredClone { value, options } => {
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ pub(crate) mod hot_tls;
pub(crate) mod inline_birth;
pub(crate) mod literal_chunks;
mod literal_descriptor;
pub(crate) mod snap_argv_site;
pub(crate) mod snap_compare_site;
pub(crate) mod rest_elems;
pub(crate) mod rest_fields;
pub(crate) mod snap_env_site;
Expand Down Expand Up @@ -534,6 +536,11 @@ pub(crate) struct FnCtx<'a> {
/// reading the field, because they consult it *after* lowering their
/// operands, by which point the field has been taken again.
pub discard_this_expr: bool,
/// SNAP census frame: the two innermost expressions `lower_expr` is
/// lowering, saved and restored by each lowering. A `process.env` /
/// `process.argv` read takes its census kind from its own parent
/// (`snap_env_site`, `snap_argv_site`).
pub snap_lowering: snap_env_site::Lowering,
/// A chunked literal birth's own arguments are being lowered
/// (`literal_chunks`): a chunked argument is the birth's `undefined`
/// fill, and anywhere else it is an invariant violation.
Expand Down
47 changes: 47 additions & 0 deletions crates/perry-codegen/src/expr/snap_argv_site.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! SNAP25 census: the grain of a `process.argv` read, emitted in
//! instrumented compiles only (`PERRY_SNAP_CENSUS` set while compiling). An
//! ordinary compile emits nothing.
//!
//! `process.argv.length` is a length read (kind 1), `process.argv[<constant>]`
//! an element read (kind 2, the index is the call's second argument;
//! `process.execPath` is a metadata property, not an argv read), and every other use lets the
//! whole array escape (kind 0). Codegen calls `js_snap_argv_site(kind, index)`
//! right before `js_process_argv_value`; no table is emitted. The read takes
//! its grain from its own parent, the expression whose lowering is lowering
//! it (the census lowering frame, `snap_env_site::Lowering`).

use perry_hir::Expr;

use crate::types::{I32, I64};

use super::FnCtx;

/// The grain `parent` reads of its direct `process.argv` child `argv`.
fn grain(parent: &Expr, argv: &Expr) -> Option<(i32, i64)> {
match parent {
Expr::PropertyGet {
object, property, ..
} if std::ptr::eq(&**object, argv) && property == "length" => Some((1, 0)),
Expr::IndexGet { object, index, .. } if std::ptr::eq(&**object, argv) => match **index {
Expr::Number(n) if n >= 0.0 && n.fract() == 0.0 && n < 1e9 => Some((2, n as i64)),
// Literal indices that fit an integer lower as Expr::Integer.
Expr::Integer(n) if n >= 0 => Some((2, n)),
_ => None,
},
_ => None,
}
}

/// Emit the site hook for the `process.argv` read `expr`, right before its runtime call.
pub(crate) fn emit(ctx: &mut FnCtx<'_>, expr: &Expr) {
if !super::snap_env_site::census_compile() {
return;
}
let (kind, index) = super::snap_env_site::parent_of(ctx, expr)
.and_then(|parent| grain(parent, expr))
.unwrap_or((0, 0));
ctx.block().call_void(
"js_snap_argv_site",
&[(I32, &kind.to_string()), (I64, &index.to_string())],
);
}
60 changes: 60 additions & 0 deletions crates/perry-codegen/src/expr/snap_compare_site.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! SNAPK2b census: the `compare_literal` site kind (DESIGN27 §3), emitted in
//! instrumented compiles only (`PERRY_SNAP_CENSUS` set while compiling). An
//! ordinary compile emits nothing.
//!
//! The generic equality compare (`===`, `!==`, `==`, `!=` in
//! `expr::compare::lower`) calls [`emit`] with the two operand values it is
//! about to compare, so the hook reads exactly the values the compare reads,
//! in the function and block that compares them. `js_snap_compare_literal(op,
//! left, right, literal)` runs before the compare itself; `literal` has bit 0
//! (1) set when the left (right) operand is a string literal. The runtime keeps
//! a row only when one operand is a host value or env string and the other a
//! string: the guard then holds the predicate, not the host value.
//!
//! A compare a fast path lowers (`typeof x === "…"`, a numeric literal operand,
//! a typed or chained compare) never reaches [`emit`] and emits no row, so the
//! read stays a whole-value member. Nothing is kept between calls and no table
//! is emitted.

use perry_hir::{CompareOp, Expr};

use crate::types::{DOUBLE, I32};

use super::FnCtx;

/// Emit the census hook for the equality compare `left op right` whose
/// operands the caller has lowered to the JS values `l` and `r`.
pub(crate) fn emit(
ctx: &mut FnCtx<'_>,
op: CompareOp,
left: &Expr,
right: &Expr,
l: &str,
r: &str,
) {
let op = match op {
CompareOp::Eq => 0,
CompareOp::Ne => 1,
CompareOp::LooseEq => 2,
CompareOp::LooseNe => 3,
_ => return,
};
if !super::snap_env_site::census_compile() {
return;
}
let literal = i32::from(matches!(left, Expr::String(_)))
| (i32::from(matches!(right, Expr::String(_))) << 1);
ctx.block().call_void(
"js_snap_compare_literal",
&[
(I32, &op.to_string()),
(DOUBLE, l),
(DOUBLE, r),
(I32, &literal.to_string()),
],
);
}

#[cfg(test)]
#[path = "snap_compare_site_tests.rs"]
mod tests;
114 changes: 114 additions & 0 deletions crates/perry-codegen/src/expr/snap_compare_site_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! SNAPK2c: the `compare_literal` hook reads only its own compare's operand
//! values. 4afa54803f kept an address-keyed site list across functions: a
//! compare whose numeric-literal or `typeof` operand is lowered without
//! `lower_expr` left a half-filled entry behind, and a later module's
//! expression at the freed literal's address emitted the hook with the first
//! module's register (Supabase: precise_roots.rs panic in an unrelated closure).

use perry_hir::types::Type;
use perry_hir::{BinaryOp, CompareOp, Expr, Module, Stmt};

fn ir_of(m: &Module) -> String {
String::from_utf8(
crate::compile_module(
m,
crate::CompileOptions {
emit_ir_only: true,
is_entry_module: true,
target: Some("x86_64-unknown-linux-gnu".into()),
..Default::default()
},
)
.unwrap(),
)
.unwrap()
}

fn local(id: u32, ty: Type, init: Expr) -> Stmt {
Stmt::Let {
id,
name: format!("k2c_{id}"),
ty,
mutable: true,
init: Some(init),
}
}

fn cmp(op: CompareOp, left: Expr, right: Expr) -> Expr {
Expr::Compare {
op,
left: Box::new(left),
right: Box::new(right),
}
}

fn hooks(ir: &str) -> usize {
ir.matches("call void @js_snap_compare_literal(").count()
}

/// Module A: equality compares lowered by the numeric-literal and `typeof`
/// fast paths (no hook). Module B: no equality compare at all, built after A
/// is dropped so its nodes reuse A's freed addresses.
#[test]
fn compare_literal_hook_never_crosses_compares() {
std::env::set_var("PERRY_SNAP_CENSUS", "1");
for round in 0..8 {
let mut a = Module::new("k2c_a");
a.init.push(local(0, Type::Any, Expr::Undefined));
a.init.push(local(1, Type::Any, Expr::Undefined));
for i in 0..48u32 {
let lit = cmp(CompareOp::Eq, Expr::LocalGet(0), Expr::Number(1.0));
let ty = cmp(
CompareOp::Eq,
Expr::TypeOf(Box::new(Expr::LocalGet(1))),
Expr::String("string".into()),
);
a.init.push(local(10 + 2 * i, Type::Any, lit));
a.init.push(local(11 + 2 * i, Type::Any, ty));
}
let ir_a = ir_of(&a);
assert_eq!(hooks(&ir_a), 0, "fast-path compares emit no hook: {ir_a}");
drop(a);
let mut b = Module::new("k2c_b");
b.init.push(local(0, Type::Any, Expr::Undefined));
b.init.push(local(1, Type::Any, Expr::Undefined));
for i in 0..96u32 {
let sum = Expr::Binary {
op: BinaryOp::Add,
left: Box::new(Expr::LocalGet(0)),
right: Box::new(Expr::LocalGet(1)),
};
b.init.push(local(10 + i, Type::Any, sum));
}
let ir_b = ir_of(&b);
assert_eq!(
hooks(&ir_b),
0,
"round {round}: B has no equality compare: {ir_b}"
);
}
// The generic compare emits one hook over its own two operand values.
let mut c = Module::new("k2c_c");
c.init.push(local(0, Type::Any, Expr::Undefined));
c.init.push(local(
1,
Type::Any,
cmp(
CompareOp::Ne,
Expr::LocalGet(0),
Expr::String("linux".into()),
),
));
let ir_c = ir_of(&c);
std::env::remove_var("PERRY_SNAP_CENSUS");
assert_eq!(hooks(&ir_c), 1, "{ir_c}");
let call = ir_c
.lines()
.find(|l| l.contains("call void @js_snap_compare_literal("))
.unwrap();
assert!(call.contains("(i32 1, double "), "op !== : {call}");
assert!(
call.trim_end().ends_with("i32 2)"),
"right is the literal: {call}"
);
}
Loading
Loading