Vk.heap_bytes pulled the Vk module - and on lang/uifree the GL window path - into a plain program,
which then failed to link (_cgl_offscreen, lgl_GetError). Os.heap_bytes is malloc_zone_statistics
through a weak reference (0 where there is none, and on Windows). Reseeded. Docs for it and for
Json.free / Json.free_all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every per-entity store (@S_ components, @H_ flags, alive, kind, freelist, owners) is a heap block
L_grow doubles from 1024 as L_alloc hands out a slot past it, the new slots zeroed; each site loads
the store's base where it indexes it (ecs_base, its registers %ecsb* so a raw function's t0 labels
cannot collide). Prop.has bounds against @L_cap, Pool.capacity answers it, a mod's registered
stores grow with the rest, every main grows the stores once before anything reads them. A snapshot
records its slot count first and a load grows to it before reading back. The overflow stop of
1c7ce84 is gone with the wall. ludic-dev test 305 passed, selfhost-test 33 passed; 1000 / 5000 /
100000 entities spawn and count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
native "<target>" "<path>" in a package's package.ludic; the compiler records the libraries of
every package a program imports and writes them into the IR (; ludic-native:), so ludicc -o,
ludic build, ludic test and ludic bundle all link one list. macOS: an rpath to the package and to
Contents/Frameworks, where ludic bundle copies and signs each library and drops the build
machine's rpath. Windows: the import library, the .dll copied beside the exe (--natives-out for
the bundle). tools/native/lib.sh builds from a pinned, checksummed source with clang on both
machines; ludic.nativeecho is the worked example; the shim rules are in packages/README.md.
Linked at build time rather than dlopen (docs/PACKAGES.md says why). Reseeded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game's exported UiAct collided with ludic.ui's, which nothing outside ludic.ui uses: it is private
to ludic.ui now, and a private record of one spelling in two modules never clashed. A real clash - a
type named like one a package exports - is still refused, and the message names the package and the
way out (`ludic_ui exports it, and exported names are one namespace - rename this one, or declare it
without export inside a module of your own`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the function that calls every reducer (and the action queue) is written in the program's own
file: in the first action's file it belonged to that module, depended on every module with a
reducer, and joined a game's modules into one 69-module cycle (examples/actions/modules and a
ludic deps case hold it); the state instances, the queue and the reducers make no deps edges
- ludicc --check / ludic build --check lower the program too and write nothing, so the code
writer's refusals are in it: a bind to a function that is gone, an unknown name (and the checker
now refuses fn <missing> itself); rejects bind_missing_fn, unknown_name, registry_count_key
- def R count is refused: its constant would be PREFIX_COUNT, the registry's size
- a file's module, package, trust and numbers-float are tables, and from the check on the lookups
of functions, enums, records, globals and externs are too (tagged enums kept as a list): Maroon
Lake's check-only build went from about 20 s to 7 s including lowering, its IR from about 2
minutes to under 10 s; duplicate declarations are found by table, not a pair of loops
- threads.ludic's pool check gives each call a little work, so a busy machine cannot run them all
on the caller before a worker wakes (it failed one run in three under load)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It lowered to an i32 compare of two pointers, which the IR refused. Two strings with the same text
are equal now, a null only to a null, and a failure says expect_eq failed (got "camp", want
"lake"). examples/library/testing_strings.ludic (two tests fail on purpose, and the output is
checked); ludic.base's actions_test uses it again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
`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>
--target <triple> (default: the host, from OS=Windows_NT) selects the Windows
runtime. emit_win.ludic defines the POSIX names the backend already calls over
the UCRT and Win32 in IR, so rename replaces an existing file, ftell is 64-bit,
and fopen is binary. Known folders follow %APPDATA% / %LOCALAPPDATA% / %TEMP%,
and the driver speaks cmd.exe, writes .exe outputs and finds LLVM's clang.
Verified: macOS three-stage fixpoint, ludic-dev test 135/135, and on Windows
the Mac-emitted Windows IR and the Windows-built compiler's IR are identical
through two generations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Ludic game opened its assets by a path relative to the working directory, so
`build/mygame` ran only from the project root and there was nothing to give
anyone but "the binary, and also this whole tree". A game is not one file, but
shipping it has to be.
`ludic pack` writes a .lpak: a header, a name-sorted entry table, a name heap
and the blobs, 16-byte aligned. Entries are stored rather than compressed - PNG,
JPEG and glTF binary arrive compressed already, and a decompressor on the load
path would spend CPU to make the file no smaller.
The reader is spliced into the compiler at the one place every asset in a Ludic
program comes through: file_open. gltf_load, tex_load, Audio.load, Fs.read_text
and the renderer's own shader loads all bottom out there, so routing it through
@lp_pak_open reaches every one of them without any of them knowing. A read of a
packed path becomes an fmemopen over the mapped bytes; everything else is the
fopen it always was. Fs.exists and Fs.size consult the packs too, so a game that
guards a load with Fs.exists keeps finding its assets once they are packed.
The pack is mmap'd rather than read: 165 MB of textures costs one syscall at
boot and pages in only what is touched. @lp_pak_boot runs from
@llvm.global_ctors, before main, so a pack is mounted before the game's first
line - and it reads packs.index, a plain list, so the mount order is explicit
and a later pack shadows an earlier one.
Without a packs.index nothing mounts and every open goes to the filesystem
exactly as before, which is every `ludic run` during development. Packing is a
shipping step and is invisible until you ship.
The compiler reproduces itself byte-exactly and the C-free bootstrap from the
seed still holds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `[a, b, c]` list literals (E_LIST → emit_list); static_type learns
slice-element, `new T`, list, string and literal kinds
- `x op= y` lowers through the same path as `x = x op y` (emit_bin_vals):
fixed `*=`/`/=` use the Q16.16 64-bit paths, string `+=` concatenates,
int→long widens; unary `-` keeps a fixed operand's type (arith_ty)
- one `unescape()` table for "strings", 'chars' and `interpolation`;
`'\''`, `'\\'`, `'\"'` no longer read as 0; unterminated char literals
and unexpected characters are errors instead of silently skipped
- every diagnostic is `file:line: error: msg` (g_parse_file / g_err_file,
Node.file + Node.line set by node()); tok_desc() in expectation errors;
duplicate `function` names and unknown `phase` names are reported in
source terms (phase_id used to default unknown phases to Overlay)
- interpolation holes skip braces inside string literals
- hand-IR preludes move from the user `@fn_` prefix to `@lp_` so a user
`is_ws` / `str_eq` / `path_join` no longer collides at link time
- `@ClearColor(expr)` accepts any constant expression; `Os.pid()` added
(docs page + inventory); `str_starts()` in support/str
- main.ludic: `else if` flag ladder, char literals, stale script comments
- examples/lang/operators.ludic covers all of the above; os.ludic covers
Os.pid; docs pages for Os.pid and the Overlay phase; ten changesets
- reseeded: selfhost/ludicc.seed.ll is the new compiler's own fixpoint
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes the two open issues and lands the pending unreleased batch:
- #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex
(scale/flip/tint), so cell / cell_span / strip ids of any size draw
through the engine sprite-render system. examples/library/sprite_atlas
is the pixel-readback regression.
- #91: `become` from an @On(Event) listener / global handler / plain
function no longer segfaults the compiler; it emits @L_scene_leave()
(a dispatch on the live scene id) so the leaving scene's on-exit runs.
UI_* handles are readable from any code (widget table built on first
use). examples/library/scene_menus covers it.
- fix: a windowed `ludicc -o` build that reaches the audio runtime only
through the atlas/Assets preload import now links audio.ll +
AVFoundation (the audio backend link was gated on a game-level
Audio.* call, so any windowed game declaring Sprite failed to link).
- the hand-written "Unreleased" CHANGELOG section is converted to
changesets under changes/ so `x release` generates it.
- plus the batch: engine-driven retained UI + UiClicked event, Overlay
phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File
namespaces, Sprite.strip, prefabs, managers, countdown fields,
enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon
packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3)
fix, shooter centre-aim fix, reserved-word function diagnostic.
Verified: x test (124/124), x test-tools, check-impl, check-vocabulary,
check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#79 — Input.axis_i(neg,pos) -> int returns a -1/0/1 movement intent from the
multi-key device set, so WASD-to-movement needs no bool->int glue and feeds an
int mover directly (dx = Input.axis_i('a','d')).
#84 — a Bounds config entity (rect + policy, from ludic.core) drives the
engine-owned world-bounds system (LateUpdate): clamp / wrap / bounce (clamp +
flip Body velocity) / kill (despawn a body fully outside). Reads the Collider
size so the box stays inside; off by default, spliced only when Bounds is
declared (byte-identical otherwise). Adds World.despawn(e) — the by-id
reflective despawn (runs @OnDespawn + frees) via a new world_despawn intrinsic
whose @fn_world_despawn helper is emitted in emit_program's tail once a use is
seen (the g_uses_* prelude pattern), used by the kill policy and callable from
any system.
Examples input_movement + world_bounds. Full suite 112/0, goldens
byte-identical, fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runtime + compiler foundation for prebuilt binary packages, over the
existing reflection C-ABI (EV0–EV8 already gives dynamic components via
ludic_register_prop).
- ludic_register_system(fn, phase) + a registry the frame loop dispatches after
each phase's own handlers — the systems analogue of ludic_register_prop,
mirroring the EV6 foreign event-listener array. Emitted for every ECS program;
a zero-length registry means a non-hosting game is output-identical.
- --emit-module: compile a package to a position-independent module — no main,
no world table — that declares the host reflection ABI it calls and carries a
load-time constructor which registers its @System(Phase) functions and runs
module_init (where it registers its dynamic components). Built as a dylib with
-undefined dynamic_lookup, it binds ludic_* back to the host image at load.
- @System(Phase) annotation marks a module function as a runtime-registered
system; the compiler supplies its address (Ludic source cannot take one).
- The reflection ABI (world table) is now emitted for every ECS program, so any
game can host binary modules with no flag; unused defs dead-strip at -O2, so
golden renders stay byte-identical and the C-free bootstrap fixpoint holds.
Proven end-to-end: a module dylib registers a component + an Update system; a
host game that never saw its source links it and the system mutates the shared
world each frame. Full suite 87/0, test-tools 30/0, fixpoint intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Types phase 3 — the option/result safety pair. result/ok/err/try shipped in
#46; this adds its companion option:
- some(v) -> option (present value; any i32-width scalar)
- none() -> option (empty; no magic -1 sentinel)
- is_some / is_none -> bool
- unwrap_or(o, fallback) -> int
A heap %Option = { i32 present, i32 value }, bare builtins guarded by find_fn
(a user fn of the same name still wins), gated by g_uses_option so unused
programs compile byte-identically — same idiom as result.
Wired: emit_call codegen + %Option decl (emit_decl) + g_uses_option (emit_core),
reseeded seed, vocabulary sync (header/JetBrains/TextMate), builtin docs +
inventory, and a self-asserting example (examples/library/optionresult.ludic +
feat_case). All suites green incl. golden renders byte-identical and the
bootstrap fixpoint.
Tagged-union enums (variant payloads + binding match + exhaustiveness) are the
deep type-system feature, split out to #56.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ECS-native shape the 2D lighting follow-up asked for, built on the
engine-owned-system hook from #43. A torch is just an entity carrying
Light2D, a wall an entity carrying Occluder, and one Ambient entity sets
the night tint — the engine runs the whole deterministic light pass at the
end of the Render phase (ambient modulate -> carve occluder shadows ->
accumulate additive radial lights) and presents. No Light.* calls wired.
- runtime/native/systems_light.ludic: esys_light2d, consuming the components
through the by-name reflection ABI and reusing the #4 accumulation core
(light_ambient / light_occlude / light_point). Reads position from a
Position component when present, else the light's own x/y.
- Split from systems.ludic so a SpriteAnim/Motion-only game never links the
light pass; spliced (with light.ludic) only when Light2D/Occluder declared.
- emit_main now boots rt_init like the auto-loop/test runner, so an
entry-driven game that renders has its framebuffer allocated (headless:
allocate only, no window, byte-identical stdout for non-rendering games).
Worked example + regression: examples/library/light_ecs.ludic (32 1 32 1).
Full suite 74 passed, self-host C-free fixpoint intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the ECS hook issues #43 and #47 named as their real dependency: a
system the *engine* owns, inserted into the frame loop over a component a
game merely declares and carries — no `handler` wired.
- runtime/native/systems.ludic: esys_spriteanim (SpriteAnim frame advance:
loop/once/pingpong) and esys_motion (Motion value tween: linear/in/out/
in-out), both on the by-name reflection ABI, integer + deterministic.
- backend: emit_engine_systems_for_phase inserts the calls after every user
handler in a phase (auto-loop and the drivable tick helpers alike);
uses_engine_systems() drives the systems.ludic splice, the world-table
force-emit, and makes a component-only game count as a systems game.
- A game that declares neither component is byte-for-byte unchanged.
Worked example + regression: examples/library/anim_ecs.ludic. Full suite
73 passed, self-host C-free bootstrap fixpoint intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fallible function returns a `result` value, built with ok(payload) on success
or err(message) on failure. The caller recovers a value with `try EXPR else {
… }`: on ok the whole expression is the payload; on err the else block runs —
with the failure message bound to `error` — and its trailing expression supplies
the fallback. It is a plain branch on the result's tag: no exceptions, no hidden
control flow, nothing unwinds. is_ok(r) / is_err(r) classify without unwrapping.
Payloads are any i32-width scalar (int/fixed/bool/entity). The feature is
additive and only kicks in when ok/err/try are used, so untouched programs
compile byte-identically (verified) and the C-free bootstrap fixpoint holds.
Complements panic/assert from #8 (the unrecoverable half). The optional
top-level frame `recover` stays deferred (needs a frame-abort mechanism); the
full tagged-union/any generalization is tracked in #1.
Adds the `try` keyword and ok/err/is_ok/is_err builtins across the compiler,
the vocabulary header, JetBrains + TextMate/VSCode grammars, the docs inventory
and pages, examples/library/recover.ludic, and a regression case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instrument each emitted statement with a per-source-line hit counter, gated
behind a new --coverage flag (default off) so ordinary builds — and the
compiler's own self-compile — stay byte-identical and the C-free bootstrap
fixpoint is untouched. A static line table plus a parallel hit-counter array
are dumped at exit through an atexit hook to $LUDIC_COVERAGE (default
ludic.cov) as a `FILE <name>` header and `<line> <hits>` rows.
bin/x test --coverage compiles the test specs with instrumentation, runs them
into per-file dumps, and aggregates a clean per-file line-coverage report that
names the unreached lines. Adds examples/library/coverage.ludic (a spec whose
tests deliberately miss one branch) and docs. Closes the last open acceptance
item of the testing framework (#12).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RFC decision (the split the issue recommended): programmer bugs abort loud and
located; recoverable failures become values. This ships the first half.
panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with
exit code 1 — a clear, located error instead of a segfault or a silent wrong
result. assert(cond, msg) is the guarded form: it aborts with `file:line:
assertion failed: <msg>` only when cond is false, otherwise execution continues.
The location is baked in at compile time (the call node carries its source line,
g_src_name carries the file); the message is any string.
Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail
(assert branches on the condition first). @fprintf and the format constant are
declared on demand (g_uses_panic), so a program that never panics is unchanged —
and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds.
- panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the
JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/)
- examples/library/errors.ludic covers the success path (asserts hold, program
runs to the end); a panic_case in the suite covers the failure path (non-zero
exit + the located stderr message). x test is now 69 checks.
Deferred: recoverable failures as `try`/`else` values (needs the tagged-union
type system, #1) and a top-level `recover` for the dev game loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.
Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.
- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
the regression suite (build, run, require exit 0 + the expected summary)
Coverage instrumentation (the biggest lift in #12) is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime type reflection: enumerate properties and fields by index, resolve ids
by name, read a field's type, and get/set/has an entity's fields generically —
the foundation the issue calls out for auto-serialization, data-driven tools,
and debug/inspector overlays. Built on the existing EV2 reflection ABI plus a
new EV8 metadata-enumeration layer, all generated at compile time (a table walk,
no heavy runtime introspection), so a binary that never reflects pays nothing.
Surface (Reflect.*, aliased in emit_call.ludic over the world_* reflection ABI):
- Reflect.prop(name) / field(prop,name) resolve ids by name (-1 = none)
- Reflect.prop_count() / prop_name(i) enumerate properties
- Reflect.field_count(prop) / field_name(prop,i) / field_type(prop,i)
enumerate a component's fields
- Reflect.get / set / has (entity, prop, ...) read/write/test a field by id
- Reflect.kind(entity) / model(name) an entity's model, by id/name
New codegen (emit_world.ludic, EV8): ludic_prop_count / prop_name /
field_count / field_name / field_type, generated the same way as ludic_prop_id
— a switch over the compile-time property/field metadata, falling through to the
mod-registered (dynamic) registries. Field names/types come straight from the
AST, so field_type reports the declared type ("int"/"fixed"/…). A program that
uses Reflect.* force-emits the reflection ABI (g_uses_reflect) so it needs no
@events of its own, exactly like Query.* (#42).
examples/library/reflect.ludic asserts 20 cases including a generic inspector
that sums every field of every component an entity has while naming none of them
— the auto-save / debug-overlay pattern end to end. Wired into x test (now 64
passed). Docs: a Reflect section + 12 per-symbol pages (positioned as an
advanced/tooling surface), inventory/coverage green. Seed reseeded; the C-free
bootstrap fixpoint holds.
Scope: this lands the reflection core and a real consumer (the generic
inspector). The generic value-tree `serialize` the proposal also sketches wants
a tagged-union/any value type from the #1 type-system work, so it is tracked as
a follow-up rather than forced in here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
07e5a20. Query.* answers questions about the live entities that carry a
property, built directly on the EV2 reflection ABI (world_query_next/world_get):
- Query.count(prop) -> int how many live entities carry prop
- Query.first(prop) -> int the lowest-id bearer, or -1
- Query.nearest(prop, pos, xf, yf, x, y) the bearer closest to (x,y), or -1
- Query.within(prop, pos, x, y, r, xf, yf) -> []int every bearer within r
prop is a property id (World.prop_id); the spatial forms read a position from a
coordinate property `pos` at two int field ids (World.field_id), so `prop` can be
a discriminating tag distinct from the position component ("nearest Enemy"), or
the same id to query the coordinate component itself. Distances are exact squared
integers (no sqrt), ties break to the lower entity id, and `within` returns
entities in ascending id order — so every answer is deterministic and replay-safe.
The engine (runtime/native/query.ludic, ~55 lines of Ludic, C-free) is a linear
scan over the entity table — ample for the entity counts Ludic targets, the same
reasoning as the grid pathfinder's open set; a bucketed/quadtree index is a
future optimisation, not a correctness need. It is spliced on demand when the
parser sees Query.* (g_uses_query), which also force-emits the reflection ABI so
a Query program needs no @events of its own (previously the ABI required them).
examples/library/query.ludic asserts 18 cases over five entities at known
positions (count/first with a component filter, nearest with a separate tag vs
position property, within radii incl. r=0 and the empty-property case), wired
into x test (now 63 passed). Docs: a Query section + 4 per-symbol pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes#29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>