Commit graph

7 commits

Author SHA1 Message Date
bc301c8d17 feat(engine): Tiled P1 — rt_tmap model + GID resolver + render + projection (#69)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m48s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 27s
The heart of Tiled support: load a map and draw it.

- rt_tmap model (Tmap/TmLayer/TmTileset in tiled.ludic): map header + ordered
  layers (dense int32 GID arrays, heap-allocated to w*h, lifting the 96x64 cap)
  + tilesets. Built from the intermediate Value tree, so the TMX and TMJ paths
  both feed it.
- GID resolver (Tiled.resolve): gid -> (tileset, localId, flipH/V/D); the three
  flip flags masked off before the local-id lookup, returned alongside. gid==0
  is empty.
- Image-backed render (Tiled.draw): every visible tile layer in file order,
  blitting each tile from its tileset image with flips applied at draw.
- Compatibility projection (Tiled.project / auto on load): a designated
  collision layer projects to the legacy byte tilemap ('#' solid, '=' one-way
  via the oneway property, ' ' empty) so Grid.*/Path.*/esys_move are unchanged.
- Tiled.load resolves external tilesets + images relative to the map file and
  auto-projects a collision/solids/walls layer.
- The grid and physics_tiles demos now run off a loaded map (grid_maze.tmx /
  physics_map.tmx) instead of hand-authored Map.row strings, byte-identically.

Two compiler fixes fell out of this (see the changeset):
- emit_index_addr set g_addr_ty before evaluating the index, so slice[obj.field]
  came back mis-typed; set it last, like the raw-pointer branches.
- @strcmp was declared by both the world table and the fs prelude; centralise
  it in the head prelude so a game that uses Fs/Path links.

Proven by library/tiled_p1.ludic (14 assertions: loads+renders Kenney map
identically from .tmx and .tmj, flip mirroring) + the converted grid/
physics_tiles demos. x test: 92 passed; self-host bootstrap fixpoint intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 14:43:29 +03:00
7cbd5d175c feat(compiler): package-declarable engine systems + namespaces (#62)
Registry-izes the two hooks that made stdlib namespaces and engine systems
compiler-hardcoded, so a package registers them with no compiler edit — the
Phase-1 prerequisite for shipping the controller libraries (#58–#61) as real
packages rather than in-repo stdlib.

- Engine systems are a data-driven registry (component, esys-fn, phase). The
  core three (SpriteAnim/Motion — Update, Light2D — Render) are seeded in that
  exact order, so uses_engine_systems / emit_engine_systems_for_phase are now
  registry-driven with byte-identical output (verified: anim_ecs, light_ecs,
  snake IR unchanged; 87/0 golden renders; C-free fixpoint holds). A package
  appends with `@EngineSystem(Component, Phase)` on its esys function.
- Namespaces are a registry too: a package marks a provider with
  `@Namespace(Foo)`, and emit_ns_call aliases an otherwise-unknown Foo.method to
  the bare foo_method (the same generic path the core namespaces use) — after
  every hardcoded core block, so core dispatch is untouched.
- Both annotations are keyword-free (like #64's @System), so no vocabulary /
  grammar churn.

Proven end-to-end (hermetic, source path, runs everywhere): a package registers
Score + esys_score via @EngineSystem and coach_bonus via @Namespace; a consumer
game imports it and prints "4 99" — the engine system ran each Update and
Coach.bonus() dispatched, with no compiler edit for the package. Package suite
18/0.

Core stdlib namespaces stay on their optimized hardcoded blocks by design
(byte-identity + determinism); the generic path is proven to carry a namespace
and packages ride it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 12:30:01 +03:00
e1537d2d45 feat(compiler): dynamic system registration + --emit-module for binary packages (#64)
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>
2026-09-01 07:44:18 +03:00
1f5e3c1c1a feat(anim): animation ergonomics — named clips, Anim.play/Motion.to, frame events, fluent Tween handles (#48)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m21s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s
The ergonomic layer over the engine-owned SpriteAnim/Motion systems (#43):

- Named clips: Anim.clip("run", frames, fps, mode) registers a clip by name and
  Anim.play(entity, "run") plays it; Anim.play(entity, fps, frames, mode) sets
  the clip directly. A name-keyed registry in systems.ludic.
- Frame events: Anim.on_frame(entity, frame) arms optional SpriteAnim
  event_frame/event_fired fields; the engine flags the tick the clip first lands
  on that frame, and Anim.fired(entity) reads it — the game reacts, so it stays
  inside the no-runtime-dispatch event model.
- Motion.to(entity, from, to, dur, ease) starts a value tween over the Motion
  component in one call (reflection-ABI writes, resetting the timer).
- Fluent Tween handles (runtime/native/tween.ludic): Tween.to / Tween.chain /
  Tween.delay build a sequenced, disposable handle advanced by a new engine-owned
  system (esys_tween, run each Update tick); Tween.value / Tween.done /
  Tween.parallel / Tween.stop read and control it. The 1-arg Tween.done(handle)
  is disambiguated from the 2-arg pure Tween.done(timer, dur).

Splicing: g_uses_anim_rt pulls in systems.ludic; g_uses_tween_rt pulls in
tween.ludic and inserts esys_tween into the Update phase. All integer and
deterministic, so animation and motion reproduce exactly under replay/lockstep.

Worked example + regression: examples/library/anim_sugar.ludic
(4 8 2 1 0 100 100 0 0 1 20 20 30 0 1). Twelve new docs pages (Anim, the new
Motion namespace, Tween handles). Full suite 77 passed, self-host C-free fixpoint
intact, no golden drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 16:53:16 +03:00
b0143337d9 feat(ecs): engine-owned systems auto-tick user components (#43)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m17s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 20s
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>
2026-08-31 15:41:44 +03:00
12f2dbe958 feat(types): add Reflect.* — runtime reflection over the world schema (#20)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m12s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 13:28:51 +03:00
23726afa90 refactor(selfhost): reorganise into concern-based subdirectories
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 12s
ci / build-and-test (push) Successful in 50s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-31 00:26:02 +03:00