Commit graph

857 commits

Author SHA1 Message Date
d4edd0f5ba Merge branch 'lang/memory-plateau' into lang/foundations 2026-09-28 22:36:50 +03:00
0f04b63516 fence: declared but unbounded, and a rewarm that keeps the warm-up's deadline
Two blind spots from the windowed walk, where the heap grew about 1 MB a minute and the census read
`frames 0 bad 0 kept 0`:

- @alloc_ok memory was never held to its reason. Every R3D_ALLOC_DWIN judged frames (600) each site's
  declared bytes are set against their high-water mark: a new high adds to a streak, a flat window
  takes one off, a fall ends it. R3D_ALLOC_DRISE (6) is "declared but unbounded", said once per site
  with its line; fail mode exits 86. The census adds `unbounded N` and a `dsite` row per declared
  site by its growth since judging began. A list pushed forever grows by doubling, rising too seldom
  to make a streak; a record or text made every time (a re-mount's defaults) is what it catches.
- Mem.play() (every screen opened) restarted the warm-up, so memory kept every frame was never flat,
  the cap never came, and nothing was ever judged. A rewarm now keeps the first deadline, and past it
  has R3D_ALLOC_REWARM frames (120) of grace.

Goldens: alloc_fence_unbounded (a record a frame under @alloc_ok: exit 86, named, census unbounded 1);
alloc_fence_rewarm (kept every frame, Mem.play() every 360: judged and failed at frame 3000 - the
toolchain before this runs all 6000 frames and exits 0). alloc_fence_leak, _declared, _auto,
alloc_ok_private and the four arena goldens unchanged; the game's frame ratchets 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:36:08 +03:00
dba978e22b Merge branch 'rt/input-bufs' into lang/foundations 2026-09-28 22:35:12 +03:00
34421b8015 runtime/input: the window's mouse, pad and touch buffers and the typed text made once
input_device_commit made words(6), words(IN_PADS * 6) and words(IN_TOUCH * 3)
every windowed frame and dropped them - the walk's exit scan found 7 MB of
them unreachable after ten minutes (headless never polls devices, so no
headless run saw it). They are made in in_init with the rest of the input
state and filled in place. input_text's UTF-8 buffer is the state's too,
sized for the most the window hands over (64 units, four bytes each): it
made one per keystroke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:34:57 +03:00
01c2971ce7 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 22:34:24 +03:00
b30711877d ludic.ui: negative numbers' texts made once per value like the positive ones, big ones kept by value in a ring of 16 (mm_int made a string per call for every coordinate), and a stylesheet without a comment is not copied to strip one
Found by the windowed walk's exit scan (mm_int 1.2 MB, lss_strip 451 KB unreachable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:34:19 +03:00
fc99cee9a9 ludic.minimap: minimap_seen_text writes the explored grid into bytes the state keeps (a string per cell before, every save)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:33:49 +03:00
2603a79cc4 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 21:46:15 +03:00
a0f60e5296 ludic.wildlife: wildlife_reserve sizes both animal tables and their id maps at a world's start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:45:55 +03:00
5ffa67b31b Merge branch 'lang/memory-alias' into lang/foundations 2026-09-28 21:17:05 +03:00
1786e144bb ludic.ui: ui_attr's miss is the node's own kept null (made with the pooled node), not a new one per ask - every control asking min/max/step/value per frame made one; a select's options without a value matched by index without asking
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:17:05 +03:00
de6d78bef5 escape (fix): kept memory is HEAP, so what is pushed through an alias of it is kept
render3d's stream_new holds its pool through a local (`let live = s.chunks; push(live, new
Chunk)`): the flow edge from s.chunks to live carried ESC to nothing, the Chunk records were
LOCAL, and the arena reset them under the stream - the row and horse scenarios' crash at
0xdddd... in fn_stream_update. An ESC class is now HEAP too, so every alias of kept memory is,
and a value stored through it is kept. Bidirectional alias edges were tried first and over-kept
through returns (el_place, rim).

- examples/lang/arena_alias.ludic: the stream_new shape; poisoned it read 3 3000, now 3 1518
- examples/modules/alloc_ok_private.ludic: @alloc_ok on a module's private function and on a
  statement in its private generic, declared at run time (it already passes: a guard)
- the game: frame_allocs, frame_keeps, owned_leaks 0; the lab builds under `arena strict`; the row
  scenario poisoned (R3D_ARENA_CHECK=1, 2400 frames) runs clean, bad 0 kept 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:15:32 +03:00
2b02c0a931 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 21:09:40 +03:00
bd2d62ddca ludic.ui: a select matches a number as a number (its text was made every frame per select); the fired-event queue's lists made with room; ludic.inventory: the counts made at full length at once
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:09:08 +03:00
4d3ecfb72c globals initialized in the order their initializers need each other
A state's field default reading a registry (jn_life_sp: []int = jn_life_species_new(), which reads
Species[sp].population) ran before the registry was filled, because globals were initialized in
declaration order: every gate scenario crashed in L_init_globals. Each global's initializer is now
followed - through the functions it calls and a record's field defaults - to the globals it reads,
and those are initialized first (a depth-first post-order; the source order kept between globals
that need nothing of each other, and in a cycle). Putting every state last is not enough: some
tables read a state's instance too. A test (a state whose default reads a registry declared after
it) crashes on d483c92 and prints '2 4' now; Maroon Lake's headless game loads and plays 180 frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:35:09 +03:00
d483c9283c frame allocs: a local shadowing a function is no edge, the drain's calls are no edges, HEAP keeps only a growth off the arena, and the arena starts at its first use
- the call graph is by name, and a local or a parameter named as a function (a float bd, part, bx)
  linked to that function: a name the function binds itself is never an edge now.
- drain_actions, generated, calls every reducer; a reducer is reached from its action's dispatch,
  so the drain's calls are not edges.
- a fresh value flowing into a local that also holds kept memory is still the frame's (storing it
  anywhere kept would have made it ESC): HEAP now keeps only a push's growth off the arena.
- the arena starts at its first use rather than at the first frame mark, so boot's temporaries are
  scratch too - dead once the Start handlers return - and a scratch site is never a birth.
Plus ludic.hints' rail and three of ludic.update's one-off lines declared. The arena goldens pass
poisoned. Maroon Lake (d79d189f): 39/11/66 -> 30/9/0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:23:28 +03:00
8228597ade Merge branch 'lang/ecs' into lang/foundations 2026-09-28 20:15:39 +03:00
858544e231 Merge branch 'lang/memory-burn3' into lang/foundations 2026-09-28 20:13:59 +03:00
4fc9d2616a ludic.fishing, net, clock: their dice made once and seeded again, never made again per reset; ludic.minimap's near list kept, its marks and grid declared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:13:58 +03:00
3ac1179440 fix(allocs): ludic.update, steps, effects and telemetry at 0 frame allocs and 0 keeps
ludic.update: whether this copy can update itself is worked out once, when the updater is configured
(the panel asked every frame, building the path to the executable each time); the notes are a list
the state keeps, filled by update_notes_for when the version or the language changes, which the
game calls from its update tick. The feed's address is declared (once, when a check starts).
ludic.steps, ludic.effects, ludic.telemetry: what is left is made on an event and declared with its
bound - an arc's tables, a chapter's columns, a step's fact, an effect's start, end and clear, the
fact pool's growth, the player id, a props record's nesting stack - and two pushes into a caller's
kept list, at most a chapter's steps and the ring's size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:13:06 +03:00
b3ce6463b5 Merge branch 'r3d/zero' into lang/foundations 2026-09-28 20:12:16 +03:00
f993c4a36a r3d/runtime: allocs, keeps and births at 0 on this side
render3d: shadow_fit, water_reflection_pass, layer_partition_lods and the
GPU cull's scratch are made with the state; v3_dist is scalar; the pushes
into lists sized at start-up, the caps probe, the table growth, the loads
and the constructors declared with their bounds (one statement a line);
the renderer's name made once with the device; the two error messages
given back; the dead lupine models removed.

runtime: a component's text is held interned in its value cell (one copy
per distinct text), so the getter's own text goes with its frame instead
of being kept by ludic.ui's model - 80 of the 83 keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:10:59 +03:00
dc614a6a33 Merge branch 'lang/memory-fnref' into lang/foundations 2026-09-28 20:10:31 +03:00
5d34d0fd09 escape (fix): a function taken as a value keeps what it returns, and an entry is walked
Two more holes of the arena's family, found from ECS's births:
- a function called through a function value (UiClass.make's cmp_x_new, a step list, a System's tick)
  has its result flow nowhere the analysis can see, so what it returned looked LOCAL - and a caller
  keeping it (ludic.ui's instance table) would keep scratch. Every function taken as a value (fn f)
  now has its result kept.
- an entry block has no name, and the analysis only walked named declarations: what an entry stored
  was never seen. It is walked now.
examples/lang/arena_fnval.ludic (a factory in a field, its records kept by a pool across frames)
crashed poisoned before and prints '5 1053' as the heap does now; in ludic-dev test. Maroon Lake:
component constructors are kept, not births (birth_leaks 115 -> 103); what fn values return is kept
(frame_allocs 194 -> 210, frame_keeps 151 -> 162); 5293 sites local, 6586 kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:10:18 +03:00
4bca5dfa92 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 20:08:02 +03:00
00408604da Merge lang/foundations b513f7b into lang/ecs 2026-09-28 20:06:25 +03:00
179dd60536 ludic.ui: parsing at load gives back what it does not keep - tpl_words written once into bytes of its own (a string a character before), a stylesheet's stripped text, its error list and each reader freed once read, ex_text's joining node freed when it collapses to one piece or none
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:06:25 +03:00
ae68b7f752 Merge branch 'lang/memory-burn2' into lang/foundations 2026-09-28 20:06:23 +03:00
b12b66e89a frame allocs: what the arena takes is not counted; a scratch site is a birth only when boot reaches it
With the arena on, a site the escape analysis proves LOCAL is the frame's scratch - made and gone
with the frame - so frame_allocs now counts only what frame code still takes from the heap. And a
scratch site is the arena's whenever the game's frames run (a frame, a click handler, a reducer), so
it is a leak at birth only when boot's code (a Start handler) reaches it, before the first frame.
Maroon Lake: frame_allocs 337 -> 194 with the game's own fixes, birth_leaks 189 -> 115.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:06:04 +03:00
b513f7b5b1 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 20:04:57 +03:00
d9612ba2a6 ludic.base: a queue's fact records come round again (q_rec / q_ring_add), used by effects, clock, wallet, weather, tracks, needs, fire, settings, update, steps, crafting, shop, gear, hints, session and photo instead of a new record per fact; the minimap's marks cleared and rubbed out in place, its scale steps without a list; the clock's dice seeded again, not made again; once-per-join, per-save and pool-miss paths declared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:03:46 +03:00
3b9b4a589b frame allocs: a click is not a frame, a reducer is its dispatch's, a capped push is bounded; ludic.photo at 0
The analysis made every component function a frame root, event handlers (cmp_x_on_delete) too, and
every reducer, whether its action is dispatched every frame or once a trip: a component's 'on'
handlers are no longer roots, and a dispatch is an edge to its action's reducers, so a reducer
counts only when frame code dispatches it. A push into a field declared @max(n) is bounded by the
fence's own check and no longer counted. Maroon Lake: frame_allocs 395 -> 337, frame_keeps 188 -> 169.

ludic.photo: the roll's order and a page of it are kept lists refilled in place (the pack's page
asked for both every frame), its kept lists say @max(256), and a shot's tags, a photograph's fact
and a new roll are declared (once per shot, sale or trip). 18 allocs and 9 keeps -> 0 and 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:02:47 +03:00
5be2422c84 render3d: a screenshot frees its read-back, header and row
gvk_screenshot kept a buffer the size of the screen per shot (the valley scan's largest unreachable
site), and its PPM header and row buffer; each goes on every way out now. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:22:01 +03:00
4df15eacfb ludic.ui: a fired event, a press, a hold and a native's events make nothing - a ring of 16 event objects and one hold object made with the state with their keys, and every ui_fire with no value carries the kept null
The last kept bytes of the leak gate's pack, shop and journal screens (memory_final, count mode).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:20:01 +03:00
1e714623b7 Merge branch 'lang/memory-fastfree' into lang/foundations 2026-09-28 18:56:10 +03:00
fc22c3f250 ludic.ui: a bar's min, max and value read without ui_attr's new null for a miss (ui_attr_float), and the action answer ring made full with the state - craft's bar kept a block a frame, pack/shop/journal kept one frame filling the ring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:56:10 +03:00
dd20cb2d5d fence: free and realloc stay on the fast path with the arena on - the arena's range checked inline
With the arena running every free and every realloc took the slow path (a call to check the range,
then the fence's own test). Now lp_free checks the arena's range inline and hands anything else to
libc unless the fence is tracking; lp_realloc goes slow only for a scratch request, a tracked run or
an arena block. Ready for when the final run's medians ask for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:37:45 +03:00
8c72437ecc reseed after the scan fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:27:50 +03:00
2f6386069d Merge branch 'lang/memory-scanfix' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-28 18:26:57 +03:00
ab6b666fd6 ludic.ui: a scroll box without scroll-top asks ui_attr_has first - ui_attr's miss made a new null every frame on every screen that scrolls (the gate's one kept block a frame on map, shop, journal and craft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:26:46 +03:00
91f91716b5 scan: follow a word only if it could be a heap block's start - 16-aligned, in the user address space, not in the arena
The exit scan crashed two of the gate's scenarios (world, swim: SIGBUS and SIGSEGV in lp_mem_scan at
0x0e00000c65800000 and 0x04000004e461c000): a word of data with its high bits set was handed to
malloc_size, and a zone faulted looking it up. A candidate must now be 16-aligned, at or above 4 GB
(macOS's page zero), below 2^47, and outside the frame arena before malloc_size sees it; a block's
own words are read only once malloc_size has said it is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:21:37 +03:00
4e0c30488f reseed after the fence's declared bytes and the Math.* result types
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:04:07 +03:00
1381c6c903 Merge branch 'lang/dispatch-check' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-28 18:03:09 +03:00
d59636bcf6 Merge branch 'lang/memory-once' into lang/foundations 2026-09-28 18:03:08 +03:00
5aa7c03022 fence: declared bytes are never judged nor listed; the scan's sites sorted by bytes, as many as asked, and all to a file
What @alloc_ok covers (a function and its callees, a statement, a statement in a generic's body on
every instance) was counted apart in the frame's verdict, but its sites still carried the bytes the
report and the census rank by, so a declared site was listed as if the frame failed for it. Declared
bytes now have their own per-site counter and never enter live, a site's row or the verdict:
examples/lang/alloc_fence_declared.ludic, all three forms after warm-up, passes the failing fence
('bad 0 kept 0', 1488 bytes declared), with and without the arena, and an undeclared site in the same
frame is still the one listed.

The reachability scan's sites are now the largest first (R3D_ALLOC_SCAN_TOP, 24 by default), and
R3D_ALLOC_SCAN_FILE=<file> appends every site that holds unreachable bytes: the whole table to triage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:03:00 +03:00
2cce41062f Merge branch 'r3d/scan-triage' into lang/foundations 2026-09-28 17:59:13 +03:00
07fe90eb5f render3d: what the scan found dropped - shader code, create infos and paths freed after the create
The valley's reachability scan (R3D_ALLOC_SCAN) listed render3d start-up objects nothing held; all were
dropped after being handed to the driver or copied into a key: gvk_module's SPIR-V bytes, create info
and handle slot; gvk_program's key parts, .spv paths, bindings and layout create infos; the compute
program's the same; gvk_sampler's and gvk_view_of's create infos; gvk_zero_vbuf_get's 64 KB of zeros;
gvk_layout_key's result (always a copy now, freed by gvk_pipeline once its key holds it) and
r3d_program's defines. Each goes once the handle it made has been read. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:58:15 +03:00
ac8c7b0c0b ludic.clock: clock_hhmm reads a table of the day's times made with the state; ludic.minimap: minimap_pin past the end is one kept empty mark; ludic.ui: ev_text's memo miss is its own declared function
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:57:30 +03:00
5e80db327d arena: a LOCAL mark is honoured only while emitting a function the escape analysis walked
A default's node is emitted wherever its record is made, some of it in code the analysis never walks
(a scene's body, a test's); a mark from a walk elsewhere took effect there unchecked. The emitter now
asks for scratch only inside a function or @On body the analysis walked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:18:55 +03:00
bad7c4a255 escape (fix): a node walked more than once is scratch only if every walk found it LOCAL
A field's default is one expression, walked at every 'new' of its record: marked LOCAL by a frame's
temporary, it stayed marked when a record a pool keeps was made from it, and that record's list came
from the frame's scratch. The marks are now taken off any node one walk found kept.
examples/lang/arena_defaults.ludic is the case (a pool's record made in frame 3, a temporary of the
same type every frame): foundations 1315baf crashes on it poisoned; this prints '497 124747', as the
heap does. In ludic-dev test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:17:33 +03:00