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>
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>
lp_os_platform and lp_os_arch malloc'd 8 KB per call (the uname buffer) and kept none of it:
string_temps now asks both every round, 327 MB over 20,000 before, 0 after. Reseeded. render3d:
MoltenVK made a mapped buffer's MTLBuffer at its first bind (fn_gvk_draw +4 blocks in the boat
window); gvk_buf_reserve queues it and the next frame's command buffer copies 4 bytes out of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vk.heap_bytes pulled the Vk module - and on lang/uifree the GL window path - into a plain program,
which then failed to link (_cgl_offscreen, lgl_GetError). Os.heap_bytes is malloc_zone_statistics
through a weak reference (0 where there is none, and on Windows). Reseeded. Docs for it and for
Json.free / Json.free_all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s[a .. b] is always a copy, so it is freed once a +, a comparison or print has read it. Reseeded.
string_temps.ludic adds a slice compared and a slice concatenated each round (960 KB over 20,000
before, 0 after) and a kept slice read after its +.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
dispatch lowered to ludic_act_push(k, new A { ... }): a fresh record every dispatch, and an input
system dispatches Move and FrameTime every frame. The queue now keeps a list per action
(kept<k>, used<k>); ludic_act_new__A hands out the next (made only when all are queued), new's
emitter fills it field by field as it fills a fresh one (every field, default or given), and
drain_actions sets every used<k> back to 0 once the queue is empty. A reducer only ever reads
its action during the drain, so nothing sees a record after it is reused. Actions are visible
to the program's file, where the queue lives, as reducers already were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every per-entity store (@S_ components, @H_ flags, alive, kind, freelist, owners) is a heap block
L_grow doubles from 1024 as L_alloc hands out a slot past it, the new slots zeroed; each site loads
the store's base where it indexes it (ecs_base, its registers %ecsb* so a raw function's t0 labels
cannot collide). Prop.has bounds against @L_cap, Pool.capacity answers it, a mod's registered
stores grow with the rest, every main grows the stores once before anything reads them. A snapshot
records its slot count first and a load grows to it before reading back. The overflow stop of
1c7ce84 is gone with the wall. ludic-dev test 305 passed, selfhost-test 33 passed; 1000 / 5000 /
100000 entities spawn and count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L_alloc handed out a fresh slot without a bound, so the 1025th spawn wrote past the end of every
component array. It stops with a located message naming the store's size and where many things
belong (ludic.base's Table). Growable stores are plan 24.8: the save, rollback snapshot and mod
table write the stores whole at a compile-time size, so that is a file-format change. Reseeded;
ludic-dev test 305 passed, selfhost-test 33 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every pool thread runs the worker at once with the same states, so a mut state in a worker was a
race nothing reported. check_worker_ref refuses a worker whose leading states include a mut one;
threads.ludic's total moves into the words the worker is handed, under the mutex. The seeds are
regenerated (ludic-dev reseed). ludic-dev test 305 passed, selfhost-test 33 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
- 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>
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>
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>
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>
- 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>
A local bound straight from another module's global (let t =
thing_cur), or from such a local, is followed within its function, and
a write through its field or element is listed as a warning after the
counted writes. A reference from a function's result is not followed.
Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parse, the types and the module rules (export, uses, layers, ports,
registries) run and nothing is emitted or linked: about three seconds
on Maroon Lake. In this mode the checker asks vis_check at each
reference it resolves, with a global's initializer and a registry's
entries seen from the files the emitter would use. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bind Purse { money: g_money } writes the getter bind_Purse_money in the
bind's own file, so a one-line wrapper per member is not needed; a
member that takes something is refused a variable. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>