Skip to content

refactor(hexmap): shared primitives, cross-lot gas and CI time (P1) - #135

Merged
bal7hazar merged 10 commits into
mainfrom
refactor/hexmap-commons
Sep 27, 2026
Merged

bal7hazar merged 10 commits into
mainfrom
refactor/hexmap-commons

Conversation

@bal7hazar

@bal7hazar bal7hazar commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Lot P1 of origami_hexmap: the helpers each lot re-implemented locally move to the shared files, every module uses them, the remaining gas is taken where it was measurable, and the hexmap CI job drops from 8m23s to under 4m20s (3m43s to 4m16s on the last runs, the L8 facade tests included).

Shared primitives

  • helpers/bits.cairo: Bits::bitwise (AND, XOR and OR from one application of the builtin, wrapping the private corelib libfunc), Bits::and / or / xor per limb, Bits::popcount now the L7 two-limb byte-count form, Bits::popcount_small, Bits::byte_counts, Bits::top_byte / low_byte (bounded_int divisions), and the generic Set<T> of L3 (WideSet, SmallSet). The local extern fn bitwise of caver, bfs, dial and their benches are gone.
  • helpers/layout.cairo: Layout::expand / expand_small run the L1 dilation (limbs, bitwise triple, up/down from X = 2P - Pe with one product each) through the new 4-value Dilation that the layer loops carry; Layout::with_interior (3 shared lookups); Layout::edge_neighbours and Layout::neighbour_in, the edge-endpoint helpers of Bfs and Dial.
  • Caver::keep_component floods the frontier through BfsInternal::component (the flood of Bfs::reachable without the checks): -5.6 % cave, -10.6 % maze, -11.5 % serpentine.
  • helpers/rng.cairo: the pool refills below 2^64 (bias 2^-56 per pool instead of 2^-24, cost +0.04 % per carved tile); Rng::draw_byte, draw6, shuffle6 and split216 divide with core::internal::bounded_int::div_rem (1.10k vs 1.56k). The walker draws 6 triples from each limb of every permutation. Keeping a spare pool or a refill counter in Rng was measured: +2 % per tile in the recursive generators (one more field), not kept.

Gas (before = main at 7adb9c4, 256 seeds for the spreader)

Area Change
Spreader -2.7 % to -9.2 % (max over the seeds), -2.9 % to -9.8 % (mean) on every library benchmark; count 1 on dense boards about -9 %
Walker -2.3 % to -4.3 %
Mazer / Digger (the maps change with the stream) per carved tile over 8 seeds -1.5 % (order 0), -2.3 % (order 1); digger maze -2.0 % per tile
Caver generate 0 % to -5.6 %, keep_component -5.6 % to -11.5 %
Dial 0 % to -7.6 %
Bfs reachable -3.4 % to -4.8 %; search, distance, range unchanged
Primitives popcount -27 %, next_below -14 %, Layout::expand -3.6 % (17x14), BFS layer step -6 %

One library benchmark costs more: bench_mazer_17x14_order_0 +0.8 %, because the new stream carves 90 tiles instead of 89 (-0.3 % per tile). bench_rng_draw (generic u128 bound) +0.4 %: one more refill per 100 draws with the 2^64 threshold. GAS.md (new section P1) has the before/after of every library benchmark, the measured losers and the bounds; 77 budgets are lowered.

L8 facade (merged while this PR was open)

The branch is rebased on eb0f4b1. map.cairo calls BfsInternal::and, expand and expand_small: they remain as one-line delegations to Bits::and and Dilation, so the facade is untouched. Its benchmarks move with the libraries: compute_distribution -6.6 %, scenarios -2 % to -5 %, new_maze +0.8 % (the 90-tile maze); budgets in tests/bench_map.cairo are left to L8.

Visibility

The public traits keep their documented functions: Mazer::generate, Digger::maze / corridor, Spreader::generate, Bfs::*, Dial::*, Caver::*, Walker::generate. The recursion, dig and selection helpers move to pub(crate) internal impls; BfsInternal, Back, Endpoint, the stores and goals, Carver, Heading, TurnTrait, BitSetTrait and Counts become pub(crate). The BfsInternal functions that misbehave with height < 3 or width >= 128 can no longer be reached from outside the crate.

CI

The pull-request job runs snforge test --package origami_hexmap --fuzzer-runs 64: the library fuzz benchmarks keep their budgets (max over 256 seeds) and check them on 64 seeds; the 100 spreader losers are #[ignore]d. A new non-blocking job, hexmap-full, runs everything with 256 seeds on pushes to main (snforge test --package origami_hexmap --include-ignored). Locally, with a fresh build: 7m55s -> 2m46s.

Expectations regenerated

The random stream changed (refill threshold, walker pools), so the exact grids of test_mazer_generate_17x14_order_0/1, test_mazer_generate_19x13, test_digger_maze_order_0, test_walker_generate_17x14/7x7/19x13 and bench_walker_17x14_504 are regenerated (printouts included). The walker's scalar reference follows the new stream. No statistical or property test was loosened.

🤖 Generated with Claude Code

bal7hazar and others added 9 commits September 27, 2026 20:44
… Bits

