Commit graph

9 commits

Author SHA1 Message Date
e1585de9b6 map packs: Fs.mount / Fs.unmount at run time, and a map's bakes under assets/baked/maps/<map>/
Maps ship outside the game, each one a content-addressed pack (.lmap: the .lpak format) downloaded to
the save root. The runtime already served reads from packs mounted at boot (packs.index); now one can
come and go while the game runs:

- Fs.mount(path) -> bool maps a pack over the ones mounted before it (searched first, as a later
  packs.index line is); Fs.unmount(path) -> bool gives the mapping back (munmap, UnmapViewOfFile on
  Windows) and closes the gap in the search order. Each slot keeps its length and path for it. A
  FILE* still open over one of its entries (a baked_open_range) is closed first. Still 8 packs at most.
- baked_path(map, file) is assets/baked/maps/<map>/<file> for a map's bake - what its .lmap carries
  and the game's own pack never does - and assets/baked/<file> for the rest.

The IR assembles for macOS and Windows (llvm-as). Compile-only: nothing mounted or run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:27:06 +03:00
a8d54e9878 fence (25.1): every allocation goes through the fence - sites, frame judging, census, callers
Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a
Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load
and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2).

On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame
on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX
after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends
holding more than it began with is reported by site with its callers (the unwinder, taken only once
judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS
writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=,
--fence-census= or a fence line in the program's package.ludic; the environment overrides them.

The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one
calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples
alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:35:29 +03:00
254097657e runtime: Log, DateTime.format, Input.text, Path, Mime, Fs, Os and Text keep nothing per call
Found by reading every builtin (Os.platform's 8 KB per call started it). Log builds its line only at
or above the threshold and frees it; DateTime.format folds through + so its pieces go; Input.text
encodes into one buffer; Path/Mime/Fs/Os free their temporaries on every path; string results of
Text/Path/Mime/DateTime/Os dirs are fresh and Text frees a fresh argument. Reseeded.
runtime_temps.ludic: 19.8 MB -> 0 over 20,000 rounds, 64 KB -> 0 over 200 of file work; clean under
MallocScribble. string_temps still 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:55:49 +03:00
1bc255bc40 fix(windows): Fs.remove removes an empty directory, as POSIX remove does
The UCRT's remove() deletes files only, so a tree removed bottom-up left every
directory behind. emit_win defines remove over DeleteFileA, falling back to
RemoveDirectoryA. Found by Maroon Lake's selftest26 ("1 left behind"), which now
passes on Windows with the rest of that game's self-tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:27:21 +03:00
5801005fca feat(windows): a Windows target for ludicc, and the compiler builds itself there
--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>
2026-09-13 01:59:41 +03:00
ce5bf0fe0e feat(pack): asset packs, so a built game is a thing you can hand someone
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>
2026-09-10 16:37:23 +03:00
647dfec334 feat(compiler): list literals, typed compound assignment, file:line diagnostics
- `[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>
2026-09-05 01:12:16 +03:00
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
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
Renamed from selfhost/emit_fs.ludic (Browse further)