Commit graph

145 commits

Author SHA1 Message Date
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
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
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
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
ce2699dfee leak at birth (25.2d): an allocation nothing keeps, made where the arena does not take it
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>
2026-09-28 16:37:50 +03:00
a0b030290b region rule (25.3c): keep() and intern(), frame_keeps, and --arena-strict
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>
2026-09-28 16:33:53 +03:00
8ca18725b6 escape (25.3a): which allocations never outlive their frame - the analysis, behind --escape-report; @alloc_ok on generics
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>
2026-09-28 16:05:07 +03:00
dd55945670 Merge branch 'lang/memory-fence' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-28 15:51:27 +03:00
76b1bd20ae frame allocs (25.2): what a frame can come to allocate, counted and ratcheted
deps_reach's graph gains the handlers and each @On body (an emit reaches its event's listeners).
Roots: a handler in a frame phase, every reducer, an @On body, and a function stored as a System's
tick. Every allocating construct in what they reach - new, a list literal, push (grow), text built
by + or a template, words/floats/buffer/bytes - is a falloc line with the shortest chain from a
root (root>..>last six), and the program's count is frame_allocs. @alloc_ok("why") on a function or
a handler takes it and what only it reaches out; the reason is required. ludic deps --allocs lists
them, and frame_allocs is a number --check ratchets. Maroon Lake starts at 2661.

Not yet: @frame on a step list's field (only 'tick' is a root field so far), statement-level
@alloc_ok, the three lints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:50:09 +03:00
84155274c4 Merge branch 'lang/memory-fence' into lang/leaks2
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
#	tools/ludic-cli/test.ludic
2026-09-28 15:46:11 +03:00
edb33400b3 Merge branch 'lang/json-out' into lang/leaks2 2026-09-28 15:40:00 +03:00
691964877b fence (25.1c): the census reads the heap outside Ludic's blocks; blocks counted at malloc's own size
The census's native line is malloc's live bytes over every zone since judging began less what
Ludic's tracked blocks kept - the libraries' and drivers' growth, read before the census file is
opened. A tracked block counts malloc_size(), not the size asked for, so kept is what the heap pays
and the residual carries no rounding. @malloc_zone_statistics is declared once, by the fence or by
Os.heap_bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:38:13 +03:00
a8d54e9878 fence (25.1): every allocation goes through the fence - sites, frame judging, census, callers
Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a
Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load
and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2).

