Repository navigation
Scaling families for the program dimensions where verification cost is non-linear - #16
Open
JonasAlaif wants to merge 3 commits into
Open
JonasAlaif wants to merge 3 commits into
JonasAlaif wants to merge 3 commits into
Conversation
…s non-linear The scaling suite covered ten dimensions. Add fourteen generated families for the dimensions the earlier performance work found non-linear: struct and tuple width, fieldless enum variants, enum-typed fields, Option nesting, n-way match arms, sequential if/match decisions, sequential reborrows, conditional moves, generic round-trips, trait impls, units per file, straight-line method length, and (Viper-only) locations that alias only under the path condition. One knob per family; sources are spec-less Rust encoded with Prusti (rewrite-2023), and every point verifies on main. The existing loops family's known failures (write through &mut in a loop) are recorded in expected_failures.txt. perf_regression now also gates two small points of each family whose counters grow non-linearly (pcalias, enum_tag, option_depth, struct_width, tuple_width), baselines in benchmarks/baseline/scaling/. Written with Claude
…ints Silicon rejects m_run in all 8 loops points at the same postcondition exhale, so the entries are a shared Prusti-encoding limitation, not main's incompleteness. Written with Claude
The-Ray-Man
force-pushed
the
scaling-families
branch
from
October 13, 2026 14:26
3bbd95a to
27a8d73
Compare
The-Ray-Man
marked this pull request as ready for review
October 13, 2026 14:27
Prusti version: 0.3.0, commit ae6032113e3 2026-10-06 13:49:33 UTC
The-Ray-Man
force-pushed
the
scaling-families
branch
from
October 13, 2026 14:34
27a8d73 to
9f361b1
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The scaling suite measured cost as a function of ten program dimensions, all of
them close to linear on
main. The earlier performance work found many moredimensions along which Helium's cost grows faster than the program: enum width,
struct and tuple width, nesting, match width, decision chains, reborrows, aliasing
under the path condition, units per file, method length. None of them had a
generated family, so no later change could show a scaling exponent before and
after, and the perf gate saw no non-linear shape at all. This PR adds a family for
each such dimension, records
main's exponents on every family (old and new) asthe baseline, and puts small points of the families whose deterministic counters
grow non-linearly under the
perf_regressionband.Branch
scaling-families, baseorigin/main(13091f3). No verifier code changes.What is added
14 families in
benchmarks/scaling/(generatorsgen_{enums,match,width,borrows,generic,size,pcalias}.py,suite.jsonentries,src/+ committed Prustivpr/). One knob each, everything else fixed;sources follow
genlib.py's rules (spec-less, no recursion / returned references / arrays / panics).struct_width(struct_w{W})sw(n^3.39 at the time)tuple_width(tuple_w{W})tw(n^3.4-3.7)enum_tag(enum_tag_v{N})tagmatch +fromint match)p_E_snap; itertools, rustls)enum_fields(enum_fields_n{N})es/seoption_depth(option_d{D})Optionnesting peeled by nested matchesomnway(nway_k{K})match, one joinCipherSuite::from), guard-conjunction depth, disproven equalities on the scrutineeseqmatch(seqmatch_k{K})md(reach DNF; correlated branches)reborrow(reborrow_n{N})sbcondmove(condmove_k{K})generic(generic_k{K})make_generic/make_concrete, unicode-ident)impls(impls_k{K})units(units_k{K})straight(straight_k{K})pcalias(pcalias_k{K}, Viper-only)xonly under a branch; K exhalesAlready covered on
mainand not duplicated: payload depth and payload-enum width(
rust/enum_v{N}_p{D}), independent sequential branches (seq_if), struct depth,call depth/fan-out, argument count, block length (
rust/depth,arith,mut_writes).Not given a family: CFG block count and file size (linear / small constants on the
old branch), disproven-equality pairs (exercised by
nway/enum_tag; the syn/ryushapes were not reproduced in spec-less Rust).
Also:
benchmarks/scaling/expected_failures.txtrecords the pre-existingloopsfamily'sfailures (all 8 points,
m_run: write through&mutin a loop, the shape ofloops/mutate_in_loop::m_bump_n). Silicon rejects all 8 at the same postcondition, so they are alimitation of the encoding, not of main. Every new point verifies on main.
Commit b5840a1 on this branch updates the
loopscomment inexpected_failures.txtto say so.Design
b51f8e0, newer than the one behind main's existing encodings) encodes
&Twith aread()fraction, andp_Ref_immutable_snapthen fails on main ("insufficientpermission" at
p_Param_snap#requires) in every file that mentions&T.Silicon verifies these files: it reads permission amounts in function preconditions as
wildcards, while main checks the exact
writeagainst theread()the predicate holds, sothis is an incompleteness of main (recorded in
design/rebuild/gaps.md). It is constant perfile and orthogonal to every axis, so reads go through owned values or
&mutinstead andevery family verifies clean. (A gap to know about before re-encoding
the older suites with this Prusti.)
branch,
let r = if c { &mut a } else { &mut b }) fails on main already at one step(
p_Ref_mutable_snap#requires), and Silicon rejects it at the same place (precondition ofp_Ref_mutable_snap): the encoding, not main, loses the joined borrow's permission. Aconditional move of one local verifies on both and still makes its ownership path-dependent
K times over.
x.finK fractions under a branch equating
xwith K references. Writingx.finstead fails onmain at K=2 (assign does not sum pc-alias partners; it also fails with
x == y0 && x == y1in the precondition), as does reading
x.fand relating it toy1.f; Silicon verifies both(probes in
scaling_baseline/probes/pa2.vpr), so both are incompletenesses of main (indesign/rebuild/gaps.md). The exhale form the family uses verifies on both.were dropped (listed in the baseline README). New encodings: ~21 MB on disk, ~1.1 MB gzipped
(the existing 74 are ~0.55 MB gzipped); the largest file is 1.35 MB (
units_k64).Baseline: main's exponents (the "before" column)
Full table, raw data and exact commands:
design/rebuild/scaling_baseline/(
table.md,points.csv,scaling.json,rust_families.json,cputime.json,README.md).Binary: main 13091f3, release, codegen-units=1, sha256
d47f387c…9d36b. Time and CPU are netof the 0.201 s per-process floor (egg's clock calibration); e = log-log slope over all points,
tail = slope between the two largest points. CPU = min of 3 interleaved runs (wall medians
were taken under load up to 40 and agree with CPU within noise). Counter exponents are over
all points. All points verify unless noted.
loopsis a gap family: all 8 points failm_run(expected_failures.txt), so its cost isthe cost of failing early.
Readings:
pcalias(every counter x3 per extra chunk; K=10 takes 76 s and fails,K=12 >400 s).
enum_tag,tuple_width,arith,option_depth(rule applications D^2.4),struct_width(nodes W^1.4,CPU tail 3.2),
struct_depth.graph, which no aggregate counter on main measures):
straight,seqmatch,units,impls,reborrow,enum_fields, and the tails ofnway,generic,seq_if.condmove,call_depth,args_*,call_fanout,rust/depthstatements.straight_k256fails (peak 100,096 nodes: the node backstop is hit)and
pcalias_k10fails; neither is in the suite, both are recorded in the baseline README.perf_regression: small points of the non-linear families
tests/perf_regression.rsnow also gatesSCALING_POINTS: two points (one knob step apart)of each family whose counters grow non-linearly:
pcalias_k4/k5,enum_tag_v8/v16,option_d2/d4,struct_w4/w8,tuple_w2/w4; baselines inbenchmarks/baseline/scaling/.Same band (1.5x growth / 2x shrink, slack 10). With the larger point one step up, a growth
rate that steepens by more than ~log2(1.5) ≈ 0.6 in the exponent trips the larger point even if
the smaller one stays in band. Debug cost: the test goes from ~1.7 s to 6.4 s locally.
Not gated: families whose counters are linear (their non-linearity is per-operation cost, which
the band cannot see) and points that cost seconds in debug (
arith,rust/enum).A feature that fixes one of these dimensions will (intentionally) trip the 2x-shrink side and
must refresh those baselines with
UPDATE_PERF_BASELINE=1.Note:
UPDATE_PERF_BASELINE=1with main's code also rewrites three existing flat baselines(
division_precond,precond_cascade,spec_ladder_post: small in-band drifts, e.g.precond_cascadesat_iterations41→36); they are left untouched here.Gates (run on this branch)
cargo fmt --all --check,cargo clippy --locked --all-targets -- -D warnings: clean.cargo test --locked --lib --bins: 358 passed;--test suite: 5 passed;--test perf_regression: passed.benchmarks/rust/check.sh: 4464 verified, 0 known-failing, 0 regressed, 0 stale.benchmarks/panic_free/check.sh: 55 as expected, 10 incomplete, 0 unsound.bench check-suites: 0 errors (4 pre-existing mtime warnings).Silicon
Checked 2026-10-07 with
silicon_cli.pyfrom the crates repo (memguard, Silicon 1.1-SNAPSHOT6b540aa4, Z3 4.16.0), one JVM at a time. "main" means release
verifybuilt from this branch(no verifier code differs from main 13091f3). A canary file with a partial-permission write
and a false assert was rejected by Silicon with both errors, so the runs do report failures.
Encodings of the Rust probes: Prusti rewrite-2023 b51f8e0, same as the new families.
Probe sources, encodings, Silicon output:
design/rebuild/scaling_baseline/probes/(
silicon_2026-10-07/).loops_n{1..4}_b{5,20}(8 files, pre-existing family)m_runm_run#ensures, insufficient permissionm_run, insufficient permission top_Param(..)&T(probeshr.rs:fn get(s: &S),fn deref_int(x: &i32))p_Ref_immutable_snapp_Param_snap#requires@ 1/1, insufficient permissioncb2.rs)m_joined,m_joined_no_use_afterp_Ref_mutable_snap#requiresp_Ref_mutable_snap, insufficient permission top_Ref_mutable(_3p, ..)m_branch_local,m_cond_movecm.rs)m_remove,m_pairspa2.vpr)wr_direct(x == y0 && x == y1in the precondition,x.f := 1)pa2.vpr)read2(v := x.f; assert v == y1.funderx == y0 && x == y1)pa2.vpr)exh2,exhfullNo member verifies on main that Silicon rejects.
Why
&Tfails on main:p_Ref_immutableholdsacc(p_Param(addr, U), read()), and the body ofthe function
p_Ref_immutable_snapcallsp_Param_snap, whose precondition asks forwrite.Silicon treats permission amounts in function preconditions as wildcards (it warns "Function
contains specific permission amount that will be treated like wildcard"), so any positive
amount suffices; main checks the exact amount inside function bodies. The gap is in main, not
in the encoding, and it hits every program that mentions
&Tonce encoded by this Prusti.The new families are well-formed: Silicon verifies, with no error, the smallest point of every
new family (
condmove_k2,enum_fields_n2,enum_tag_v8,generic_k2,impls_k2,nway_k4,option_d2,reborrow_n4,seqmatch_k2,straight_k16,struct_w4,tuple_w2,units_k4)and
pcalias_k3,pcalias_k8; main verifies all of them too.Accounting
From the old branch: the probe shapes of eac77b1 (sw, tw, sb, om, es, se, md) as the starting
point for the new generators (re-derived and regenerated, not copied); the synthetic
members/preds probes (work log 2026-08-20) behind
units; the fieldless enum ladder (work log2026-08-25) behind
enum_tag. No old-branch code. The old probe families that measured linear(bc, cd, fc, gw, ne, sd) are not re-added.