Commit graph

830 commits

Author SHA1 Message Date
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
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
1315baf332 Merge branch 'lang/memory-gaps' into lang/foundations 2026-09-28 17:13:53 +03:00
c8a588b2de escape (fix): the arena took ludic.ui's pooled nodes - a generic's call and an unknown callee now keep what they are handed
memory_final's gate crashed in all 13 scenarios at the first frame of play, R3D_ARENA_CHECK=1 reading
0xDD in ludic.ui's nd_take: a node the pool keeps had come from the frame's scratch. Two holes:

- a call to a generic (ui_kept(list, n)) names the generic, and the analysis knows only its instances
  (ui_kept$UiNode), so the callee looked unknown - and an unknown callee was taken to keep nothing.
  A generic's call now reaches every instance, and an unknown callee keeps everything it is handed,
  but for a short list of intrinsics known to keep nothing; view() shares its list's storage.
- a push's growth into a parameter's list was LOCAL whenever the list was not seen kept, though a
  parameter may be a state's list. A site is LOCAL now only when its class is neither ESC nor HEAP.

examples/lang/arena_pool.ludic is the shape (a pool keeping records across frames through a generic
push): built with --arena it prints '7 3498' poisoned on every reset and with the arena off, in
ludic-dev test. On the valley every ludic.ui pool site is kept; 5480 sites local, 6431 kept.

Also, from ECS: a record's field defaults are stored into it when it is made, a global's initializer
is kept, and a component's own functions are frame roots (they run while its page is open).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:13:05 +03:00
e91091d62b Merge branch 'lang/memory-census' into lang/foundations 2026-09-28 17:10:20 +03:00
5a5e5cb258 Merge branch 'r3d/owns' into lang/foundations 2026-09-28 17:10:20 +03:00
0a078c7822 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 17:10:20 +03:00
1bf3470873 render3d: a model owns its skin - @creates(Skin) skin_load, @releases(Skin) skin_free, @owns on Model.skin
resource_drops 0 and owned_leaks 0 over main. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:09:00 +03:00
bc50bd9b9a render3d: @owns on the handles records hold
Target's framebuffer and its colour and depth textures, a Mesh's index buffer (gvk_buf_new /
gvk_buf_delete now @creates / @releases GpuBuffer too), a Prim's mesh and an Actor's own skin clone.
ludic deps --resources over main: resource_drops 0, owned_leaks 0; a probe freeing a Target's fbo
alone was reported for its colour and depth (2), then removed. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:08:15 +03:00
8fbfeb7f9d ludic.base: core_undrained fills a list the caller keeps; hd_refuse's panic declared; ludic.photo's slug declared (a photograph taken)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:06:50 +03:00
a6364199da census by owner (25.5c): which state holds what, and how much it grew
The escape analysis now follows which state a kept value is stored into (a state parameter, a state
global - each its own class - through the flows to and from it), and every heap site in the fence's
table carries that owner. The census writes, per owning state, what its sites hold and how much that
grew since judging began: 'owner NotesState holds 6400 (+5600 since judging began)'. A keep() or
intern() is a site of its own for this, never scratch and never reported as a keep or a birth. It
needs the analysis, so the arena's (or --escape-report's) build; this is what a soak watches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:05:53 +03:00
f16ac4ef9e Merge branch 'lang/memory-owned' into lang/foundations 2026-09-28 17:04:40 +03:00
c5114a73fc Merge branch 'r3d/resources2' into lang/foundations 2026-09-28 17:04:40 +03:00
b87ee96805 owned fields (25.5e): @owns(Kind) on a record's field, and owned_leaks
A field marked @owns(PhysShape) holds a handle its record owns. A function that releases one owned
field of a record (body_free(w, s.body)) and neither releases nor hands on another owned field of
the same record type (s.shape) gives the first back and loses the second - the phys_remove bug, at
compile time. ludic deps --resources (or --owned) lists them; owned_leaks is a number --check
ratchets. A test: the function that frees a solid's body alone is the one found; the one that frees
both is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:01:28 +03:00
90074bfe60 render3d: @creates(Pipeline) on gvk_pipeline
A pipeline has no release: it is kept in the cache for the program's life (gvk_pipe_build stores it).
With every render3d handle annotated, ludic deps --resources over main e447acdd reads 0 drops; a
GpuBuffer made and bound to a local in a game function was reported (1), so the 0 is a real one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:59:45 +03:00
0b89ed85b7 render3d: @creates / @releases on its handles
GpuBuffer (gpu_buffer_new/free), GpuTexture, GpuFramebuffer, GpuRenderbuffer, Target (target_new/free),
Model (gltf_load / model_release), Mesh (gpu_mesh_new / gpu_mesh_free, mesh_release), Actor
(actor_new / actor_release) and SkinClone (skin_clone / skin_clone_free), for ludic deps --resources.
Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:58:31 +03:00
64b361510d resources (25.5e): @creates/@releases on Physics' handles - every shape maker and phys_shape_free (PhysShape), the body adds and phys_remove (PhysBody), the walker (PhysWalker), and ludic.nav's crowd agents (NavAgent)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:57:39 +03:00
a0c6f8ce7f Merge branch 'lang/memory-resource' into lang/foundations 2026-09-28 16:55:04 +03:00
e009ea313b resources (25.5e, first half): @creates(Kind) / @releases(Kind), and resource_drops
A function marked @creates(PhysShape) makes a handle one marked @releases(PhysShape) gives back.
ludic deps --resources lists every creating call whose handle is thrown away, or bound to a local
that is never released, passed on, stored or returned, and resource_drops is a number --check
ratchets. A test: a thrown-away create and one bound and never handed on are the two found; one
stored in a state and one released are not. A record's owned fields and a borrow form (a shape
used by several scaled ones) are the second half, with a resource type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:54:27 +03:00
a8906b4f79 Merge branch 'lang/memory-prim' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-28 16:53:21 +03:00
1cc507e181 escape: a value of a primitive type holds no reference - no flow, no store
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>
2026-09-28 16:52:24 +03:00
6f16e96de2 Merge branch 'lang/allocs6' into lang/foundations 2026-09-28 16:52:04 +03:00
cf8cbb3afb Merge branch 'lang/ecs' into lang/foundations 2026-09-28 16:50:50 +03:00
2fca1056c6 capacities (25.5a): @max on Physics' bounded lists - every fact pool and free list (1024), the walker slots (64), the shape and body-owner tables (262144), npc's walkers and posts (64)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:49:58 +03:00
39af9cabc0 reachability scan (25.5b) and exit accounting; free() and print release what they are handed
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>
2026-09-28 16:49:27 +03:00
3908163bdf Merge branch 'lang/memory-caps' into lang/foundations 2026-09-28 16:49:06 +03:00
4480353cb2 Merge lang/foundations 3205408 into lang/ecs 2026-09-28 16:49:02 +03:00
7d85ea3432 @max on the bounded lists: TextRing's slots, the pack's counts and change ring, the net's fact ring, the compass pools, ludic.ui's memo (its 3/4 of MM_CAP), screen pools, answer and pointer rings; ludic.base's intern test holds the table to its cap now that past it is Mem.over
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:48:53 +03:00
3762453d83 reachability scan (25.5b) and exit accounting; free() and print release what they are handed
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>
2026-09-28 16:48:35 +03:00