Commit graph

821 commits

Author SHA1 Message Date
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
3205408290 Merge branch 'r3d/keeps-max' into lang/foundations 2026-09-28 16:47:35 +03:00
c6bea826f6 render3d: the lists sized at start-up carry @max at their room
The retire, free-range and spare-id lists (4096), the per-buffer flags and the prime handles (16384),
the stage and the actor spares (2048), the per-program uniform offsets (4096) and a draw's set key
(130) are each @max'd at the room gvk_startup_state / actor_init made, so outgrowing one ends a dev
run with its name instead of quietly copying. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:47:20 +03:00
54c0feafc2 Merge lang/foundations 6fb5118 into lang/ecs 2026-09-28 16:46:20 +03:00
46000362ba render3d: frame keeps 32 to 16 - tables and queues sized at start-up, the rest declared; birth leaks freed
The texture and framebuffer tables grow to 4096/1024 and gvk_prime's queue is made in
gvk_startup_state; gpu_unit_2d's and the actor lists' lazy starts go. @alloc_ok on layer_room (a layer
outgrowing its cap), overlay_init, gvk_read_screen, gvk_hdr_metadata (a settings change), DLSS's
gsl_struct/gsl_fn, an actor's part tables (given back by actor_release) and the pool's fallback.
Birth leaks freed: gpu_caps_probe's create structs, gpu_caps_fake's text, gvk_note's line,
gvk_layout_key's table and each key it grew from, ov_text_wrap's slices, the default sky path.
(ludic deps --births still lists freed sites: its walk does not see free().) Compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:46:17 +03:00
f8825dc55a ludic.ui, ludic.i18n: frame_keeps and birth_leaks to 0 - :nth-child's a and b read once per argument, a border-image's miss its own function; typing, the dev dump, parsing, the pointer ring's making, Ln's counted line and the plural header declared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:46:00 +03:00
6fb5118afd capacities (25.5a): @max(n) on a list field, and a full table is a failure
'@max(64) boxes: []Box' on a property's or a state's field is a promise: the grow path of a push to
that field (only the grow path, so nothing is paid until it doubles) checks it, and growing past n is
reported by the fence - 'PoolState.boxes grew past its @max(16) (it holds 16)' - counted under
count, said under warn, and under fail (a headless or dev build's default) the run ends with exit 87.
Mem.over("what") is the same for a package's own table: ludic.base's StrTable past its most
(sb_intern) and ludic.ui's memo past three quarters of MM_CAP no longer quietly copy per call. The
census counts overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:43:02 +03:00
23d204b9fc Merge lang/foundations 69d1db0 into lang/ecs 2026-09-28 16:41:55 +03:00
429e419327 ludic.ui, ludic.i18n: frame_allocs to 0 - a wrap, an ellipsis, a tooltip's lines, a text-shadow's colour and a range's value kept by what made them; pointer events from a ring; inline text joined through the memo; the pool's and the state's lists through ui_kept; parse, memo miss, pool miss, lazy start and template errors declared; the translation cache clears in place
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:41:47 +03:00
69d1db06a5 Merge branch 'lang/memory-birth' into lang/foundations 2026-09-28 16:38:21 +03:00