ludic.effects: effects_add / effects_add_after / effects_clear / effects_run,
effects_sum / effects_has / effects_at / effects_live, an EffectEnded queue
(ran out, pushed out by the bonus cap, crowded out of a full ring) and a
System with its own save section. Nine test blocks in tests/effects_test.ludic.
ludic.base: q_new collided with render3d's quaternion q_new the moment a game
imported both, so the queue constructor is queue_new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
A mechanic package depends on ludic.base and nothing else: ports (records of
function values for now) for questions, queues for facts, verbs for changes,
phases for order and its own versioned save section. The runner inits, resets,
saves and loads systems in the order added and ticks them phase by phase; a
missing save section is a reset. Tests for each piece and a worked route
between two toy mechanics live under tests/. The old ludic.core (engine ECS
components) is used by examples/library and stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
- 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>
- <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>
- 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>
- An action whose first word is `set` or `emit` followed by `(` is a call, so a component's
`on set(v: int)` is pressed as `set(4)`; `set x = ...` is still the action.
- A `string` prop (or state, or parameter) given a number in a template reads it as text through
ludic.ui's new `ui_val_text`; `label="{3}"` used to arrive as "".
- Two components of one name (or of names that differ only in case, which share their functions'
names) are an error at the second declaration that names the first's file and line, instead of a
"function cmp_x_def_y is defined twice".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `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>
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>
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>
- 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>
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>
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>
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>
registry NAME of RECORD [as PREFIX] is a global table; def NAME key { ... } in
any file is one entry, collected in source order and filled before any code
runs. Each entry gets an index constant (PREFIX_KEY), the table PREFIX_COUNT,
and a record with a key field gets it filled and a NAME_find(key). A record
literal naming a field its record lacks is now an error everywhere; a global's
initializer is lowered as its own file's code (its errors, and what its module
may see, were whichever statement came last).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.
What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`alias meth(labels) = target` in a namespace block makes Ns.meth a call to
target with those labels (the target's own parameter names without a list). The
engine's 41 table-driven namespaces - 438 methods: Http, Udp, Process, Json,
Value, Screen, Input, Audio, World, Tiled, ... - leave emit_ns_call for
runtime/native/namespaces.ludic, spliced into every program; 532 lines of
compiler go and the seed shrinks by 23k lines of IR. The game's IR is byte
identical. The checker checks an alias call's arguments against its target.
Still built in: the inline namespaces (Math, Text, List, Vector, Color, Time,
Date, ...) and the methods that pick a target by argument type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
property Pool<T> { ... }, function first<T>(xs: []T) -> T, map<T, U> over fn
types; a type writes an instance as Pool<Thing>, nested as deep as needed. The
parser names an instance Pool$Thing and remembers its generic and arguments; the
checker takes the generic declarations out, infers a call's type arguments from
its arguments or its result's declared slot, and makes each instance once as an
ordinary record or function, checked like any other. Errors print Pool<Thing>.
An instance keeps its generic's module and export (L3). ludic-fmt keeps type
arguments together while spacing comparisons and shifts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selfhost/check/ walks every function, the entry, tests, globals' initializers and
@On listeners with real scopes, and refuses mixed number kinds, text and numbers,
two record types, mismatched slices and fn types, wrong argument counts, wrong
returns and wrong push elements - every mix-up at once, each at its line.
LUDIC_CHECK_REPORT=1 lists them by category. pointer stays untyped (L7's).
What it found is fixed: render3d's HDR scan calling the float-bits extern f_lt
with floats; ludic.shooter's right-stick aim overflowing past half a push;
prof.ludic storing longs in []int; extern arguments now coerced to their
parameters. Text-returning runtime functions say string; Assets.ready says bool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A barrel's 'module NAME' makes its directory a module; a declaration other modules
use says 'export'. Private use from another module is an error naming the module
and where to mark it; 'friend module' sees everything (a test harness); a file in
no module is public and a package keeps its own module. LUDIC_VIS_REPORT=1 lists
every violation instead of stopping, so a codebase can be given its exports first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fn(int, float) -> bool is a type, fn name is any top-level function's value, and a call through
a local, a global, a record field, a slice element, a parameter or a result of a function type
is an indirect call; two function types mix only when equal, a call checks its argument count,
and a value may be null (examples/functions/values.ludic). Job.parallel_for keeps its worker
check.
render3d's scene is registered rather than required by name: r3d_on_draw, r3d_on_casters and
r3d_on_stream_fill (hooks.ludic). The two rendering examples register theirs - and had defined
scene_draw_casters with no parameter while the renderer passed one, which nothing checked.
render3d declares numbers float itself; smooth.ludic is converted to floats and returns when
r3d_init fails instead of running on into a segfault. Noise.* check their argument count (a call
one short crashed the compiler). selfhost-build says why it failed. The migration tool reads a
declared float as evidence. Seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L1. Two vars, consts, enums, properties or events of one name in a program are an error naming
both places (functions already were); the first used to win silently. A let / var of a name its
own block already declared is an error (it used to shadow). A function with a result type whose
body can reach its end without a return is an error, asked structurally of the body - a return,
an if/else or a match with a default arm whose every branch ends - rather than handing back
whatever the result slot held. The game, the lab and every package example pass all three;
the lab had one harmless shadow, and the game's split screens had two real bugs of the kind.
examples/rejected/ holds the four refusals, checked by the test runner's new reject_case.
Seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text.to_float reads a leading decimal number (strtod), the twin of Text.to_int, for data files.
A return with a value in a function that returns nothing now says so where it is, instead of
reaching clang as 'store void'. Seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Listeners are compiled into one @ev_<E> function and return branched to its exit, so every
listener declared after one that returned early - and every foreign listener - never heard
the event. The per-kind listener shape (return unless it is my kind) answered only the
first-declared kind. examples/events/answer.ludic holds it; seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.
render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Input.text, App.monitor_count / window_to_monitor / window_fixed,
gl_sleep_us, and the grass and collide changes, uncommitted on main and
depended on by the game; carried here so the language work starts from
what the game actually uses. main's working tree is untouched.
`reseed` writes both, and the committed Windows seed carried none of the bounds
check (0 sites against 859 in the regenerated one), so a bootstrap there would
have built a compiler without it - the same half-a-change as the host seed, on
the other platform.
Not verified by running: a Windows bootstrap cannot be done from this Mac. What
is checked is that it is generated by the same `reseed` from the same source as
the host seed, and that it carries the emission the host seed does. The host's
own bootstrap is a proven fixpoint and selfhost-test's C-free bootstrap passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0998cb5 committed the backend source and not the IR seed it is built from, so
`ludic-dev build` on a clean checkout rebuilt the compiler from a seed that
predates the change and produced one with no bounds checking at all. The check
only existed in whichever binary happened to be sitting in bin/.
That is the same half-a-change this project has just been bitten by twice - a
caller committed without the definition it needs, a table's declaration moved
without its parameters. A compiler change is the source AND the seed.
seedcheck.ludic:5: index out of range: 5, len 1
from a compiler bootstrapped out of the checked-in seed, which is the thing
that was not true before.
bootstrap: FIXPOINT (gen2.ll == gen3.ll). selfhost-test: 33 passed, 0 failed,
including the C-free bootstrap from the seed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A slice has carried { ptr, len, cap } since it existed and nothing ever read
the len: every index emitted a bare getelementptr. Running off the end of one
wrote into whatever the allocator had put next, and the program died somewhere
else entirely - a maroon-lake crash took a day to find because the stack named
a texture upload and the write was in a telemetry buffer five frames earlier.
Now each index loads the length and compares unsigned, which rejects a negative
index in the same instruction, and a failure aborts with the location the other
located errors use:
oob.ludic:9: index out of range: 7, len 3
`words` and the other raw buffers are unchanged - they are a bare malloc with no
length to check, which is the argument for moving off them.
The SPIR-V variants are regenerated in the same commit: shaders --check was
failing against the edited GLSL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
scrollingDeltaY is a double and the Cocoa event pump truncated it to an int per EVENT
before accumulating. A trackpad or a Magic Mouse sends a stream of fractions of a line,
every one of which truncated to zero, so the wheel was dead; a notched mouse sends three
to ten lines at once, so it jumped. The fraction is accumulated now, a precise delta is
scaled from points to notches, and the remainder carries to the next frame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
layer_flutter's mask was the vertex's height alone, so on a ten metre aspen every part of
the trunk above 1.2 m trembled with the leaves and the bole read as cloth. What separates
a leaf from a bole is distance from the model's centre line, not height; the mask is
radial now, and small plants keep the old one because a flower is all leaf.
NEAR_FADE dithers tree foliage out inside arm's length of the camera, in the depth
prepass, so the lit pass never sees those pixels and the equal-depth optimisation is
untouched. gl_Position.w is the view depth, so it costs one varying and no uniform.
Blades, cards and flowers are excluded - they live at the player's feet - and the shadow
pass keeps every leaf.
daylight_hand takes a reach in metres. The falloff was an inverse square windowed between
26 and 6 with both numbers hard-coded: two per cent of its own near field at ten metres,
so a torch lit your boots. It is a gentle power out to the reach now.
Measured in Maroon Lake, twice per side: the crown mask moves 1.7% of an aspen frame and
nothing at all in the meadow under it; the near fade moves 7.1% of a first-person frame
at a conifer and 0% where there is no foliage in the lens; the torch's reach lifts the lit
ground 20 to 45% and leaves the sky bit-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>