The analysis gives each local its declared or inferred type (a record's field, a list's element,
a call's result, words/floats) and takes a value whose type is a number or a bool out of every flow
and store: a float copied out of a frame's floats into a state's no longer makes the frame's list
kept (shadow_fit, water_reflection_pass, layer_partition_lods). frame_keeps 190 -> 181 on the game;
birth_leaks 564 -> 581, the lists that copy was hiding now seen as made and dropped outside a frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mem.scan() and R3D_ALLOC_SCAN=<frame> (at that frame's mark, when no function is running) walk
every heap block reachable from the program's globals - the states among them - conservatively: each
word that is a heap block's start (malloc_size says so) is followed, blocks made before tracking too.
A tracked block nothing reaches is a leak whatever a frame's totals say; they are summed by site and
printed ('alloc-scan: frame 39 - 32 bytes in 2 blocks ... reachable from no global or state', then
the sites). R3D_ALLOC_EXIT=1 runs the same scan as the program quits. A test: two records dropped in
frame 20 are the two found, the one pushed into a state is not.
The escape analysis now takes free(x) as giving x back (ES_FREED, flowing to what reached x) and a
print's argument as used up, so ludic deps --births lists only what is never given back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic deps --births lists every site the escape analysis finds kept by nothing and not the frame
arena's - boot and load code, a function spanning frames, frame code with the arena off - which is
made and dropped and never given back; birth_leaks is a number --check ratchets. A text used up by +
or == where it is made is freed at once and not counted; nor is what @alloc_ok covers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
keep(x) copies a string, a slice (header and elements) or a record (shallow) onto the heap; intern(s)
hands back one heap string per distinct text from a fixed table in the runtime (FNV-1a, 65536 slots,
copied the first time; past 49152 only copied). Both are how frame code keeps what it made on purpose:
the escape analysis takes the copy as the heap's and leaves the argument LOCAL.
The analysis now records why a class escapes (the store, the event, the global it reached) and
ludic deps lists every allocation frame code makes and keeps - fkeep lines, 'ludic deps --keeps',
the frame_keeps number --check ratchets - leaving out what is under @alloc_ok and a push's growth
(25.5's capacities). --arena-strict (or 'arena strict') makes each an error naming the store, before
anything is emitted. A test: a template stored into a state is the one error; keep and intern of the
next two, an @alloc_ok push and a scratch temporary are not; 195 frames of arena resets under
R3D_ARENA_CHECK=1 later the kept and interned texts read as made, and intern gives the same string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behind ludicc --arena (or 'arena on' in the program's package.ludic): the escape analysis runs and a
LOCAL site's allocation raises @lp_want for that one call, so it comes from the frame's arena. Two
halves in one mmap reservation (R3D_ARENA_MB each, 256 by default), bump-allocated with a 16-byte
size header, flipped at each frame mark: a frame's scratch is good through the next frame, then its
half is started again (R3D_ARENA_CHECK=1 fills it with 0xDD first). The heap takes over when no frame
is running, off the main thread, or past the half's end; lp_free ignores an arena block and
lp_realloc copies one out. R3D_ARENA=0 turns it off at run time; the census reports each half's
high-water mark. A program that builds text, a list and a record per frame: 29998 heap blocks made
and 18002 freed without it, 8 and 8 with it and 544 bytes of scratch a frame, the same output.
A dispatch's 'new' fills the queue's kept record (E_NEW.b), so 25.2 no longer counts it and 25.3
treats its fields as kept. '@frame' is keyed by property and field: a 'run' field is a root only in
a property that marks it, and 'tick' stays a System's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
emit_escape.ludic: every value is in a class, joined by flow edges (a let, an assignment, an argument
into its parameter, a result into the call) and store edges (a field, an element, a push). HEAP (a
parameter, a state, a global, what an unknown call hands back) flows forward; ESC (stored into
something HEAP, into a global, into an event's fields or named values, handed to an unknown callee)
flows backward, and from an ESC or HEAP target along a store. A load is its base's class. A site that
is neither ESC nor in a function reaching Mem.frame is LOCAL (Node.uns = ES_SCRATCH). ludicc
--escape-report prints each site and the totals; nothing is emitted differently yet - the arena that
allocates the LOCAL sites is next.
@alloc_ok on a generic now covers its instances (kept_push$NetFact is under kept_push's), and a
statement's @alloc_ok is carried on the node (Node.uns), so a generic's clone keeps it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>