Commit graph

170 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
d4df925b8f checker: a function named like a built-in a call always takes is refused at its declaration - function words(st, k) compiled and every call became words(n) with a pointer for n (invalid IR, far from the cause); the table is selfhost/check/check_builtins.ludic, emit_call's built-ins no find_fn guard lets a declared function take, and ludic-dev syntax --check holds it to emit_call both ways; every other built-in (buffer, floats, double, ...) yields to a declared function in the checker as it already did in codegen. string_temps' keep and cross_heap_test's keep renamed (hand, keep_cell); rejected/builtin_call_name and functions/builtin_yield with their cases (added, not run); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:10:24 +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
9a0011c38f compiler: a test program may hold a phase handler - the test runner called @rt_init whenever the program had systems, and a handler alone makes none (LLVM: use of undefined value '@rt_init'); it calls it only when it exists, as an entry program's main does; a ludic test case with a handler (added, not run); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:34:34 +03:00
eb0d3b1846 Merge lang/schemacode (R7, d39a27a) into lang/editortools, so R9 lands after R7 with nothing to resolve: no conflicts (LANGUAGE.md and test.ludic merged), selfhost/ is R7's (its seeds; bootstrap-cfree fixpoint holds)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:27:16 +03:00
50333571d7 Merge lang/foundations (9391695) into lang/editortools: no conflicts, selfhost/ identical to lang/foundations (the seeds unchanged; bootstrap-cfree fixpoint holds at 289532 lines), the help lists R10's --stdin-file, R8's syntax and R9's fmt / remove lines together
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:26:26 +03:00
047bdf4189 ludic fmt for editors: --lint --json, and a buffer on stdin (R9)
ludic-fmt --lint --json prints the violations --lint reports as one JSON array on stdout,
[{file, line, col, rule, message}] ordered by file, line and column (col where the rule knows it),
the summary on stderr, --lint's exit status, and never rewrites the baseline. ludic-fmt - refuses a
buffer that does not read as Ludic (a string/template/key literal left open, a bracket never closed
or closed by the wrong one) with exit 2 and name:line:col on stderr; - --lint judges a buffer as the
file --stdin-name names (--stdin-rel: that path relative to the project), against its baseline and
lint paths. ludic fmt --lint and ludic fmt - run it from the nearest package.ludic upwards (from
--stdin-name's directory when given). Hooks read nothing and write to stderr under --json or -.
Regression cases added to test-tools and ludic-dev test (fmt_editor_cases), not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:24:43 +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
4ccbc12b48 Merge commit '27024de' into lang/foundations 2026-09-30 00:00:28 +03:00
2ad6edaae3 ludic syntax, and ludic-dev syntax: every grammar written from the compiler's vocabulary and checked against it
`ludic syntax [--json] [-o FILE]` prints what `ludicc --emit-syntax` does (a line
per entry, or the JSON). `ludic-dev syntax` writes, between "ludic-dev syntax:
begin" / "end" lines, the keyword, type, phase and attribute tables of
ludic_syntax.h, LudicVocabulary's sets (JetBrains), ludic-mode.el's lists and the
language server's word tests (is_keyword_word and the rest; is_contextual_word is
every word the parser does not reserve, and every declaring or modifying one),
and every TextMate pattern marked "comment": "ludic-dev syntax: <group>" (shared
and the VS Code copy). The grammars gain module uses port bind action reducer
dispatch registry def open component prop view alias friend unsafe numbers of as
from mut system; import and extern colour as declarations; the phase clause
knows Overlay; the bitwise pattern matches | and ^ on their own again.