Bits::bitwise wraps the private corelib libfunc (AND, XOR and OR in one
application), Bits::and/or/xor apply it per limb. The local declarations of
caver, bfs, dial and their benches are removed. Bits::popcount now sums the
byte counts of both limbs (L7 winner, 15.6k vs 19.0k per op), Bits::popcount_small
and Bits::byte_counts serve the spreader. The first SWAR popcount stays in
bench_spreader as the measured loser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs to Layout

Layout::expand and expand_small now run the L1 formulation (limbs, bitwise
triple, up/down from X = 2P - Pe with one product each) through the 4-value
Dilation constants carried by the layer loops; Layout::with_interior builds the
layout and the interior mask from 3 shared lookups. Bfs drops its Step and its
private dilation, Dial its dilations; both take their edge endpoints from
Layout::edge_neighbours and Layout::neighbour_in. The generic Set<T> of Dial
moves to Bits. Caver::keep_component floods the frontier through
BfsInternal::component (cave -5.6 %, maze -10.6 %, serpentine -11.5 %), and
Caver::generate shares the layout lookups. The L0 forms stay in Variants for the
loser benchmarks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spreader fuzz benchmarks took 6 of the 8 minutes of the hexmap job. The
100 loser benchmarks are #[ignore]d and run, with the library fuzz tests on
256 seeds, in the non-blocking hexmap-full job on pushes to main
(snforge test --include-ignored). The library fuzz benchmarks take their
runs from the command line: 64 seeds in the pull-request job, their budgets
(max over 256 seeds) unchanged. Local run with a fresh build: 7m55s -> 2m46s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 216, 720

The pool is refilled below 2^64 instead of 2^32: the modulo bias falls from
2^-24 to 2^-56 per pool (2^-54.5 for shuffle6) at no measurable cost on the
generators (mazer per tile 31.87k vs 31.88k). Rng::draw6, Rng::shuffle6 and
Rng::split216 divide by their constant with core::internal::bounded_int
(1.10k vs 1.56k per division): mazer -1.4 % (order 0) and -2.2 % (order 1)
per carved tile over 8 seeds, digger maze -4.2 %, walker -2.0 % to -2.4 %.
The walker draws 6 triples per pool and takes both limbs of each permutation.
A spare pool kept in Rng (all the outputs of a permutation) and a counter
refill were measured and lose (+2 % per tile: one more field through the
recursion). Exact-grid expectations of the mazer, digger and walker tests are
regenerated; the reference walker follows the new stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The public traits of Mazer, Digger and Spreader keep their documented
functions only (generate, maze, corridor); the recursion, the dig and the
selection helpers move to pub(crate) MazerInternal, DiggerInternal and
SpreaderInternal. BfsInternal, Back, Endpoint, the stores and goals of Bfs,
Carver, Heading, TurnTrait, BitSetTrait and Counts become pub(crate): the
BfsInternal functions that misbehave with height < 3 or width >= 128 are no
longer reachable outside the crate, only behind the dimension check of the
public functions. No gas change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rng::draw_byte divides the pool by a NonZero<u8> bound with
bounded_int::div_rem (1.10k vs 1.56k, and no conversion of the result);
Rng::next_below and the spreader draws use it. Bits::top_byte and
Bits::low_byte replace the DivRem by 2^120 and by 256 of the popcount and of
the spreader select. Spreader -2 % to -5 % (max over the seeds), popcount
-12.6 %, next_below -14 %, walker start -4.6 %. Same draws: no expectation
changes. Measured and not kept: u8 by u8 (coords, parity, distance) costs the
same with bounded_int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One tile on a set of more than 8 is always a pick (30k-42k against a radix
level and the table, 46k or more): the cost rule is skipped. The grid is
checked on the limb that holds the end of the board (a POW128 lookup)
instead of a u256 comparison with a converted power. Count 1: -6 % to -7 %
mean, -5 % to -6 % max; every other count -0.6 % to -1.3 %. Same draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The field loops receive the 4-value Dilation and the width instead of the
layout: -0.1 % to -0.5 % on every field benchmark, the 3-class budget-2 case
back to its L3 cost (134_645).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctor

Every benchmark more than 5 % cheaper than on main gets ceil(1.05 * measured)
(77 budgets); the 10 spreader losers whose draws follow the new Rng stream
get their new max over 32 seeds + 5 %. The lot sections of GAS.md get their
measured and budget columns refreshed (their derived figures stay the lots'
own); the new P1 section lists the promoted helpers, the Rng decisions and
bounds, the measured losers, the per-unit costs of the generators, the CI
time and the before/after of every library benchmark. The spreader trial
microbenchmark draws with Rng::draw_byte like the library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bal7hazar
bal7hazar force-pushed the refactor/hexmap-commons branch from feee707 to 7f8214d Compare September 27, 2026 20:57
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bal7hazar
bal7hazar marked this pull request as ready for review September 27, 2026 21:02
@bal7hazar
bal7hazar merged commit 2fa4b58 into main Sep 27, 2026
10 checks passed
@bal7hazar
bal7hazar deleted the refactor/hexmap-commons branch September 27, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant