Commit graph

138 commits

Author SHA1 Message Date
e4265f5fdb feat(lang): 27.1 - a state machine as data: @Machine(Record.field) on a registry of transitions
A registry marked `@Machine(Deer.mood)` is the transitions of a machine over that enum field of the
records a state's Table<Deer> holds. Its record has from and to (the enum's variants), on: string (an
action's name, "" for a transition the tick asks), guard: fn(Row<Deer>, reads...) -> bool and
enter: fn(Row<Deer>, reads...) -> void; the states are the enum's variants and the start is the
field's default. The rows are data (an .lres or defs), the names the studio already edits.

Written by the compiler (machines.ludic, machines_write.ludic): for each action an `on` names, a row
reducer in the registry's file (named ..__machine__DeerSteps, so it sits beside the program's own
row reducer on the same action, after it): the row's state, the first transition from it on that
action whose guard passes, the field set, enter run - guards and enters called by name. When a row
leaves a state on a guard alone, `state DeerStepsMachine` (the kept row view) and
deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...), one transition a row a tick.
Nothing allocates.

The table is the whole machine: the field written anywhere else - an assignment, or a `machine`
block's become over it - is a type error (check_stmt.ludic, ck_machine_write). Guards and enters take
the row first, are the record's module's, keep a row reducer's rules (and may be handed the row);
a guard writes nothing through it. The graph is checked, each error at its row (in the .lres when
the rows are there): a state never reached from the start, a state with no way out, an `on` naming
no action or an action with no @Target, a self-transition with no guard, two ways out of a state on
one trigger behind an unguarded first. Also refused: @Machine off a registry, a field that is not a
plain enum with a default, a @Column field, no table (or two) of the record, a transitions record of
another shape, a machine outside its table's state's module.