`ludic-dev syntax --check` - and check-vocabulary, whose old parser comparison it
replaces, and the regression suite (syntax_cases, one line per file) - fails when
a written list is behind, when a grammar lacks a keyword, type or phase, when
docs/language has no page for a keyword, type, phase or attribute, or when the
parser (a scan of selfhost/frontend: is_id / text == words, a == / ann ==
attributes) tests a word or reads an attribute vocab.ludic lacks, or the reverse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:59:30 +03:00
27024de00e ludic-dev test: the deps case expects english_left (26.2) after owned_leaks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:57:56 +03:00
cdaeb33e12 Merge commit '9f9ac4a' into lang/foundations (26.2 follow-up: a Key in a template hole refused, trn only with kn"", tr/trf refuse one); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:48:35 +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
e3788c0a43 Merge commit '66b8e51' into lang/foundations (phase 26.2: Key, k""/kn"", derived @Text keys, the en.po checks, english_left, the schema's lang section); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:41:54 +03:00
f9808e31ec examples: text keys - lang/i18n (k"" and kn"" keys, == on them, a @Text Key field's derived key, @TextKey, a component's t('key', ...) and its one line of English left, en.po with a plural and descriptions, tr.po missing, fuzzy and extra) and rejected/i18n_keys (a key en.po lacks, a plural key without msgid_plural, a Key / string mix-up, a derived key en.po lacks, a template's missing t() key, hole counts and an undescribed split); their cases in ludic-dev test (added, not run)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:38:03 +03:00
b5b3edc6ec ludic.ui: t(key, holes...) in a template - the key marked (byte 1, a marked key kept as it is) and each hole's value after a byte 31, the text the translator decodes at draw time; joined through the text memo, so nothing is made while it reads the same (phase 26)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:16:05 +03:00
82c652ca31 ludic.ui: an override's path is compared normalised, and an absolute path matches the relative one it ends with - a lab build registers its components as lab/../src/ui/..., so the studio's src/ui/... (or absolute) path matched nothing and the override changed nothing; nothing is normalised while no override stands; examples/library/ui_override_up registers under a .. path as the lab does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:00:12 +03:00
31dcc389a2 ludic.ui: ui_mounted(out) - the classes of the components the last frame showed, in tree order and each once, into the caller's list - and ui_model_of(cls), the first mounted instance's model read in place; examples/library/ui_mounted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:51:33 +03:00
aad3f8ca27 ludic.ui: ui_override(path, text), ui_override_clear(path | "") and ui_overridden(path) - a template's or a stylesheet's text given from outside, its component shown again with its state kept and its own text brought back when let go; lib_text answers a copy of the override, and the dev poll leaves an overridden component alone; examples/library/ui_override
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:48:48 +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
2c800ea84d Merge commit 'ec49207' into lang/foundations (deps: roots out of the reach ratchet); the seed regenerated from the merged compiler, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:56:57 +03:00
ec49207533 ludic deps: widest_reach and widest_write_reach leave out the ROOTS - a function that reads fn values out of a table (a step list's walker, a registry of systems) reaches every state by definition - and count every other function through its calls only, not through a dispatcher nor a fn value it writes into a list; the roots are listed on a line of their own and marked in --reach / --wreach; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:51:50 +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
d8baf4e579 tests: the full suite's seven failures on lang/foundations - one compiler fault and six stale expectations
- the test runner re-makes every state between tests again: since 12fdc07 a state is made by its getter on
  first use, so re-running L_init_globals left the last test's state in place (state/tested failed its
  second test); @L_reset_states forgets every lazy state, and the runner calls it before each test
- diag_json_case counts errors, and an absent @Ref / @Tint / @OneOf target is a warning since d82dc31:
  ref_unknown is 1 error and node_bad 8
- schema_hash.ludic prints 1: a bool is 1 or 0 as text (random_plain's 1 1 1)
- permap_check_case looks for the unit warning without the quotes the JSON escapes
- baked_test's inputs-hash test makes its directory: each test has a temp directory of its own
- ludic deps prints phase 25's five counters too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:19:00 +03:00
9ff219d065 Merge commit '4376ddf' into lang/foundations 2026-09-29 21:13:56 +03:00
4376ddf0ec Json.parse decodes \uXXXX to UTF-8 - a surrogate pair joined, a lone surrogate or bad hex as U+FFFD - and \t \r \b \f, where it dropped the backslash and kept the hex as text; examples/lang/json_unicode checks it byte by byte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:07:10 +03:00
5a751434ac tests: the map-scoped tables' example and rejected fixtures
examples/lang/permap (two maps, a chunked table with a negative, a positive and a
missing chunk, constants and fn by name, a nested list, a cross-row @Ref, a reload
shrinking in place, a bad file's file:line:col; built under arena strict with an @On
frame root), --check fixtures for a wrong field, a wrong type, an unknown constant and
a dangling cross-row @Ref, @Ref(PerMap) on an int, as PREFIX, and a @Unit warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:03:56 +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
b647964839 schema: every function a fn value can name, not only zero-argument ones - fn_type is the type a field sees with the states stripped, spelled as a field's type is (fn(A,B)->R), with params beside states; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:15:04 +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
3254d8e81e ludic bundle: app env "K=V" - the environment Launch Services starts the app with (LSEnvironment)
A switch a library reads only as a process starts - libmalloc's MallocLargeCache, which on a game
keeps ~200-300 MB of freed load buffers - has to be in the environment before main. For a .app started
from Finder, the Dock or `open` that is Info.plist's LSEnvironment; `app env` is repeatable and each
K=V becomes a string in it. bundle_case's probe app carries one and checks it with plutil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:00:29 +03:00
181d317d20 ludic.ui: <option disabled="{...}"> - a select steps past it, clamps off it and greys the arrow toward it
- The arrows, a press on the select and the pad step past a disabled option (ct_opt_step), wrapping
  as before.
- A value naming a disabled option is shown as the nearest enabled one (the lower of two as near) and
  set to it once the build is done: an event fired while the tree is built is cleared with the frame's,
  so the clamp is queued (ct_clamp_n / _i) and fired after nt_fired_clear.
- The < or > whose next option is disabled is drawn disabled (its part's :disabled), so a cycler shows
  where the options stop being on; it still steps past them.
- ct_build_select moves to controls_opts.ludic with the rest (controls_build.ludic 119 -> 104 lines).

Golden ui_select_disabled: options 3 and 4 disabled, a value of 4 clamps to 2, > is drawn disabled
there, > wraps to 0 and < from 0 steps back to 2. The 38 ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:01:43 +03:00
2958539514 ludic.ui: a new component instance's first build is declared - a first show mid-play is not a frame's keep
Master's windowed fog profile failed the fence at walk t 35 s: +512 B from value_new / value_put /
value_slot / value_lists under bd_class and cmp_keycap_model - a KeyCap first shown mid-walk.

It is a first build, bounded, not a per-show leak: an unmounted instance goes to in_free and the next
show of its class reuses it (in_reuse, `renew`), its props and model objects kept and filled in place.
Only a NEW instance makes them - bounded by how many of that class are up at once, and the ones
unmounted less than two builds ago. in_reuse already declared the record; the props and model objects
and their first fill, made afterwards in bd_class, were not. They are now: in_reuse marks a new record
`fresh`, and bd_class's first fill of it goes through bd_first_fill (@alloc_ok) - a reused instance's
fill stays judged, so a real per-show leak would still fail.

Golden ui_first_show: a component first shown at frame 700, once the fence is judging, then hidden and
shown twice more: bad 0 (the toolchain before this fails frame 701: +464 B value_new from value_slot
under cmp_cell_model, value_put grow - the profile's failure). The 37 ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:18:22 +03:00
3f685af868 Json.free_all is safe on a parsed tree a migration and a loader have edited; sv_str keeps a copy
A load freed only the parsed trip's nodes (Json.free) and kept every string the parser made, because
a loader might keep one; free_all freed every string and every key, so on a tree a migration had
added a literal to (`Value.put(v, "sver", Value.str("2"))`) it freed the literal and aborted.

- The parser marks the string values it makes (JP_OWNED, in the node's otherwise unused num) and
  interns object keys (a few names, never freed); free_all frees only marked strings, never keys,
  and a list's spares too. A setter that gives a marked node other text (value_set_str/_strs,
  value_into_str/_strs, value_become) frees the parser's text first. value_as_int / _as_float read a
  string as 0 as before.
- ludic.base sv_str returns intern(...): every package load that keeps a text read from a section
  (minimap labels, a Thing's look, photo tags and files, an effect's label, ...) holds its own copy.

Golden json_free_edited: parse, put a literal key and string in, set a string, keep an interned copy,
free_all - 1100 loads: the copy reads on and nothing grows (the toolchain before this aborts, 134).
Tests: ludic.save, base, settings, minimap, things, photo, effects; json_saves, value_list_regrow; the
36 ui examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:43:24 +03:00
9b3d3e3298 ludic.ui: the pointer looks through a box with no size; a control's parts answer :hover
From the user's play, reproduced on Maroon Lake's customisation screen with the pointer scripted over
each control (the lab's R3D_CLICKS): nothing on it was ever pressed or hovered.

- A screen's root <div> round a positioned panel (a component's root holding the kit Screen's
  modal) lays out to 0x0, and the hit test (fr_pick) and :hover (fr_under) only walked into a child
  whose box held the point - so nothing under it was reachable. A box with no size holds no point and
  clips nothing: both look through it now (fr_empty), without hovering it.
- A control's parts (a select's < and >, a range's thumb) are made by ct_part, which never set
  `hovered`: `select .ui-prev:hover` could not match anywhere. It is set as an element's is.

Golden ui_pointer_through: the panel's button pressed and hovered through the empty root, and a
select's > hovered (before this: pressed 0, hovered 0). The 36 ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:26:33 +03:00
e476a9d975 ludic.ui: emit click reaches on-click, and a select's < steps back
From the user's play: clicking an item in the pack opened nothing, and the wardrobe's picks did
not respond - both are a component whose button says `emit click` (ItemCell, LookCell, ListRow),
answered by its user's on-click. An on-* attribute's name is kept as a browser would have it, so
on-click is on-press (tpl_event), and the emit looked for "click" and found nothing. An emitted
name now goes through the same tpl_event.

And a settings cycler's left arrow did the right one's job: any press let go on a select stepped it
forward. ct_activate_at steps back when it is let go over the select's .ui-prev; Enter and the rest
of the select still step forward.

Goldens ui_emit_click (the mouse and a press both reach on-click; with the toolchain before this,
neither does) and ui_select_arrows (1 -> 0 on <, then 2 on > >; before this, 1 -> 2 on <). The 33
ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:32:45 +03:00