Commit graph

20 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
7e7f3ab297 feat(lang): L8 registries by declaration - registry and def
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>
2026-09-24 13:34:51 +03:00
9259808f80 feat(lang): L6 namespaces declared in Ludic - alias
`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>
2026-09-24 02:40:57 +03:00
6a24b14f0e feat(lang): L5 generic records and functions
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>
2026-09-24 02:21:15 +03:00
57b66bdf47 feat(lang): L4 type checker between parse and emit
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>
2026-09-24 01:34:49 +03:00
0677aee93f feat(lang): L3 module scope - module, export, friend module
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>
2026-09-24 00:52:12 +03:00
0c73287e35 feat(lang): functions are values (L2), and render3d takes its scene as callbacks
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>
2026-09-24 00:27:26 +03:00
dc75a5a5ab feat(lang): a name is defined once, a local once per block, and a result is always returned
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>
2026-09-23 23:55:20 +03:00