ludic schema's code section gains `machines` (registry, record, field, enum, table, start, states,
actions, tick, module, at); ludic deps names a machine's reducer `reducer Deer in Herd.deer on Spook
(machine DeerSteps)`. vocab @Machine; docs annot-machine, kw-machine; LANGUAGE.md "A machine as
data"; examples actions/machine (+ deer_steps.lres) and ten rejects; test.ludic feat, reject and
schema cases (not run); changes/machines.md. Reseeded; bootstrap-cfree fixpoint holds (317642
lines); Maroon Lake's `ludic build --check` is clean against this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:37:37 +03:00
ab0b84b87e feat(lang): 27.3 - a reducer on a table's row: reducer T in S.table on A, @Target, Row<T>, @RowVerb, @Column
An action names one row of a ludic.base Table<T> by its handle, in a field marked @Target, and
`reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook)` runs once, for that row
alone (the table may sit down a path, S.w.tab). The drain resolves the handle (tb_row) and hands the
reducer a Row<T> - new in ludic.base: tb, row, h, rec - that the queue keeps, one per row reducer,
filled in place, so a targeted action allocates nothing; a stale handle runs nothing, and
LUDIC_ACTIONS_LOG=1 prints a line for it (@alloc_ok). Row reducers order among an action's by their
state's name, then the table's path.

Checked at compile time (actions_rows.ludic): the row reaches r.rec and r.h only - r.tb / r.row
refused, the view never assigned, stored, copied or handed on except to a @RowVerb (a function of
the record's own module taking Row<T> first; any other function taking a row is refused); a field
marked @Column (a table column mirrors it) is not written through r.rec; only the module owning the
state declares a row reducer; one @Target, an int, per action; the states between the row and the
action are read. `mut` is allowed on a Row<T> parameter.

ludic schema's code section gains row_reducers (record, table, state, action, target, predicted,
net, module, at) and row_verbs (name, record, module, at), and every action its target; row
reducers are left out of `reducers`. ludic deps and ludic-lsp name a row reducer
`reducer Deer in Herd.deer on Spook`. vocab: @Target, @Column, @RowVerb; docs/language pages;
LANGUAGE.md "A reducer on a row"; examples actions/rows and ten rejects; test.ludic feat, reject and
schema cases (not run); changes/row-reducers.md. Reseeded; bootstrap-cfree fixpoint holds (307497
lines); Maroon Lake's `ludic build --check` is clean against this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:14:59 +03:00
56762050fb i18n: a key used only in a listener counts as used - the key check walked prog but not the bodies kept beside it (@On listeners, the lifecycle hooks, tests, computed fields, scenes), so their keys were never checked against en.po and ludic schema listed them unused (the game's 15 itemuse.* keys, emitted through ItemPlace); they are walked now as privates.ludic walks them. examples/lang/i18n_listen and its schema case (added, not run); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 03:12:25 +03:00
25a57c099b i18n 26.9, error mode: English left is an error under a lang line (a template's words, a text attribute's, a choice that reads as words, a @Text row's English), ludic deps still counting english_left; hole counts and undescribed splits stay warnings. ludic.ui's own words are keys - ui_tk(ui_st, k"ui.right_click", plain) for the key field's Right / Middle / Left click and press a key..., its plain text for a program with no translator; the examples' en.po / tr.po carry ui.*, i18n_ui checks clean, rejected/i18n_keys/english_left holds the error, data_missing counts 2; LANGUAGE.md and the changeset; on lang/foundations 2288feeb; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:45:15 +03:00
26b986de5b parser: an attribute before export is kept - @ToClients export event E was a local event (the attributes read in front of export were dropped when the declaration was parsed afresh one call down), as were @Sync / @Owned / @Server / @On / @Public ... before export; and export @ToClients event was refused. Attributes and export now read in either order into one declaration, and a wrapper marks what it added exported (g_at_keep goes); examples/networking/net_export and three schema cases (added, not run); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:03:44 +03:00
10a394ae6c compiler: text keys below a row, trf padding and tr's cast (ECS's 26.4 gaps) - a @Text Key in a record nested in a row, or in each item of a list of them, is filled with its derived key <registry>.<row>.<field>.<i>.<field> as a top-level one is, and a @Text []Key a row leaves out takes <...>.0, .1, ... for as many as en.po has (so no .lres spells a key; the manifest and en.po are now read before the parse that fills the rows); field: null is no text; trf / trn's trailing "" literals are padding, not counted against the English's holes; and string(x) of a string or a Key is x itself to the escape analysis, no allocation site, so tr(key) passes arena strict (a template's lone hole still copies); examples/lang/i18n_nested and its feat_case (added, not run); LANGUAGE.md and the changeset; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:41:05 +03:00
d39a27a904 Merge lang/foundations (9391695) into lang/schemacode: LANGUAGE.md's command list keeps --stdin-file (R10), the code map in schema (R7) and ludic syntax (R8); selfhost/main.ludic carries all three flags; reseeded from the merged source, bootstrap-cfree fixpoint holds (298653 lines), syntax --check clean, no conflict markers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:21:47 +03:00
bfd99cdda1 compiler: ludic schema gains a "code" section (R7) - the code map from the compiler, so the studio's scan is only a fallback: modules (package, layer, uses and where, friend, files), states, actions, reducers, every dispatch, events (cancellable, net) with every emit and their @On listeners, ports (members with fn types, defaults, required) and binds (port, member, fn, value, at), handlers (phase, hook and target, @Public, @Server / @Predicted, @Queries with filters as written, scene and layer), models (@Owned, @Sync), prefabs, scenes (start, public, shows, lasts / then, loads, enter / exit, layers) and fn_refs - every fn name in code or a resource file with the slot it fills (registry field and row, port member or default, record field, emit field, call argument, assignment, let). Places are "file:line:col"; sorted by name, sites by place; schema_version stays 1 (additive). The parser records emits, fn refs, handlers and scenes' layers / lasts / loads for it (emit_schema_code.ludic, emit_schema_code_game.ludic); LANGUAGE.md and the changeset say so; schema_case lines added (not run); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:19:24 +03:00
3d65ec6def Merge commit 'c804c4b' into lang/foundations (R10: --stdin-file checks an unsaved buffer in place of a file the program reads); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:15:01 +03:00
c804c4b0ee ludicc / ludic build: --stdin-file <path> checks an unsaved buffer in place of its file (R10)
stdin is read once, whole, and read_file serves a copy of it wherever the program opens <path> -
the entry, an import through a barrel, a component's .xml / .lss, an .lres. Paths match after
normalising both ('/' separators, relative under $PWD, . / .. / // folded, case on Windows);
diagnostics keep the file's usual name, with the buffer's lines and columns. A <path> nothing
opens is one warning. On ludic build it implies --check. Reseeded; bootstrap-cfree fixpoint holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:10:04 +03:00
9f8aec3bc1 Merge lang/i18nkeys (27024de) into lang/syntax: the vocabulary gains the text keys - the Key type, @TextKey (a registry's), the k"..." and kn"..." literals - and @Text's doc; docs/language pages for Key and @TextKey; ludic-dev syntax rewrote the grammars; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:02:48 +03:00
73f7d0b7a3 ludicc --emit-syntax: the language's vocabulary as one table beside the parser
selfhost/frontend/vocab.ludic holds every keyword with its role (declaration,
modifier, statement, operator, constant), every declaration's form, the built-in
types and phases, every attribute with what it goes on, its arguments and a
one-line doc, the operators and the literal forms; `ludicc --emit-syntax` prints
it as JSON ("syntax_version": 1) and exits before reading any program. The
parser dispatches on words where it meets them, so the table is held to it from
the emitter's side: a keyword's "reserved" is is_reserved_word's answer, a phase
must pass is_phase_name and a field attribute listed as read must pass
at_field_known, or the emitter refuses to print. Reseeded (both seeds assemble;
bootstrap-cfree: out.ll == seed.ll).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:59:02 +03:00
9f9ac4a7ce compiler: text keys' follow-up (Master's rulings) - a Key in a template literal's hole is an error (the parser marks each hole's string(), TPL_HOLE; write trf(k"...", ...)), trn takes only a plural key kn"..." and tr / trf refuse one (with or without en.po), and a program with no lang line (a package test) uses key literals unchecked and silently - the warning stays for a lang line whose en.po is missing; rejected/i18n_keys gains key_hole (2) and key_rules (3), key_missing's plural moves to trn; LANGUAGE.md and the changeset say so; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:46:48 +03:00
4988adb170 compiler: a Key reaches a template as its marked text (a component's or a view's Key field, for t(expr)), and a derived key's message names it as one; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:38:03 +03:00
cc1ea0f4ae compiler: text keys (phase 26.2) - k"pause.resume" / kn"catch.count" of the builtin type Key (not a string, and no string one; == compares), its run-time value the key after a marker byte (1, or 2 for a plural); package.ludic's lang "assets/lang" en and a gettext .po reader; every key literal checked against en.po (a kn"" one wants a msgid_plural), trf / trn and a template's t('key', ...) against the English's holes, a component's template words and a @Text row's English as "English left" (warnings, english_left in ludic deps), a @Text Key field a compiled row leaves out filled with its derived key <registry>.<row>.<field> (or @TextKey's prefix; maps.<map>.<table>.<row>.<field> for a @PerMap row, checked), one English under several undescribed keys warned at en.po's line; the schema's "lang" (keys and their sites, unused, undescribed, split, and every other language's missing / fuzzy / extra); k"" in .lres data, @PerMap rows included (lres_key); ludic-fmt keeps a key literal whole; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:33:52 +03:00
af90a1a334 @Asset(kind, map): a path under each map's directory - ludicc --check looks for it in every map (a @PerMap row's in its own, a game-wide row's in all), unless @Asset(kind, map, optional); the schema marks it scope map; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:13:14 +03:00
49e32f9b47 chunked tables: a key is unique across its map and the slot's own, not interned - a slot keeps row i's key in its own buffer i, rewritten when the slot is refilled (interning ~92k map-unique keys as a player walks would fill the bounded intern table and keep them all); ludicc --check refuses a key written in two of a map's chunk files, naming both; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:43:20 +03:00
2f4860ae71 @PerMap and @Chunked(n) registries: rows read per map, a state and verbs written, every map checked
- @PerMap registry R of T from "file.lres" reads <maps root>/<map>/file.lres at run time
  (package.ludic's new `maps` line, default assets/maps); @Chunked(n) one file per chunk,
  {cx}/{cz} in its name. No as PREFIX, no constants, no def, never open.
- permap_gen: state R, r_load/_clear/_find/_path; chunked: RChunk, r_in/_out/_slot/_find/
  _clear/_path; a typed reset and fill per record over lres.ludic, rows, lists and list
  records pooled per table / chunk slot, @alloc_ok on what grows at high water.
- permap_consts: lres_consts__(), the program's int and float constants by name.
- permap_check: ludicc --check reads every map directory (fields, types, constants, fn,
  @OneOf/@Range/@Ref, cross-row @Ref keys in the same map, a key twice); --no-maps skips.
- @Ref(PerMapTable) is refused on a non-string field; the schema says scope/chunk per
  registry and scope map on such a Ref, and lists the canonical @Unit spellings; any other
  @Unit spelling is a warning naming the canonical one.
- reseeded (mac + win seeds; bootstrap-cfree out.ll == seed.ll).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:03:56 +03:00
dd25267502 templates: {s} with one string hole and nothing else is a new string, not s itself - it was the one template that passed its string through (string() does), and code that wrote it as a copy kept what it then freed; the mark rides the callee, since escape analysis writes the call's uns; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:14:58 +03:00
281ebc23e1 ludic schema: a components section and a natives list (R4)
Each UI component: module, place, doc, xml and lss paths, props and state
(type, default as written, place, doc), states_read (the header's states),
derived fields with their types, functions and events as the template calls
them (states and instance stripped), and the native tags its template uses.
natives: every ui_native / ui_native_input call with a literal tag - tag, via,
handler, place. schema_version stays 1; the lists are additions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:36:04 +03:00
81cb992fad attributes: @Material(field) resolved as @Node is; @OneOf on a string field takes words, and every registry row's value is checked against them - words on another field, or constants on a string, is an error; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:13:32 +03:00
d82dc3162f attributes: a target the program does not have at all (an @Ref registry, an @Tint / @OneOf constant) is a warning and "unresolved" in the schema, not an error - a package names the game's registry without importing it; a name of another kind is still an error; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:10:45 +03:00
dacc0f7280 Build.schema_hash(): FNV-1a 64 of the program's own schema (the bytes ludic schema prints), worked out only when a program names it, 0 under ludicc --release, which ludic bundle now passes (Mac and Windows); reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:05:28 +03:00
b5bffe99fa attributes: @Tint(SLOT) on a colour field, into the schema - SLOT must be a constant that exists (a row of the program's tint-slot registry); reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:51:27 +03:00
34c2ac2cb9 attributes: @Clip(field) resolved as @Node is, and @OneOf takes constants as well as a prefix - one argument ending in _ is a prefix, otherwise each is a constant that must exist; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:34:33 +03:00
ebfbabdfd8 attributes: @Node(field), @Derived, @Text, @Multiline and @Key on a field, into the schema - @Node's field must be @Asset("gltf") or an @Ref to a registry whose record has exactly one, every failure reported; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:31:17 +03:00
42deb76c28 schema: ludicc --emit-schema / ludic schema, --check --diagnostics=json, and editor attributes
--emit-schema FILE writes the compiler's resolved view once the program type-checks: every
record (fields, types, defaults as written, docs, places, attributes), every registry with its
entries in their final order after the open-registry merge (key, constant, index, file:line:col
of the entry and of each field value, and which file contributed which keys), every const, and
the zero-argument functions a fn value can name. Deterministic, schema_version 1; the runtime is
left out. `ludic schema [file] [-o FILE]` wraps it.

--check --diagnostics=json prints every error as one JSON array on stdout: the checker's and the
module rules' all, a parse or lowering error as the last. Tokens and nodes now carry a column.

Fields take several @attributes; @Ref(Registry), @OneOf(PREFIX_), @Range(lo, hi), @Unit("..."),
@Asset("..."), @Color on a field and @AppendOnly / @ByKey on a registry change nothing but go into
the schema, and @Ref naming no registry is an error (every one reported). Fixtures:
examples/lang/attributes.ludic, examples/rejected/ref_unknown.ludic, cases in ludic-dev test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:06:54 +03:00
a299117858 frame keeps: a model's records filled in place, kept event numbers, list spares, Json.read_file
From the fence's kept frames in 22 minutes of play:

- A component field that is a record or a list of records was `value_put(o, k, view_val_T(x))`,
  a whole new tree every frame (HudPrompt's notifications). view_fill.ludic generates view_set_T /
  view_set_list_T / view_fill_T that fill the object and list under the key in place.
- value_list_fit dropped the items it cut off and value_item made new ones as the list grew back,
  a Value per item per regrowth (value_item / value_set_strs); the cut-off items are now the
  list's spares (Val.spare), and an item of another kind is turned rather than replaced.
- ludic.ui: a scroll box's "scroll" and a slider's "change" fired a fresh Value.float a frame
  (sc_walk, scroll.ludic:36); ui_fire_float takes one from a ring kept with the state (fired.ludic).
  ui_object_fit_into is exported, for a draw that keeps its list.
- Json.read_file(path): read, parsed, and the file's text given back - Json.parse(Fs.read_text())
  kept the whole file on every read (Maroon Lake's settings peeks).

Golden value_list_regrow: a list alternating 6 and 2 items every frame keeps nothing (the toolchain
before this fails it: +128 B new Val from value_item). Game compiles; ludic.i18n/settings/hints/
base tests pass (ludic.ui has none); arena and fence goldens unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:22:13 +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
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
bef0d6fbca arena (25.3b): LOCAL sites allocate from the frame's scratch; dispatch is not an allocation; @frame by property
Behind ludicc --arena (or 'arena on' in the program's package.ludic): the escape analysis runs and a
LOCAL site's allocation raises @lp_want for that one call, so it comes from the frame's arena. Two
halves in one mmap reservation (R3D_ARENA_MB each, 256 by default), bump-allocated with a 16-byte
size header, flipped at each frame mark: a frame's scratch is good through the next frame, then its
half is started again (R3D_ARENA_CHECK=1 fills it with 0xDD first). The heap takes over when no frame
is running, off the main thread, or past the half's end; lp_free ignores an arena block and
lp_realloc copies one out. R3D_ARENA=0 turns it off at run time; the census reports each half's
high-water mark. A program that builds text, a list and a record per frame: 29998 heap blocks made
and 18002 freed without it, 8 and 8 with it and 544 bytes of scratch a frame, the same output.

A dispatch's 'new' fills the queue's kept record (E_NEW.b), so 25.2 no longer counts it and 25.3
treats its fields as kept. '@frame' is keyed by property and field: a 'run' field is a root only in
a property that marks it, and 'tick' stays a System's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:25:32 +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
1e2a6bab5b frame allocs (25.2): @frame on a step list's field, @alloc_ok on a statement, and on an exported function
A field declared '@frame run: fn(...)' makes every function stored in it a frame root, as a System's
tick is. @alloc_ok("why") before a statement takes that statement out of frame_allocs and makes what
it allocates declared at run time; a function holding one keeps the fence's scope depth and puts it
back at its return, so a return inside the statement cannot leave the scope open. @alloc_ok above
'export function' was lost - export parses the declaration one call down - and is now carried to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:56:32 +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
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
e8a34255ac Merge commit 'f104995' into lang/ecs 2026-09-28 02:02:11 +03:00
93bc8a90db compiler(0.R): a dispatched action's record is the queue's to keep - one list per action, filled in place, all free again when the drain ends
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>
2026-09-28 02:02:11 +03:00
4d8526ad7e ludic.ui, runtime, compiler: a component call's answer is written into a pooled record while the screen is built (call(p, name, args, into); value_into_*), a component root's passes pooled, an icon's atlas and name read in place; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:51:56 +03:00
fa119d234b ludic.ui, runtime, compiler: an expression's Value comes from the screen's pool while it is built (never a state's start or an action's), true and false shared, calc() terms pooled and read in place, a model's list fields filled in place (value_set_ints/strs/floats/bools); reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:05:35 +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
429eeb7754 ludic.ui, compiler: a component's model is filled in place into an object its instance keeps (value_set_*); colours, border-images and a class screen's stub made once; object-fit into a kept list - benchmark 20 -> 10 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:17:47 +03:00
e2626a0687 perf(compiler): the generated drain_actions allocates nothing - an empty queue returns at once, and a drained one is cleared in place instead of replaced by two new lists (seven drains a frame, never freed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:08:24 +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
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
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