On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame
on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX
after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends
holding more than it began with is reported by site with its callers (the unwinder, taken only once
judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS
writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=,
--fence-census= or a fence line in the program's package.ludic; the environment overrides them.

The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one
calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples
alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:35:29 +03:00
4499cedcb8 runtime: Json.write_file, and Json.encode through a kept buffer
The encoder appends into RtJsonState's buffer (grown only past the biggest document yet): ints and
Q16.16 fixeds written as digits in place, floats through string() and freed. Json.encode copies the
answer out once; Json.write_file hands the buffer to Fs.write_text (.tmp + rename) and keeps nothing.
json_saves.ludic: exact text, the file equals encode, parse round-trips, 1000 saves grow 0; clean
under MallocScribble. json_quote (the + builder) is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:32:36 +03:00
6cdef20cc2 ludic.ui: an unmounted component is mounted again rather than made again - its record renewed (a generated renew), its props and model kept; an action's call answers into a ring
A prompt that comes and goes as a player walks (co-op's netleak: in_get, cmp_*_new, bd_class, value_slot/put)
made a new record, props and model on every mount, and an action's call answered into a new Val (ev_call_with).
examples/library/ui_remount: two thousand comings and goings hold the heap at 0, and the counter starts at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:25:55 +03:00
254097657e runtime: Log, DateTime.format, Input.text, Path, Mime, Fs, Os and Text keep nothing per call
Found by reading every builtin (Os.platform's 8 KB per call started it). Log builds its line only at
or above the threshold and frees it; DateTime.format folds through + so its pieces go; Input.text
encodes into one buffer; Path/Mime/Fs/Os free their temporaries on every path; string results of
Text/Path/Mime/DateTime/Os dirs are fresh and Text frees a fresh argument. Reseeded.
runtime_temps.ludic: 19.8 MB -> 0 over 20,000 rounds, 64 KB -> 0 over 200 of file work; clean under
MallocScribble. string_temps still 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:55:49 +03:00
d9c574f107 Merge branch 'lang/str-temps' into lang/uifree 2026-09-28 12:19:16 +03:00
9660587e10 compiler: free a string an expression made once it has been used
The left half of a + chain, a template's pieces and holes, a number's text and a side made only to be
compared are marked fresh and freed after the +, ==, != or print that reads them. lp_int_str and
lp_long_str move their digits to the start of the buffer, so the pointer they return is the one
malloc gave. Reseeded. examples/lang/string_temps.ludic: kept intermediates stay good, and 20,000
rounds grow the heap 0 bytes (2.9 MB before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:06:45 +03:00
08a6fc8a3f Merge branch 'lang/chunks' into lang/uifree - the ground's heights by chunk and its Jolt ground per chunk; both new render checks kept (steady, chunks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:55:55 +03:00
ff658633fb feat(render3d): terrain_chunk_heights - the ground's B-spline heights a chunk at a time (23.5)
n x n samples of chunk (i, j) of a size_m grid from the terrain's corner, row-major into the
caller's buffer with nothing allocated, each the (1 4 1) / 6 B-spline filter of the texels under it
(ter_spline_at): what ludic.physics' jph_shape_heightfield_bspline makes of the whole map, so a
chunk's physics ground matches the drawn one and its neighbours' to the bit. The read-back lives
for the whole map (terrain_generate to terrain_unload). examples/rendering/chunks.ludic, in the
suite, checks the shared edges, a sample against the filter by hand, and a short buffer refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:25:21 +03:00
866037a0d7 fix(render3d): nothing allocated in the steady state - the Vulkan allocator reuses its records
gvk_mem_new made a one-slot []pointer per allocation, and turned the requirement's size and
alignment into strings to read them as ints; gvk_list_drop_last rebuilt the spare-record list to
drop its last entry; gvk_mem_id did the string round trip on every free. One slot is kept
(gvk_map_slot), int() truncates a long, the spare list pops. Every Text.to_int(string(x)) in
render3d is int(x) now.

Vk.heap_bytes() (vk_mac.ll: malloc_zone_statistics' size_in_use; 0 on Windows) and
examples/rendering/steady.ludic, in the suite: a buffer released and made again 5000 times and
600 whole frames gain 0 bytes each - the allocator before this, 1,120,000 over the 5000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:14:56 +03:00
8cc4bdf66b feat(render3d): 23.3 - a .dds beside a .png is uploaded BC-compressed with its whole mip chain
The device's textureCompressionBC is asked for and remembered (gvk_has_bc). tex_load_ex prefers a
DX10 .dds with the full chain beside the .png (not for an edge-padded cut-out atlas):
texture_dds.ludic reads BC7 / BC5 / BC4, gpu_tex_compressed makes the image with every level and
no colour-attachment use (a compressed image is only sampled and copied into), and
gvk_tex_upload_blocks copies each level's blocks from one staging buffer. A colour map is BC7
sampled as sRGB, a data map BC7 read as it is. examples/rendering/bc.ludic holds it, in the suite;
ludic.lab's plate carries its .dds (its three shots render at 56-60 dB against the .png's).

ludic-dev test 307/307, no Vulkan SDK in the environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:23:45 +03:00
9425bdc4e5 feat(render3d): 22.10 - ludic.render3d carries MoltenVK, so a Mac program has its Vulkan driver
native/build.sh builds MoltenVK v1.4.2 from its pinned, checksummed tag (its dependencies at the
commits its ExternalRevisions pins), thinned to arm64, id @rpath/libMoltenVK.dylib, signed ad hoc;
its licence goes in native/LICENSE-MoltenVK. Every render3d program links it through its rpath -
the package's lib/ while developing, Contents/Frameworks in a bundle, where ludic bundle puts and
signs it - and vk_mac.ll also looks for @rpath/libMoltenVK.dylib (after an SDK loader, so the
validation layer still stacks in development). The suite's SDK stand-in (vk_env) is gone: the
render checks draw on the MoltenVK the package carries. gpu_is_gl() removed; nothing calls it.

ludic-dev test 306/306 with no Vulkan SDK in the environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:16:26 +03:00
9a4137e9da wip(render3d): plan 22.14 - the OpenGL backend removed (suite 306/306, not yet handed over)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:30:11 +03:00
e634177ca4 feat(render3d): 23.4 - model_release, shared textures counted, and scatter layers sized to what they hold
- model_release(model) frees each primitive's mesh, and each texture once no other model uses it:
  the glTF texture cache counts the primitives using each texture (a path loaded again, and a LOD
  chain borrowing its LOD0's material, each take a reference); the last one frees the texture and
  forgets it along with any remembered material naming it, so a later load is a fresh one
- a scatter layer's instance and sort arrays start at 256 and double as layer_add or a stream
  fills them, to the layer's cap, instead of the whole cap up front (0.4 GB in Maroon Lake)
- gvk_tex_read's copy struct comes from the scratch ring
- examples/rendering/release.ludic holds it (RELEASE OK on Vulkan and OpenGL), in the suite

smooth renders pixel-identical before and after (max |d| 0). ludic-dev test 306/306.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:34:14 +03:00
cc384efee0 feat(render3d): the meadow's blades are culled on the GPU - under 1.5 ms of a MoltenVK frame for all the grass
- grass_cull.comp decides each candidate blade once a frame (place, ground, density, water,
  slope, frustum, colour field) and writes survivors into three bands by distance, one indirect
  draw each; grass_inst.vert only bends and places the vertices. OpenGL keeps grass.vert's
  per-vertex path; R3D_GRASS_GPU=0 compares
- compute programs take sampled textures after their buffers (gpu_compute_tex / gpu_dispatch_tex),
  and every dispatch now records a compute-to-draw memory barrier
- the blades as a sward: 4 m cells, bands with five, three and one-quad blades, spacing doubling
  every 18 m to 70 m, never narrower than a pixel; lit facing the sun and leaning to the sky,
  shadow looked up above the ground (it read the terrain as its caster), a colour ramp that
  leaves only the sheath dark, clumps, dry patches and a tussock shade worked out per blade
- scatter layers flagged grass are skipped while the blades draw (and under R3D_NOGRASS);
  R3D_BLADES, R3D_NOBLADES win over the game's setting; R3D_GRASS_S0/D0 for measuring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:47:36 +03:00
1fc01df39c feat(bundle): a native library's licence ships with it - each linked package's native/LICENSE* beside the .exe, in Contents/Resources on macOS
Checked on both machines with a bundled Jolt probe: the licence and the library in place, the app
signed (Mac) and the probe's ball landing at the same height on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:43:16 +03:00
b247603b7e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:20:44 +03:00
38d4ea72b1 feat(render3d): Vulkan in a macOS window through MoltenVK; window built-ins take a slice's elements; the NEAR_FADE SPIR-V
- a CAMetalLayer on the view (cocoa.ll win_metal_layer), VK_EXT_metal_surface, QuartzCore linked
  with Vk.*; the drawable measured after the layer sets the backing scale
- vk_mac.ll opens MoltenVK directly after any loader: a bundle ships only libMoltenVK.dylib
- a covered window is not presented to (win_visible); one frame in flight on macOS
  (R3D_VK_INFLIGHT), with images, buffers and descriptor pools held until it is done
- win_held / win_mouse / win_pad / win_touch / win_text / win_present pass slice elements (arg_buf):
  every windowed program died on its first input poll
- variants.list and SPIR-V for the three NEAR_FADE foliage programs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:52:45 +03:00
b44b88e00e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:22:50 +03:00
54f5efb23f feat(migrate): 0.R5 - ludic migrate state --tighten takes mut off every state parameter nothing down the chain writes; --root DIR lets the edits reach a directory without running its programs; a write through a local holding part of a mut state counts as a write to it
Maroon Lake: 149 parameters became read-only. What stays mut is a real write - in the packages mostly a
lazy start inside a question (things_all, gear__ensure), which is what to take out next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:20:44 +03:00
5d3a799e33 feat(pkg): phase 15 - a package can carry a native library
native "<target>" "<path>" in a package's package.ludic; the compiler records the libraries of
every package a program imports and writes them into the IR (; ludic-native:), so ludicc -o,
ludic build, ludic test and ludic bundle all link one list. macOS: an rpath to the package and to
Contents/Frameworks, where ludic bundle copies and signs each library and drops the build
machine's rpath. Windows: the import library, the .dll copied beside the exe (--natives-out for
the bundle). tools/native/lib.sh builds from a pinned, checksummed source with clang on both
machines; ludic.nativeecho is the worked example; the shim rules are in packages/README.md.
Linked at build time rather than dlopen (docs/PACKAGES.md says why). Reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:35:45 +03:00
5df0747642 feat(cli): 0.R5 - ludic deps says what a function can come to change (widest_write_reach, --wreach N); a port member and a registry field reach only themselves
A reach through Port.member() follows that member's binding (or its default), and Registry[i].field -
or a local holding Registry[i] - follows that field in each entry, so a question asked of a port or a
table that also holds verbs no longer reaches the verbs. In Maroon Lake that took the valley's
'what is this Thing called' from 47 states it could change to 1. examples/state/write_reach.ludic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:19:54 +03:00
c64f8750e2 feat(lang): 0.R5 - a reducer writes one state and may read others, declared between its state and the action
What only becomes known inside the drain - a value another reducer just set, the map in play - no longer
has to be faked into the action, so a verb that reads several systems while it changes one is a reducer
instead of an act handed its states. A second state to write is still refused, and the message says the
way out. examples/actions/reads.ludic; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:23:48 +03:00
259b4d9b35 fix(ui): 0.R4 - an action a UI button dispatches is reduced before the frame is presented
ludic.ui runs a frame's presses after drawing it, and what they dispatched waited for the end of the
phase - after the host had presented - so a button's change showed a frame late. Decided: drain, not a
phase per action. ui_show and ui_press drain the queue once the presses have run, so the code after
them and the frame presented next see the change; a host that runs presses some other way calls
drain_actions() before it presents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:08:26 +03:00
d46f0adb5e fix(lang): 0.R4 - ludic.ui's UiAct is its own, and a name that meets a package's export says whose it is
A game's exported UiAct collided with ludic.ui's, which nothing outside ludic.ui uses: it is private
to ludic.ui now, and a private record of one spelling in two modules never clashed. A real clash - a
type named like one a package exports - is still refused, and the message names the package and the
way out (`ludic_ui exports it, and exported names are one namespace - rename this one, or declare it
without export inside a module of your own`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 06:57:35 +03:00
eb1e780733 feat(cli): 0.R4 - ludic deps sees through fn values (widest_reach) and lists the widest functions (--widest N, --reach N)
A step list or a registry of fn values takes no state and still reaches every state its steps take.
The compiler now writes `reach <n> <function>` - every state a function can come to by a call, a
`fn f` it writes or a global holding fn values it reads, to a fixed point - and ludic deps reports
widest_reach beside widest_function, with how many of those states the function does not take
(Maroon Lake: app_boot, 72, all 72 through fn values). --widest N lists the N functions that take the
most states with what each reaches; --reach N orders them by reach. A baseline without widest_reach
does not hold it until rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 06:24:46 +03:00
2eefae0618 fix(check): 0.R4 - a misspelled type in a parameter, a result or a field is refused where it is written
`function kind_of(f: CharFact)` for a CharacterFact was taken on trust and failed in the code writer
as "member access on non-aggregate". A capitalised type - plain, in a slice, or a generic's argument -
must name a declared property, record, state, event, enum, action or packed value type, or a type
parameter of its declaration: `kind_of's parameter f: there is no type CharFact`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 05:58:35 +03:00
c35481f344 fix(migrate): 0.R4 - --prune keeps a state declared after a plain parameter; a parameter named twice is refused
On Maroon Lake the prune gave home_keep_records(base_app_st, home_st, r: RunRecords, save_app_st) a
second save_app_st at the front, and every call a second argument: a state declared after a plain
parameter was not counted as declared. It is now, and a call to such a function is never given the
state again. `ludic build --check` let the duplicate through and clang refused it; the checker now
refuses a function that names two parameters alike (`add names two parameters n`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 05:32:08 +03:00
71735b10a2 feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune
ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v),
q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding
facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)).

ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no
longer uses, nor anything it calls, and the argument that fills it - including an argument for a
parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is
told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would
make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:19:56 +03:00
8cf4b3fed6 fix(lang): the action drain is the program's; --check runs every check a build makes; a registry key named count is refused; name lookups are tables
- the function that calls every reducer (and the action queue) is written in the program's own
  file: in the first action's file it belonged to that module, depended on every module with a
  reducer, and joined a game's modules into one 69-module cycle (examples/actions/modules and a
  ludic deps case hold it); the state instances, the queue and the reducers make no deps edges
- ludicc --check / ludic build --check lower the program too and write nothing, so the code
  writer's refusals are in it: a bind to a function that is gone, an unknown name (and the checker
  now refuses fn <missing> itself); rejects bind_missing_fn, unknown_name, registry_count_key
- def R count is refused: its constant would be PREFIX_COUNT, the registry's size
- a file's module, package, trust and numbers-float are tables, and from the check on the lookups
  of functions, enums, records, globals and externs are too (tagged enums kept as a list): Maroon
  Lake's check-only build went from about 20 s to 7 s including lowering, its IR from about 2
  minutes to under 10 s; duplicate declarations are found by table, not a pair of loops
- threads.ludic's pool check gives each call a little work, so a busy machine cannot run them all
  on the caller before a worker wakes (it failed one run in three under load)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:34:43 +03:00
2fdefa6040 fix(test): expect_eq on strings compares their text and prints both on a failure
It lowered to an i32 compare of two pointers, which the IR refused. Two strings with the same text
are equal now, a null only to a null, and a failure says expect_eq failed (got "camp", want
"lake"). examples/library/testing_strings.ludic (two tests fail on purpose, and the output is
checked); ludic.base's actions_test uses it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:23:38 +03:00
55c1de8734 feat(cli): 0.R2 - ludic deps reports the widest function and ratchets it
widest_function: the most states any function or entry point of the program's own takes, with
which one; --check holds it like the other numbers and --baseline writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:13:46 +03:00
808c4a6f7a feat(lang): 0.R1 - actions and reducers
action Name { fields } is a typed record; reducer State on Action(s: mut State, a: Action) { ... }
in the module that owns the state takes exactly that state and the action (a second state is
refused); dispatch Action { fields } queues one from anywhere, the queue supplied by the runtime.
The queue is drained at the end of every phase of the frame loop, after every phase of ludic.base's
core_tick_all, and by drain_actions(): in dispatch order, each action's reducers in the order of
their states' names, an action a reducer dispatches queued behind, a queue still growing after 64
rounds stopped with the action named. Examples actions/pack, phases, runaway; rejects for a second
state, a reducer on a non-action and an unknown dispatch; ludic.base's actions_test; LANGUAGE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:13:46 +03:00
ce80e64b24 feat(lang): 0.S - components take states in their header; migrate writes them, never inside a name, never outside the programs given, never into a package's state
- component Name (a: mut A, b: B) { ... }: every getter, default, function and event takes the
  header's states before the instance; a member's call to another passes them on; the glue is
  supplied them; the template never sees them; a read-only one is read-only in every member
- ludic migrate state: a component's members' needs go into its header (added to an existing one,
  mut added where now changed); a field read or a member call inside a component is the compiler's,
  so nothing is written inside a name and no ', )' is left; an entry point that declares states
  already gets the rest after them
- a program's module named like a package gets <Name>AppState; a program's own file its own state;
  a friend module's files go by directory; a package's settable var stays state
- it writes only under the programs and directories given (and runtime/ with --runtime), and
  refuses the whole run naming any other file that would have to change
- a name a package already moved into its state is rewritten through it; a read of the runtime's
  var through the runtime function that answers it (gl_w: gl_width())
- a state's instance supplied by the runtime is not a uses reference
- tests: state/component, rejected/state_component_ro, rendering/ui_render3d, migrate component
  and foreign cases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:11:15 +03:00
c167ecc714 feat(lang): 0.S - a reference out of a read-only state is read-only (named so), a program's type named like the runtime's is refused (PadButton), tests for both, the migrate case covers lets; changeset
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:17:03 +03:00
5ffe50ed02 wip(0.S2): migrate - a var nothing writes becomes a let; a state is keyed by its module, directory or program, so every program's plan agrees; paths normalized; program states named SceneDemoState / scene_demo_st; the LSP reads state, mut and entry/handler parameters
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
d490d4f9f1 wip(0.S2): a migration makes a declared read-only state mut where it is now changed; net_sync builds --unsafe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:02:45 +03:00
e7f8ee6b91 wip(0.S3): ui blocks, scene on enter/exit, hooks, machines over a state's field, namespace and engine-system injection; a reference out of a read-only state or a module let is read-only; deps counts writes into another module's state; the rejected examples hold states
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:49:03 +03:00
07505e7ef2 wip(0.S3): the packages migrated by ludic migrate state packages - every package test green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:02:21 +03:00