Commit graph

230 commits

Author SHA1 Message Date
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
1e8b5b0523 wip(0.S1, 0.S2): state records, mut and read-only state parameters, entry injection, module var refused; ludic migrate state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:45:09 +03:00
63a1fa1378 feat(cli): ludic deps --writes warns about writes through a local alias
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>
2026-09-25 13:22:37 +03:00
8892f51096 feat(cli): ludic build --check / ludicc --check - check without building
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>
2026-09-25 13:13:42 +03:00
7ef6c7ec75 feat(lang): bind a port member that takes nothing to a variable
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>
2026-09-25 13:02:01 +03:00
7b8c134c21 feat(lang): layers - module flow in layer app uses base, items
The modules of one layer use each other freely and may go round;
anything outside the layer is held to the module's uses, and a layered
module with no uses reaches nothing outside it. The cycle check walks
the graph with each layer as one node, so a cycle leaving a layer is
refused. ludic deps shows the layers and counts the largest cycle
without their own edges. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:54:15 +03:00
e3219f17dd feat(test): ludic test -j N runs tests side by side
Every file is compiled, linked and every test block run as up to N jobs
at once (xargs -P; default the CPU count), each test with a TMPDIR of
its own, and the report is read back in file order. Windows keeps the
sequential path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:38:09 +03:00
e738741521 feat(cli): ludic deps - the module graph as the compiler resolved it
With LUDIC_DEPS=<file> the compiler records every reference the
visibility pass resolves (from module, to module) and every assignment
to another module's global. ludic deps prints the modules,
dependencies, largest cycle, cross writes and globals written from
outside, with --graph, --dot, --writes, --uses MOD, --check FILE and
--baseline FILE. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:16:34 +03:00
69352babfd feat(lang): a module's private records and events are its own
A property or event a module does not export no longer collides with
another module's of the same spelling: each private one is renamed for
its module, with the types, new, emit and @On written in that module.
Exported ones stay one namespace; two events of one spelling are now
refused. Generic records, entity components and component or view
records stay global. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:02:48 +03:00
afa4d23a4f fix(lang): a template inside another's hole, and integer literals past 2^31 - 1
A template literal nested in a {...} hole crashed the parser: the outer
literal ended at the inner backtick. The lexer (and ludic-fmt's) now
reads a hole as code, taking strings, chars and templates in it whole.
A decimal literal past 2147483647, or a hex one of more than eight
digits, was wrapped into a negative int; it is a long with its value
now, and giving one to an int is refused. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:10:46 +03:00
b401f7acc1 feat(ludic.lab): the visual lab as a package - a scene on a plate, never a world
A scene is an entry in the open registry LabScenes, shown on a plate: a
flat lit disc, the package's own small sky with the sun at one hour, and
a frame clock. Headless each camera (lab_shot, lab_shot_at) settles and
is written as build/lab/<scene>/<shot>.png (a stored PNG, written here);
in a window N and P step through them. tools/lab/run.sh and sheet.py
make the contact sheet; tools/lab/plate_build.py makes plate/.
ludic.render3d gains r3d_plate_mode (no terrain, grass or water is made)
and r3d_sky_path. quit() in a program with no frame loop links. The
example takes ~120 MB resident, 417 MiB peak footprint. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:48:43 +03:00
e146c7a337 docs(packages): an index of the packages; the pack skips what is not a package directory
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:22:05 +03:00
a5f52c4581 feat(lang): def REGISTRY from "file.lres" - a game fills an open registry from its own file
The entries are read and checked against the registry's record as a
registry ... from file is, errors at the resource file's line, and they
are defs of the module that wrote the line: the registry must be open to
it, and they take that module's place in the stable order. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:22:05 +03:00
a09b622a1d feat(lang): a module's private names are its own
A private function, var or const no longer collides with the same
spelling in another module, in no module or in the runtime: where two
meet, each private one is renamed for its module (seed$shop), with every
reference its module writes that no local shadows. Exported names stay
one namespace. Nothing is renamed without a clash. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:08:05 +03:00
f4062010e9 feat(lang): packages count under uses; a port of defaults may go unbound
A module that says uses must name every package module it reaches; a
package with no module line is named for its directory (ludic_render3d)
for this rule, so a mechanic reaching into the renderer is caught. Only
the engine's runtime needs no naming. 'module x uses' with nothing after
it reaches no other module. A port whose every member has a default
answers with its defaults when unbound, and 'new' of a port says to bind
it. ludic-dev test runs 'ludic test packages'. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:36:11 +03:00
73129b59d5 fix(lang): a function named like an engine namespace method's target is refused
Random.range is rng_range, so a package's own rng_range(a, b, c) took
every Random.range call silently. Where the program calls such a method
and the target resolves to a function of its own, the function is
refused, naming the namespace method and the call. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:14:45 +03:00
31d842fca4 fix(test): expect_eq and expect_near compare floats as floats
On a float or double they emitted an i32 compare and clang refused the
IR; they compare as the wider float kind now and a failure prints the
numbers with %g. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:06:15 +03:00
f5e7fe4245 fix(runtime): Random.* in any program; value_* called directly brings the value tree
The seeded random numbers lived in the ECS runtime, so a tool or a
program of test blocks got "unknown function rng_range". They are
runtime/native/rng.ludic now, spliced on Random.* or a bare rng_*/seed
call nobody defines, and imported by core.ludic for a game. A bare
value_*/json_* call nothing in the program defines splices the value
tree the way Value.* does. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:59:48 +03:00
a2012a5afe fix(test): a generic function called from a test block works
Test blocks were never type-checked, and the checker is what makes a
generic call real; they are checked like entry now. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:46:08 +03:00
9faaa2094d feat(test): ludic test <dir>, each test block in a process of its own
A directory stands for its *_test.ludic files and the files straight
inside any tests/ under it. The runner answers --list and runs one test
by name, and ludic test runs every block in a child process, so tests
no longer share globals. A failed expect names the file it is written
in (the path the compiler was given) and its line. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:39:55 +03:00
6cfaaf0bf9 feat(lang): open registry - other modules' defs, in an order imports cannot change
A def from another module into a registry that is not open is refused,
and defs go through visibility (the registry exported, its module in the
definer's uses). Index order: the declaring module's entries, then the
other modules' by module name, each in reading order. A registry entry
is emitted as its own file's code, so what it names is seen from its
own module. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:28:34 +03:00
b1ccaf08bd feat(lang): ports - port Clock { ... } in a module, bind Clock { ... } in the app
A port is a record of function values a module calls through
(Clock.now()) without naming the module that answers. A port used and
never bound, a bind missing a required member or naming one the port
lacks, and a second bind are refused; the binder must see the port, and
what it binds is checked from its own file. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:20:12 +03:00
70d05bd504 feat(lang): friend module lab of fishing, data - a friend of the modules it names
A scoped friend sees those modules' private names and only the exports
of every other; a plain friend module still sees everything. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:12:47 +03:00
f4533331e7 feat(lang): module NAME uses A, B - a module reaches only the modules it names
A reference from a module that declares uses into a module it does not
name is refused, exported or not, naming the use and the fix. A module
with no uses clause keeps the old rule; a package's module is always
usable; a friend is not held to it; a cycle in the declared graph is
refused; LUDIC_VIS_REPORT=1 lists the violations as uses: lines. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:06:45 +03:00
ec9a650e65 feat(ui): ui_scale and ui_box, on-submit, zoom (and a length over a length in calc), on-hold
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:31:38 +03:00
3d2a103dfb fix(lang): a function named like a compiler built-in is refused; reseed
A program's own `run` was never called: every call to it lowered to the
built-in System.run (C's system()), and clang failed on the IR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:09:29 +03:00
ae61e99e06 test(ui): a component with no stylesheet reads the theme's :root variables
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:07:05 +03:00
077b38e684 feat(ui): a title shows for the keyboard's focus as well as the pointer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:06:27 +03:00
d15cc79822 feat(ui): a key field takes a mouse button, and is :capturing while it listens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:04:47 +03:00
6bd3228990 feat(ui): translate="no", and a title of several lines translated whole or line by line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:03:49 +03:00
7605a0253c feat(ui): linear-gradient backgrounds, object-fit and aspect-ratio
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:02:49 +03:00
abf45ef9db feat(ui): scroll-top holds a scroll box at an offset, on-scroll, and ui_scroll_set
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:00:21 +03:00
cf8b9e425e feat(ui): nine-slice corners clamped to half the box in each direction, cut on whole pixels
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:59:26 +03:00
12f4ba8431 feat(ui): min(), max() and clamp(); insets as percentages of the containing block
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:53:44 +03:00
355201f094 feat(ui): text-fit, line-height, and ems against the font size an element ends with
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:51:41 +03:00
d163bad406 feat(ui): a popover's align along its side, and within= for the bounds it flips against
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:48:58 +03:00
0aad2294ec feat(ui): pointer events for natives and elements, with capture; on-down and on-up; hits in painting order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:48:02 +03:00
d2b0413c30 feat(ui): the gamepad moves the focus, presses and goes back; a held direction repeats on the clock
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:42:32 +03:00
a4c7a756f0 feat(ui): a scrollbar held where it was taken, a popover that keeps the mouse, easing that lands
- the scrollbar is .ui-scrollbar with a .ui-thumb, styled by the default sheet or a theme; a press
  on the thumb holds it at the point it was taken and the pointer moves it in proportion, a press on
  the track jumps the thumb's middle there and holds it, and neither presses what is under the bar
  (it used to centre the thumb on the pointer and press the row beneath as well);
- while a popover is up only scroll boxes inside it take the pointer, and a press outside it that
  closes it forgets any press in progress;
- a transition ends exactly on its value (it stopped a rounding error short, at every frame rate).

Tests: ui_popover_mouse, ui_scrollbar, ui_ease (60/120/180/240 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:39:28 +03:00
9f59000f5b feat(ui): number and key fields, notes, anchored popovers, text-shadow, and pictures as painted
- <input type="number" min max step>: typed digits replace its value (a minus when min allows one,
  a point when step has a fraction), Enter or leaving the field commits them held between min and
  max, the left and right arrows step it.
- <input type="key">: Enter or a click starts it listening and the next key is its value, Tab, the
  arrows and Enter included; Esc stops it listening, Backspace clears it, `shown` names the value,
  and ui_capturing() tells the host to leave its keys alone (through the frame that ended it).
- note="..." under any control's label (.ui-labels > .ui-label + .ui-note); a range's (or number's)
  value with decimals, format="percent" and a unit.
- A popover with `anchor="id"` (the nearest element of that id) or a bare `anchor` (the element
  before it) sits beside it by `placement` (right, left, bottom, top), its margin the gap, opens to
  the other side where it would leave the screen, and is kept on it.
- A tooltip's title splits into lines at a newline or a written \n.
- text-shadow, inherited, drawn sharp under the text.
- ui_opacity(): the group opacity a native's draw is at.
- border-image is drawn as painted with no background colour, tinted by one, and not at all when
  that colour is transparent (it drew a white nine-slice).
- A picture file is drawn untinted, as a browser draws one; an atlas cell (prefix:name) still takes
  the color around it, and an <img> with its own color is tinted by it.
- The render3d backend loads a picture again when its file changes or appears (a failed load was
  kept for ever), and at once after ui_image_reload(path); takes a path with a drive letter (C:/...)
  as a path rather than an atlas; and slices a nine-slice by its texture's own width and height.
- ui_inputs.ludic and ui_look.ludic; LANGUAGE.md and the changeset say all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:55:20 +03:00
7d8f34de17 feat(ui): flex-shrink, text that wraps beside its siblings, calc(), order, and 0 as a size
- flex-shrink: a line whose children want more room than it has takes it back from those that
  shrink, in proportion to shrink times size and never below a min size, a fixed size or the
  content. A scroll box shrinks (and scrolls), and a box in a column shrinks as far as the scroll
  boxes inside it let it, so a list in a column takes the room its siblings leave with no height.
  `flex: grow shrink`, and `flex-shrink` by name.
- A row wider than its room measures its texts (and boxes without a width) again in the room the
  rest leave them, so a line wraps beside an icon.
- calc() with + - * / and brackets over px, %, em, rem, vw, vh, plain numbers and var(); a width
  or height keeps its percentage, taken of the room it is given. Lengths lists (padding, margin)
  keep a calc()'s spaces.
- `order` places a box's children without touching the tree.
- width: 0 and height: 0 are 0: an unset size is UI_AUTO now, not 0.
- A component root that is itself a component takes each user's class, style and id in turn (the
  outermost's id wins), and the rules of all their sheets are weighed by specificity together; a
  user's rule wins a tie. The parent's sheet used to override the child's whatever its specificity,
  and a middle component's sheet was lost.
- The default sheet lays .ui-track out as a row, so a range's fill is as tall as its track and a
  checked box's knob goes right; a range's thumb is centred on the fill's end.
- ui_boxes.ludic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:54:14 +03:00
0a7525b125 feat(ui): popovers, tooltips, progress; the render3d backend installs itself; duplicate names caught first
- `popover`: a top layer that keeps the pointer and the keyboard, closed by a press outside or Esc.
- `title` tooltips after half a second's rest, styled by .ui-tooltip.
- `<progress>` and `<meter>`.
- Atlases take rows and number cells, and importing ludic.ui/render3d.ludic is enough to draw with
  render3d.
- The compiler reports two declarations of one name before it type-checks, so a package global
  clashing with a program's reads as that, not as 29 type errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:41:02 +03:00
1dad96102a feat(ui): ludic.ui is a UI framework - input, controls, render3d backend, the CSS a game needs
A game no longer writes a host:
- The runtime reads Input itself: focus and keyboard navigation (Tab, arrows, Enter/Space,
  autofocus), the pointer (hover, :active, click on release, drag), and scroll boxes with the
  wheel, a draggable scrollbar, clipping and scroll-into-view.
- HTML's controls are built in (button, checkbox, radio, range, select, text, key capture), made
  of plain parts a stylesheet styles, each reporting with on-change and event.value.
- ludic.ui/render3d.ludic draws with render3d's overlay: textures, named atlases (icon:NAME),
  nine-slice border-image, rounded rects and rings, clipping, and a scale.
- Hooks for the program's language, sounds and clock (ui_translator, ui_sounds, ui_clock).
- ui_dev: hot reload, errors on screen, LUDIC_UI_DUMP.
- CSS:
  - colours as #rgb / #rrggbbaa / rgb() / rgba() / names;
  - border-radius and outline (following the radius), box-shadow, background-image and a tinted
    border-image;
  - group opacity, @keyframes / animation, transition;
  - :focus, :focus-visible and :focus-within.
- HTML mixed content, and boolean attributes.
- render3d gains tex_width / tex_height, and the XML reader keeps text runs in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:26:09 +03:00
20d011f8e9 feat(lang): UI components as files - component Name { ... } beside Name.xml and Name.lss
The compiler turns a component declaration into:
- a record of its props and state;
- a constructor, a props setter, a model and a call;
- a class, registered with ludic.ui at start.

Its template and styles are read from beside it and compiled in, with @import inlined, and a
missing template fails the build.

In the runtime:
- each mounted instance keeps its own props and state, and `set` in a template writes the state;
- styles are scoped to the component;
- class, style and id on a component's tag land on its root, styled by the parent's sheet too;
- ui_reload re-reads a component's files from disk and keeps instances.

The package's own module is now ludic_ui, so a program may call a directory of its own ui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:50:12 +03:00
637d07400e feat(ui): ludic.ui grows toward HTML, CSS and React (Phase 0.U1-U4)
- Natives: ui_native(tag, measure, draw) makes an element the program draws itself; ui_fire
  runs its on-<event> with event.value. input, select and textarea have simple defaults.
- React:
  - keyed <each>, <let>, <provide> context through components, <fragment>;
  - named slots, default props on <component>;
  - on-mount / on-unmount;
  - inline text inside text elements.
- CSS:
  - custom properties and var();
  - position relative/absolute/fixed with insets and z-index;
  - em/rem/vw/vh and @media;
  - wrapping text and ellipsis, overflow;
  - + and ~ combinators, :nth-child(an+b), :checked, :active.
- Developing: errors with file:line, ui_errors(), ui_reload() keeping state, and an
  inspector-style ui_dump.
- view fields infer the type of a literal or a named function's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:17:25 +03:00
66a2bc2214 feat(lang): L11 views and templates - the UI is markup, not code
A `view Name { field = x; function q(..); on e(..) }` declaration is the one bridge between a
program and its UI: it writes view_<name>() -> UiView, whose model is a Value of every field and
whose call runs a query or an event by name.

ludic.ui is a template runtime:
- HTML-shaped XML screens and components, loaded at run time;
- {expression} bindings, if/else/each, props, slots, per-instance state;
- on-press / onclick actions (event, set, emit);
- component libraries (export="true", <import src as>).

Styling:
- stylesheets in <style> or importable .lss files (@import);
- CSS selectors (#id, .class, [attr=v], descendant and > combinators, :hover, :disabled,
  :first-child, :last-child, :nth-child, :not) weighed by specificity;
- the box model and flex under CSS's property names.

Also:
- default parameters, and positional-then-named calls;
- Value gains a float kind;
- a function shadowing a runtime one is refused;
- an index is evaluated before the slice is read;
- runtime errors name the right file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:58:47 +03:00
334469ef61 feat(tools): L10 ludic-fmt enforces a project's style
lint lines in package.ludic - one_statement, max_file_lines, max_function_lines,
max_comment_lines, max_header_lines, paths, baseline - checked by ludic-fmt --check
<files> and ludic-fmt --lint (the project), at the line; a baseline ratchet lets a
rule arrive in a codebase that breaks it (--init-baseline), lowered as it is fixed.
check-impl reads the alias declarations too (it had been blind to every namespace
L6 moved out of the compiler), and eight methods get their pages; a test holds the
formatter to keeping type arguments together (L5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 14:38:35 +03:00
e543525eb7 feat(lang): L9 resource files - registry ... from
registry NAME of RECORD [as PREFIX] from "file.lres" fills a registry from a data
file of key { field: value } entries, read at compile time with the record as its
schema: every entry is checked like a def, errors name the resource file's line,
nested records and lists of them are bare { } / [{ }] typed by their fields, and
values are expressions in the registry's module. The entries are compiled in, so
nothing parses at start-up and a build that succeeds has validated its resources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 14:00:20 +03:00