Commit graph

922 commits

Author SHA1 Message Date
25c8c9279c intern: past its store or its table, the fence's overflow - said once
Past the 4 MB store lp_copystr falls back to the heap, and past the table
(49152 texts, or 64 probes) lp_intern copies on every call: either way a
program interning without bound would grow unseen. Both paths now call
lp_intern_over, which reports through lp_cap_over once ("intern (4 MB of
text, 49152 distinct texts) is full"), so warn says it and fail stops the
run (exit 87) like any capacity past its promise.

Checked with a compiler built from these sources (selfhost-build): 60000
distinct texts under R3D_ALLOC_FENCE=warn print the line once and every text
comes back right; under fail the run exits 87.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:37:56 +03:00
48a8caa292 reseed after the intern store
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:34:35 +03:00
a99f75f2a4 Merge branch 'lang/intern-store' into lang/foundations 2026-09-29 00:33:43 +03:00
47bb0e5d4d intern: texts copied into one store in the binary, not a malloc each; number texts interned too
Walk 9's fence named 256 frames of +16 B at gvk_tex_storage:244 and
m4_new:92, always under HudDay's and HudGuide's models. Neither site
makes 16 B: the blocks were intern's copies (lp_copystr), which never set
lp_site and so were charged to whatever allocated last. The HUD's clock
and the guide's distance are a new text every few seconds, and each first
one was a malloc kept for good.

lp_copystr now copies into @lp_istore, 4 MB in the binary (untouched pages
cost nothing), and falls back to the heap only past it; lp_free ignores a
pointer into the store, so a text freed after it was interned is no fault.
value_num_text - the text a screen shows for a number, dropped whenever
the number changed - is interned the same way and its string() given back.

Checked: a program interning 100000 texts gets every one back right, the
same text as the same pointer, a freed one harmless; the headless valley
compiles, with frame_allocs, frame_keeps and birth_leaks at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:31:34 +03:00
765d800842 Xml parse gives back what no node keeps: a closing tag's name, and the text runs and joins an element's text leaves behind (a run a #text node shares is kept); ludic.ui frees an attribute's value once parsed (every reader copies what it keeps) and act_parse's reader
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:30:13 +03:00
7b20f305ff Xml.free gives back a parsed tree's records and lists (never its strings) and the parser's cursor is freed; ludic.ui frees a template's XML once built, sel_parse's and ex_parse's readers, lss_rule's declarations holder, and px_cmp no longer makes a list of its operators per call
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:11:40 +03:00
3816d9924b render3d/water: the saved camera's two views made once, not with each reflection target
water_reflection_pass made view(water_saved, 16, 16) and view(water_saved, 32, 16)
inside the block that makes the reflection target, so every time the target was
made again (a resize, a render scale, a settings change) two more 16-byte views
were made and the last two dropped: the windowed walk's fence caught one such
frame, +32 B. water_saved is the state's for good, so its views are made once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:47:25 +03:00
afc2d019a3 ludic.ui: a node is made with its UiNodeX, so nx() makes none in a frame; a screen's pool grows both generations together and by half again at once, so a screen's first frames and its reopenings make nothing past the first growth
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:20:38 +03:00
0ed66baed0 Merge branch 'lang/sb-stdout' into lang/foundations 2026-09-28 23:17:32 +03:00
90dd603cee fix(ludic.jobs): jobs_taken and jobs_shown fill a kept list per group (the map and the journal asked every frame, and the fence caught a list kept)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:01:53 +03:00
7187aaa4d5 Merge branch 'rt/input-bufs' into lang/foundations 2026-09-28 23:01:44 +03:00
a5f6c02a09 runtime/audio (macOS): voices on one AVAudioEngine instead of an AVAudioPlayer per sound
AVFAudio keeps a 64-byte AudioQueueOwner for good on every AVAudioPlayer play
after the clip has finished - measured one a play, whatever is called around
it (prepareToPlay, pause, no rewind) and still there after the player is
released; a stop before the play made one every time. The windowed walk showed
it as AudioQueueOwner 73 -> 87 in a minute.

A sound is now decoded once into a PCM buffer and played by a voice of its own
on one shared engine: a player node (the buffer scheduled again on each play,
looping for -1), a varispeed (the rate) and a small mixer (volume and pan).
snd_playing compares the uptime clock with the end worked out when the clip was
played (asking the node where it is made two AVAudioTime objects a call), and
snd_play drains an autorelease pool of its own. The C interface is unchanged.

A harness driving snd_* directly (400 plays past the end, the playing flag
checked during and after each, a loop and a stop, the setters) holds the heap
flat to a block and gets the flag right 400 of 400; the windowed valley
compiles and links against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:00:59 +03:00
f4233ba11d ludic.base: sb_stdout - a StrBuf's bytes to standard output with no string made (a per-frame log line the fence would otherwise see kept)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:58:37 +03:00
88580ca319 ludic.ui: reading a sheet gives back what no rule keeps - lss_strip writes into bytes of its own, a rule's selector and body texts, its selector pieces, each declaration's piece and raw key and value slices, a @keyframes or @media body and a keyframes reader freed once parsed (a kept tpl_words result, a keyframes name and a @media condition never)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:55:40 +03:00
beebbd0bc1 ludic.ui: lss_text frees the stripped sheet only when lss_strip made one - a sheet without a comment comes back as src itself since b307118, and freeing it aborted the game at start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:40:33 +03:00
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