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>
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>
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>
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>
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 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>
- `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>
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>
The grass had a gust built from two sines; the trees had no gust term AT ALL, only a
per-instance wobble. So the meadow rippled and the canopy above it swayed to an unrelated
rhythm, and nothing ever crossed the valley.
wind.glsl is prepended to every stage (programs.ludic, and shaders.ludic for the SPIR-V
build) so grass, crowns and water read the same field at the same world position. It
declares no uniforms on purpose: u_time and u_wind already exist in several of those
files and redeclaring them is a compile error in whichever stage includes both.
The wavelength is what made it work. At 0.030 the front was 209 m crest to crest -
longer than the meadow you can see - so the whole frame sat in one phase and the gust
read as everything breathing together. At 0.085 it is 74 m and a front crosses a view in
a couple of seconds.
R3D_DEBUG_WIND=1 paints the field on the ground. It is the only honest way to show a gust
in a still image, and two shots 0.3 s apart move by 39/255.
400 frames: GL 6.9 s, VK 7.2 s. Backends agree to 0.68/255.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The platform gave these keys no code at all, so a game's rebinding screen could not take
one and nothing said why. Windows asked the active layout what they type and got nothing
(w_vk_char answers 0 for a key with no character); macOS let them fall through to
charactersIgnoringModifiers, which reports NSF1FunctionKey and its neighbours at 0xF704
and up - outside the 256-bit held set either way.
w_keyval and ev_keyval now name them, with the same codes on both: 132-143 F1-F12,
144-149 Home / End / PageUp / PageDown / Insert / Delete, 150 Caps Lock, 152-161 the
numpad digits, 162-166 its * + - . and /. The numpad's Enter is Enter.
Key.F1, Key.Home, Key.Numpad0 and the rest fold at compile time, and Input.key_label
names them without asking the layout - a key that types nothing is called the same thing
on every layout.
examples/library/input_typeless_keys.ludic covers the codes, the names, the held set and
the press edge; 150 passed in ludic-dev test, 33 in selfhost-test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Crypto.random_* and Uuid.* read /dev/urandom, which Windows lacks, so every
byte was zero; the Windows target now calls RtlGenRandom (advapi32).
ludicc takes --title, which ludic build/run/bundle pass from package.ludic's
app name, so rt_init opens the window under that name instead of the
program name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Process.spawn/poll/kill/free - non-blocking child processes with no shell: posix_spawn on
macOS (process.ll), CreateProcessW with MSVC-quoted arguments and no console window on
Windows (process_win.ll), linked only when a program uses Process.*.
Http.save_to streams a response body into a file (NSURLSession with a run-time delegate
class on macOS, the WinHTTP read loop on Windows); Http.received / Http.expected report
progress while it is pending. Freeing a pending request cancels it and parks the slot
until the worker has finished.
App.window_hide / App.window_show take the game's window off the screen and back without
closing it; the run goes on while hidden. Docs, examples, tests and a changeset.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The package overview, its commands, scripts and hooks, dependencies against
the lock with fetch/update/verify/vendor/add/remove, the app preview and the
asset roots. The bar over package.ludic now only prompts when something
needs doing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Udp.open/port/send/recv/from_ip/from_port/close/resolve/local_ip/ip/ip_text, native
BSD sockets (udp.ll) and Winsock (udp_win.ll, -lws2_32), linked only when a program
uses Udp.*; example, docs and tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
emit_bin_vals lowered every pointer-typed ==/!= to @lp_str_eq, so two
distinct records compared their bytes up to the first zero byte. Content
compare now needs both sides to be text (string, or the untyped
pointer/ptr runtime code carries text in); other references use
icmp eq ptr, and string vs a non-text reference is a compile error.
Adds selfhost/tests/identity.ludic; both seeds regenerated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`float` (32-bit) and `double` (64-bit) with + - * / %, comparisons and unary minus.
Decimal literals take their type from context and stay `fixed` elsewhere; int and long
promote implicitly (LUDIC_WARN_FLOAT_PROMOTE=1 lists every promotion). float(), double(),
int(), long() and fixed() convert; floats(n)/doubles(n) buffers; float fields, globals,
constants and parameters; Math.* computes in float for float arguments; string/print
write the shortest round-tripping decimal; float_bits/float_from_bits expose the bits.
@deterministic code may not use floats. The f_* runtime helpers stay as they are.
Editors know the new type words; the JetBrains plugin is 1.5.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- JetBrains plugin 1.4.0: semantic colours (builtin / vendor / own), template strings,
brace handling, run configurations and a test console, package.ludic and
package.lock.ludic editing (completion, docs, app preview, colour previews, asset
navigation), External Libraries for the runtime and packages, doc pages for built-ins
- ludic-lsp: go to definition for imports, document links, hover with inferred types,
type definition, signature help with parameters, docs from docs/language
- `import "dir"` resolves a barrel `dir/index.ludic`
- package.ludic `entry`, `script` and `hook before|after <command>`; `ludic <script>`,
`ludic script`, `ludic scripts`
- `ludic test --verbose` and `--test NAME`; the test runner filters by name
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Windows runtime keyed the held set by the character MapVirtualKeyA gave a
virtual key, so a game's WASD belonged to whatever the active layout put there,
and with an input method on every letter arrived as VK_PROCESSKEY. The typing
block is now read from the scancode (the arrows, numpad and F-keys still by
virtual key); macOS reads keyCode the same way. Input.key_label(key) names a key
in the player's own layout (Windows) or as its US character elsewhere.
Verified on Windows with SendInput into a live window: VK_Z carrying W's scancode
holds 'w', VK_PROCESSKEY carrying A's holds 'a', Caps Lock changes nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>