Commit graph

57 commits

Author SHA1 Message Date
a0b030290b region rule (25.3c): keep() and intern(), frame_keeps, and --arena-strict
keep(x) copies a string, a slice (header and elements) or a record (shallow) onto the heap; intern(s)
hands back one heap string per distinct text from a fixed table in the runtime (FNV-1a, 65536 slots,
copied the first time; past 49152 only copied). Both are how frame code keeps what it made on purpose:
the escape analysis takes the copy as the heap's and leaves the argument LOCAL.

The analysis now records why a class escapes (the store, the event, the global it reached) and
ludic deps lists every allocation frame code makes and keeps - fkeep lines, 'ludic deps --keeps',
the frame_keeps number --check ratchets - leaving out what is under @alloc_ok and a push's growth
(25.5's capacities). --arena-strict (or 'arena strict') makes each an error naming the store, before
anything is emitted. A test: a template stored into a state is the one error; keep and intern of the
next two, an @alloc_ok push and a scratch temporary are not; 195 frames of arena resets under
R3D_ARENA_CHECK=1 later the kept and interned texts read as made, and intern gives the same string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:33:53 +03:00
bef0d6fbca arena (25.3b): LOCAL sites allocate from the frame's scratch; dispatch is not an allocation; @frame by property
Behind ludicc --arena (or 'arena on' in the program's package.ludic): the escape analysis runs and a
LOCAL site's allocation raises @lp_want for that one call, so it comes from the frame's arena. Two
halves in one mmap reservation (R3D_ARENA_MB each, 256 by default), bump-allocated with a 16-byte
size header, flipped at each frame mark: a frame's scratch is good through the next frame, then its
half is started again (R3D_ARENA_CHECK=1 fills it with 0xDD first). The heap takes over when no frame
is running, off the main thread, or past the half's end; lp_free ignores an arena block and
lp_realloc copies one out. R3D_ARENA=0 turns it off at run time; the census reports each half's
high-water mark. A program that builds text, a list and a record per frame: 29998 heap blocks made
and 18002 freed without it, 8 and 8 with it and 544 bytes of scratch a frame, the same output.

A dispatch's 'new' fills the queue's kept record (E_NEW.b), so 25.2 no longer counts it and 25.3
treats its fields as kept. '@frame' is keyed by property and field: a 'run' field is a root only in
a property that marks it, and 'tick' stays a System's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:25:32 +03:00
8ca18725b6 escape (25.3a): which allocations never outlive their frame - the analysis, behind --escape-report; @alloc_ok on generics
emit_escape.ludic: every value is in a class, joined by flow edges (a let, an assignment, an argument
into its parameter, a result into the call) and store edges (a field, an element, a push). HEAP (a
parameter, a state, a global, what an unknown call hands back) flows forward; ESC (stored into
something HEAP, into a global, into an event's fields or named values, handed to an unknown callee)
flows backward, and from an ESC or HEAP target along a store. A load is its base's class. A site that
is neither ESC nor in a function reaching Mem.frame is LOCAL (Node.uns = ES_SCRATCH). ludicc
--escape-report prints each site and the totals; nothing is emitted differently yet - the arena that
allocates the LOCAL sites is next.

@alloc_ok on a generic now covers its instances (kept_push$NetFact is under kept_push's), and a
statement's @alloc_ok is carried on the node (Node.uns), so a generic's clone keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:05:07 +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
b247603b7e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:20:44 +03:00
38d4ea72b1 feat(render3d): Vulkan in a macOS window through MoltenVK; window built-ins take a slice's elements; the NEAR_FADE SPIR-V
- a CAMetalLayer on the view (cocoa.ll win_metal_layer), VK_EXT_metal_surface, QuartzCore linked
  with Vk.*; the drawable measured after the layer sets the backing scale
- vk_mac.ll opens MoltenVK directly after any loader: a bundle ships only libMoltenVK.dylib
- a covered window is not presented to (win_visible); one frame in flight on macOS
  (R3D_VK_INFLIGHT), with images, buffers and descriptor pools held until it is done
- win_held / win_mouse / win_pad / win_touch / win_text / win_present pass slice elements (arg_buf):
  every windowed program died on its first input poll
- variants.list and SPIR-V for the three NEAR_FADE foliage programs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:52:45 +03:00
b44b88e00e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:22:50 +03:00
54f5efb23f feat(migrate): 0.R5 - ludic migrate state --tighten takes mut off every state parameter nothing down the chain writes; --root DIR lets the edits reach a directory without running its programs; a write through a local holding part of a mut state counts as a write to it
Maroon Lake: 149 parameters became read-only. What stays mut is a real write - in the packages mostly a
lazy start inside a question (things_all, gear__ensure), which is what to take out next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:20:44 +03:00
5d3a799e33 feat(pkg): phase 15 - a package can carry a native library
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>
2026-09-26 23:35:45 +03:00
71735b10a2 feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune
ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v),
q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding
facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)).

ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no
longer uses, nor anything it calls, and the argument that fills it - including an argument for a
parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is
told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would
make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:19:56 +03:00
8cf4b3fed6 fix(lang): the action drain is the program's; --check runs every check a build makes; a registry key named count is refused; name lookups are tables
- 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>
2026-09-25 23:34:43 +03:00
07505e7ef2 wip(0.S3): the packages migrated by ludic migrate state packages - every package test green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:02:21 +03:00
1e8b5b0523 wip(0.S1, 0.S2): state records, mut and read-only state parameters, entry injection, module var refused; ludic migrate state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:45:09 +03:00
8892f51096 feat(cli): ludic build --check / ludicc --check - check without building
The parse, the types and the module rules (export, uses, layers, ports,
registries) run and nothing is emitted or linked: about three seconds
on Maroon Lake. In this mode the checker asks vis_check at each
reference it resolves, with a global's initializer and a registry's
entries seen from the files the emitter would use. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:13:42 +03:00
e738741521 feat(cli): ludic deps - the module graph as the compiler resolved it
With LUDIC_DEPS=<file> the compiler records every reference the
visibility pass resolves (from module, to module) and every assignment
to another module's global. ludic deps prints the modules,
dependencies, largest cycle, cross writes and globals written from
outside, with --graph, --dot, --writes, --uses MOD, --check FILE and
--baseline FILE. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:16:34 +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
0a7525b125 feat(ui): popovers, tooltips, progress; the render3d backend installs itself; duplicate names caught first
- `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>
2026-09-24 17:41:02 +03:00
b0b0b62bce feat(lang): L7 memory is safe unless it says unsafe
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>
2026-09-24 12:53:27 +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
7f9f8de08a fix: CSPRNG on Windows, and a window's first title is the package's app name
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>
2026-09-17 16:22:05 +03:00
43d5eb5b88 feat(launcher): Process.*, Http.save_to/received/expected, App.window_hide/show
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>
2026-09-17 13:32:23 +03:00
ebb1a00352 feat(udp): Udp.* - polled IPv4 datagrams on macOS and Windows
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>
2026-09-17 11:16:14 +03:00
c10abd9f9f feat(jobs): real OS threads - Job.parallel_for, fn name, thread-safe Sync
- `fn name` names a top-level function as a value (E_FNREF, lowers to @fn_<name>); the worker
  entry point for Job.parallel_for, which checks it takes (int, pointer-like) and returns void.
- runtime/native/threads.ll (pthreads) and threads_win.ll (Win32 SRWLOCK/CONDITION_VARIABLE): a
  pool of one worker per core but one, parked between batches; every thread claims chunks by
  compare-and-swap. Linked only into programs that use Job/Promise/Sync, by `ludicc -o`,
  `ludic build` and the test suite's build helper.
- Sync.* is real: native mutexes, atomics as cmpxchg retry loops (neither clang takes atomicrw,
  the PC's rejects seq_consistent), mutex-guarded channels, Sync.cpu_count from the OS.
- spawn/despawn on a pool thread stop the program with a located panic.
- examples/library/threads.ludic and its test; docs for fn, Job.parallel_for, Job.is_worker.
- Reseeded (bootstrap-cfree: out.ll == seed.ll). 141/141 on macOS; jobs, threads and the guard
  pass on Windows from the reseeded Windows seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:52:22 +03:00
208cad7ca1 feat(vk): Vk.* - Vulkan 1.0-1.4 generated from the registry, loaded at run time
ludic-dev vkgen reads vk.xml into runtime/native/vk_api.ludic (constants, every struct's
<Struct>_sizeof and <Struct>_<field> offsets, one extern per command) and vk_thunks.ll.
Every size and offset was compiled against the SDK's C headers; `ludic-dev test` checks the
tracked files against the registry wherever the Vulkan SDK is installed.

vk_win.ll (vulkan-1.dll) and vk_mac.ll (libvulkan.1.dylib, MoltenVK) open the loader at run
time, so a program built with Vk.* starts on a machine without Vulkan. ludicc and ludic
build link both for any program that uses Vk.*. The seeds are regenerated for the new
compiler.

vk_probe reports what a machine's Vulkan can do; vk_compute dispatches a Slang compute
shader and reads the picture back, clean under the validation layer on an RTX 3070 Ti and
on an M4 Pro through MoltenVK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:52:12 +03:00
40d78cbd92 feat(windows): Http.* over WinHTTP, and the splash and window icon through WIC
http_win.ll implements http.ll's hs_* contract over WinHTTP: the request is a
record built on the game thread, the whole exchange runs on a worker thread,
TLS uses the system's certificate checks, and headers are answered from the
kept request handle. ludicc links it with winhttp instead of refusing Http.* on
Windows.

win32.ll decodes the splash and App.set_icon images with WIC (ole32): the
splash is a topmost borderless window at the artwork's size in points times the
display scale, composited over its background colour; the icon becomes an
HICON set on the window now or when win_open makes one.

Verified on the PC: examples/library/http.ludic prints its expected output, a
real GET to https://git.workshopsoft.io/ returns 200 with its body and
Content-Type, and the bundled Maroon Lake shows its splash centred at launch and
its icon in the title bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:24:50 +03:00
55b0e2ebf6 feat(windows): the ludic CLI and ludic bundle on Windows
The CLI's shell commands go through shell(), which is run() on POSIX and a
scratch script handed to Git for Windows' bash on Windows (exit codes read
directly there). compile_app links through `ludicc -o` on Windows, ludic run
starts the .exe, and ludic-dev build and ensure_ludicc assemble
selfhost/ludicc.win.seed.ll, which ludic-dev reseed now writes beside the
macOS seed. ludic bundle makes build/<name>/ with a GUI-subsystem exe carrying
the .ico beside `app icon` as an llvm-rc resource, game.lpak and packs.index;
ludicc gains --gui and --link, and quotes its whole link line for cmd.exe.

Verified: ludic-dev test 135/135, selfhost-test 32/32; on the PC a checkout
bootstraps from the Windows seed, ludic-dev build makes the toolchain, and
`ludic bundle` makes build/Maroon Lake/, which runs from its own folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:01:33 +03:00
3300fb3957 feat(windows): sound, through XAudio2
audio_win.ll implements audio.ll's snd_* contract over XAudio2 from IR: a
source voice per clip, restart on play, LoopCount for loops, SetVolume,
SetFrequencyRatio and a balance pan through SetOutputMatrix, with the COM
slots taken from the SDK's xaudio2.h. Clips are RIFF WAVE read through
lp_pak_open (weakly referenced), so a packed sound loads directly. ludicc links
it with xaudio2 and ole32, and silences xinput.lib's importeddllmain warning.

Verified: ludic-dev test 135/135, selfhost-test 32/32; on the PC a harness
loads, plays, pans, loops and stops clips, and Maroon Lake windowed reports
"sound: 31 of 31 clips loaded".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:42:48 +03:00
00d25dcc3e feat(windows): a window - win32.ll, WGL on it, and a windowed Windows build
win32.ll implements cocoa.ll's contracts over user32 and xinput: the message
pump, the held-key set and frame key in Ludic codes, the mouse with raw input
for cursor mode 2, clip-based cursor modes released on focus loss, XInput pads,
and the software framebuffer present. win32_gl.ll puts the WGL 4.1 core context
on that window (vsync, borderless full screen); gl_win.ll's context code is now
lgl_wgl_core, shared with the headless hidden window, whose win_gl_* stubs move
to gl_win_nowin.ll. audio_win.ll links Audio.* silently for now.

Verified: macOS fixpoint, ludic-dev test 135/135, selfhost-test 32/32; on the
PC gl_triangle renders identically headless and in a real window, and Maroon
Lake builds windowed and runs a playtest to frame 240 in the desktop session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:33:53 +03:00
cb05721a89 feat(windows): Gl.* on Windows, through WGL and a driver-filled thunk table
gl_win.ll creates a hidden-window WGL 4.1 core context and carries gl.ll's
float/memory helpers, with ldexp and QueryPerformanceCounter in place of the
libSystem calls. glgen now also writes gl_thunks_win.ll: the same 478 thunks,
calling through pointers that @lgl_win_load fills from wglGetProcAddress (and
opengl32.dll for GL 1.1). ludicc links the pair against opengl32/gdi32/user32
on a Windows target.

Verified: gl_api.ludic and gl_thunks.ll regenerate byte-identically, ludic-dev
test 135/135, selfhost-test 32/32, and headless gl_triangle on an RTX 3070 Ti
matches the macOS frame to within one level per channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:10:36 +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
f25289db20 feat(gl): OpenGL 4.1 and the ludic.render3d renderer
Some checks failed
ci / build-and-test (push) Waiting to run
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Has been cancelled
docs / build-and-deploy (push) Successful in 34s
`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the
platform gl3.h with every GL_* constant, generated by `ludic-dev glgen`
with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the
existing window at Retina resolution; headless builds render into an
offscreen CGL context, so a program that uses Gl.* renders and
screenshots identically under the test harness. It links gl.ll, the
thunks and OpenGL.framework only when used; every other build stays
byte-identical.

packages/ludic.render3d is a physically based renderer written on that
surface: HDRI image-based lighting, GPU-generated terrain with scanned
PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced
vegetation with impostors, procedural grass, water, SSAO, and an HDR
pipeline with bloom, auto-exposure and ACES.

It also carries this session's work on it: the terrain at half its cost
(10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer
you played, a resize that emptied the world, and the packaging that lets
a game use the renderer from its own repository — `ludic assets`, the
material manifest shipping with the package, and shader lookup falling
back to the install root. See changes/ for each, with its numbers.

The camping game that drove all of it has moved out to its own
repository, Maroon Lake; examples/rendering/smooth.ludic stays as the
renderer's example here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 03:31:12 +03:00
e175619543 refactor(cli)!: split the contributor tool out of the ludic CLI
`ludic help` ended with a section titled "contributing to the toolchain itself",
listing bootstrap, reseed, docs-gen and release tasks. None of that is available
to someone who installed the language — those tasks need the repository — so the
shipped tool was advertising work its user cannot do, in a namespace they have to
read past to find `new` and `run`.

The tasks move to a second program, dev.ludic -> bin/ludic-dev, built from a
checkout and excluded from every release artifact. `ludic` keeps the project and
package commands and nothing else; `ludic dev …` now explains where the tasks
went instead of failing as an unknown command.

What this shook out: the two programs share prelude/build/project/pkg, so the
helpers each had accreted in whichever file first needed them — cc(),
ensure_ludicc, the string functions, title_case, cmd_version — moved to where
both can see them. The argument-shift indirection added for the `dev` namespace
is gone with the namespace, so commands read argv directly again.

`ludic-dev test` asserts the split rather than trusting it: the staged install
must build a project, and `ludic dev build` there must fail while naming
ludic-dev. install.sh keeps building older tags, whose bootstrap goes through
main.ludic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:15:12 +03:00
005cc39394 feat(compiler): resolve the install root from the binary's location
The engine runtime and the bundled ludic.* packages belong to the toolchain,
not to the project, so the compiler has to know where the toolchain lives. It
derived that from the directory the binary sits in, which is `bin` — so an
in-repo build only found runtime/native/cocoa.ll when the caller set
LUDIC_HOME=., and an installed compiler had no way to find it at all.

ludic_home() derives it properly instead: $LUDIC_HOME when set, else the
binary's directory with a trailing `bin/` stripped, else a $PATH scan for
argv[0] (an install is invoked by bare name, which carries no directory). Both
layouts that exist then have the same shape — ~/.ludic/{bin,runtime,packages}
and a repo checkout — so the same rule serves both and LUDIC_HOME becomes an
override rather than a requirement.

`import "ludic.core/…"` also falls back to $LUDIC_HOME/packages, after the
project's own ludic_modules/, so a project that has not fetched its own copy
gets the packages that shipped with the toolchain instead of a symlink farm.

The `ludic` multi-call name is dropped from the compiler: that name now belongs
to the CLI, and --run is the flag it drives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:01:39 +03:00
60a1547124 build(x): stop printing a clang warning on every build
Each clang invocation the task runner makes emitted

    warning: overriding the module target triple with arm64-apple-macosx15.5.0

four times per `x build`, with nothing for anyone to act on. `cc()` now passes
-Wno-override-module, the same flag ludicc already passes for its own link.

The comment in selfhost/main.ludic explaining that flag had it backwards — it
claimed "our IR carries an explicit target triple", when the emitted IR names
no triple at all, which is precisely why clang substitutes the host's and
warns. Corrected. No IR changes, so the seed is untouched.

Also adds .claude/launch.json entry for previewing the generated docs locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:48:09 +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
ad548840c7 feat(engine): #90 atlas-aware Sprite component, #91 become from listeners, 0.3.x ergonomics batch
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 32s
ci / build-and-test (push) Successful in 2m49s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 30s
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>
2026-09-04 01:36:08 +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
3df6640fa5 feat(http): Http.* poll-based HTTP/HTTPS client over a native NSURLConnection backend (#6)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 21s
Adds the Http.* namespace and its transport, the HTTP client from #6. The JSON
companion the proposal called for already shipped as Json.* (#44).

- runtime/native/http.ll: the macOS transport — NSURLConnection driven through
  the objc runtime C ABI (no ObjC/C source), run on a detached pthread so the
  frame never blocks; TLS is the system's, on by default. A fixed slot pool holds
  each in-flight request; the worker publishes status/body/response behind an
  atomic done flag (release/acquire). Spliced + Foundation linked only when a
  program uses Http.*.
- runtime/native/http.ludic: the Http.* runtime — get/post/request, the
  open/set/body/send builder, poll/status/ok/text/body_len/header/free, plus a
  pure-Ludic response parser (Http.parse + case-insensitive header lookup) that
  is transport-independent and portable.
- compiler: Http.* dispatch, g_uses_http splice, hs_* transport intrinsics +
  declarations, the conditional Foundation link, and a new \r string/char escape
  the protocol needs.
- docs: a full docs/language/http section (16 pages); check-impl/check-docs green.
- test: examples/library/http.ludic self-asserts the parser offline (Darwin-gated
  build via the canonical path, since it links Foundation).

Verified end to end against real endpoints: HTTPS GET (200 + headers + body) and
POST (body + custom header). HTTP is out-of-band and never feeds the
deterministic sim. Reseeded; suites green (81 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 18:10:53 +03:00
4eef5ebbce feat(audio): Audio.* standard library over a native AVAudioPlayer backend (#22)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m25s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s
Adds the Audio.* namespace and its platform backend, the audio subsystem #22 was
blocked on.

- runtime/native/audio.ll: the macOS backend, AVAudioPlayer driven through the
  objc runtime C ABI (no ObjC/C source), same style as cocoa.ll — snd_load /
  play / stop / playing / set_volume / set_rate. Spliced and linked with
  AVFoundation only when a windowed build actually uses Audio.* (needed_framework,
  since AVAudioPlayer is reached by name).
- runtime/native/audio.ludic: the Audio.* runtime — a handle table, master
  volume/pitch, a single music channel. load/play/play_sound/play_music/stop/
  stop_music/stop_all/volume/pitch/is_playing. Every native call is
  is_windowed()-guarded, so a headless build carries the API as no-ops (load
  returns 0, is_playing false) and needs no audio device.
- compiler: Audio.* namespace dispatch, g_uses_audio splice, snd_* intrinsics +
  declarations, and the conditional AVFoundation link in both the canonical
  (main.ludic) and dev-runner (x app) paths.
- docs: a full docs/language/audio section (10 method pages); check-impl green.
- test: examples/library/audio.ludic self-asserts the headless no-op path.

Playback is out-of-band and never feeds the deterministic sim, but triggers are
frame-driven so replays fire the same sounds. Reseeded; suites green (80 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 17:52:31 +03:00
d6ca320269 feat(input): native gamepad + touch + mouse-position hardware bindings (#51)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s
Wire the macOS platform side of the #50 device layer, feeding the same state
buffers the read APIs consume — no API changes, purely OS glue.

- mouse: cocoa.ll reads the live cursor via mouseLocationOutsideOfEventStream,
  converted to framebuffer pixels and y-flipped, so windowed games get
  Input.mouse_x/y without injection (W_mx/W_my were never written before).
- gamepad: win_pad polls GCController.controllers each frame, packing extended-
  gamepad buttons (SDL_GameControllerButton order) and thumbsticks (16.16 fixed,
  Y negated for SDL convention) into in_pad_*. Windowed builds now load
  GameController via -needed_framework (its classes are reached by name, so a
  plain -framework link dead-strips it); DCE'd in headless builds.
- touch: the view's NSTouch phase handlers snapshot the touching set into
  in_touch_* (normalizedPosition -> framebuffer pixels).

Web platform.js gains zero-fill stubs for win_held/mouse/pad/touch so a windowed
wasm build resolves the device-layer imports. New test asserts the windowed link
loads GameController. Reseeded; full + selfhost suites green (79 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 17:37:21 +03:00
498593311f feat(testing): line coverage via --coverage + bin/x test --coverage (#45)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 14:51:14 +03:00
ea2c6ab246 feat(testing): built-in test blocks + expect assertions, auto-run with pass/fail (#12)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 14:27:45 +03:00
fed80f2152 feat(release): SemVer + ludicc --version, changesets, and x release
Some checks failed
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Successful in 13s
ci / build-and-test (push) Has been cancelled
The project had no versioning discipline: 0 tags, no CHANGELOG, no way for the
compiler to report a version. Add a lightweight, native release flow.

- Versioning: SemVer, with VERSION as the single source of truth. `ludicc
  --version` (and `ludic --version`) read it at runtime — so a bump touches one
  file and never reseeds the compiler. `x version` reports it too.
- Changesets: one small Markdown file per user-facing change under changes/
  (bump level + type + summary; see changes/README.md). This replaces "remember
  to edit the changelog" with a mergeable artifact, no Node changeset tool.
- `x release [major|minor|patch] [--publish]`: fold the pending changesets into a
  new CHANGELOG.md section (grouped by type), bump VERSION, commit, and tag
  vX.Y.Z. The level defaults to the highest changeset bump. `--publish` also
  pushes and creates the Forgejo release with source + toolchain tarballs;
  tools/ci/forgejo_release.py is the small stdlib-Python HTTP glue for the
  release API (a native Http client is issue #6).

Seed the initial changesets describing the shipped surface; the first `x release`
turns them into the v0.1.0 CHANGELOG. Reseeded for the --version flag; C-free
bootstrap fixpoint holds; suites 56 / 29 / 29 on macOS, 51 / 28 (+skips) on Linux
CI, bootstrap-cfree byte-identical on both.

Part of the repository-cleanup / DX pass (with #32, #34).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 23:46:59 +03:00
effb3f637f refactor(lang): rename the ptr/ptrs types to pointer/pointers
Expand the abbreviated pointer types to full words on the language surface:
  ptr   ->  pointer     (a raw address / FFI handle)
  ptrs  ->  pointers    (a buffer of pointers)

The Ludic type name is distinct from LLVM's own `ptr` spelling: llty() maps
`pointer`/`pointers` to LLVM `ptr`, and the emitted IR keeps `ptr`, so only
the Ludic-level surface changes. Rewrites type annotations across all
sources, the 8 hardcoded pointer type-tags, the `pointers`-buffer indexing
in emit_addr, the grammars/LSP/JetBrains tokens, and the docs
(type-ptr -> type-pointer, type-ptrs -> type-pointers). int/bool keep their
conventional short spelling (like Math).

Reseeded; C-free fixpoint holds; all suites green (45/24/29); site + check.py OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 01:58:54 +03:00
4c48077d68 refactor(lang): rename the fn keyword to function
Expand the function-declaration keyword to the full word across the whole
language and toolchain:
  fn name(...) -> T { ... }   ->   function name(...) -> T { ... }

Done as a self-hosting migration: teach the parser both spellings, reseed,
rewrite every .ludic definition to `function`, then drop `fn`. The compiler
now rejects `fn`. Touches the parser, all selfhost/tools/runtime/example/test
sources, the grammars (TextMate shared+vscode, ludic_syntax.h, JetBrains
LudicTokens.kt), the LSP and formatter, the Python doc/vocab tools
(check-impl, check-docs, validate, palette, test-lsp), and the docs
(fences, prose, kw-fn -> kw-function).

Reseeded; C-free bootstrap fixpoint holds. All suites green (45 regression,
24 self-host, 29 tool); the docs site generates and check.py passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 01:43:22 +03:00
d3301684f1 Phase 8b: modern names for the raw-memory and OS/IO primitives
Groups B and C of the leftover-primitive cleanup — renames, not new machinery,
and deliberately NO unsafe_ prefix (a __-prefix is itself a C convention, and an
`unsafe` marker carries no signal in a fully-manual-memory language with no safe
subset to contrast against).

  memory:  mem_free -> free   mem_realloc -> resize   mem_set -> fill
           ptr_add -> offset
  process: os_argc -> arg_count   os_arg -> arg   os_exit -> exit
           os_system -> run   os_getenv -> getenv   read_byte -> read_char
  dead:    mem_copy, os_time, write_byte (0 uses) — deleted

Two reseeds: accept both old and new names in the intrinsic dispatch, then
migrate every call site and drop the old names. file_open/read/write/seek/tell/
close are left as-is — they're the domain-prefixed syscall layer wrapped by
read_file, not the argc/argv-style C-ness the audit targeted; a `File` type is a
separate, larger design if wanted.

test.sh's CLI smoke updated (os_exit -> exit); check-vocabulary's grammar marker
moved off the deleted names. Reseeded (22243 lines); C-free fixpoint holds;
goldens identical; 18/18; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:55:24 +03:00
feb3a71e56 Phase 7i: byte indexing p[i] / p[i] = v (retire peek8/poke8)
Raw byte access is now indexing, not C-style peek/poke:

  peek8(src, i)          -> src[i]        (reads a byte, widened to int)
  poke8(out, j, r)       -> out[j] = r    (narrows the int to a byte)

E_INDEX on a non-slice pointer/string lowers to a `getelementptr i8` + load/zext
(read) or trunc/store (write) — byte-identical to the old peek8/poke8, so the
migration reproduces the compiler exactly. A "byte" element type (llty i8) drives
the widen/narrow. The compiler's own byte work now reads naturally, e.g.
`is_slice_ty` is `t[0] == 91 and t[1] == 93`.

Subtlety fixed on the way: g_addr_ty (the out-param carrying the indexed element
type) must be set AFTER evaluating the index expression, since a member/index in
the index would otherwise clobber it — doing it early made a byte read load a
full pointer from a byte address and crash the self-compile.

Two reseeds: add byte-index support keeping peek8/poke8, then migrate 148 call
sites and delete the intrinsics (+ the now-dead emit_gep_i8). Vocabulary drops
peek8/poke8. Reseeded (22604 lines); C-free fixpoint holds; goldens identical;
18/18; vocab clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:01:59 +03:00
1b9789630a Phase 7h: string slicing s[a..b] (retire substr)
`s[a..b]` is a fresh substring of the bytes [a, b) — the modern, end-based form
of the C-style `substr(s, start, count)`:

  substr(src, start, i - start)   -> src[start..i]
  substr(t, 2, len(t) - 2)        -> t[2..len(t)]
  substr(src, i, 2)               -> src[i..i + 2]

Mechanics: a new E_SLICE postfix (`base[lo..hi]`, distinct from `base[i]`
indexing) lowers to a @fn_str_slice prelude (malloc + copy + terminate), emitted
once into any program that slices. Two reseeds: add the syntax + prelude, then
migrate the 22 substr calls and delete substr. The migrator recognises the
common `count == end - start` shape and emits the clean `s[start..end]` rather
than `s[start..start + (end - start)]`.

examples/strings.ludic gains slicing (now prints 1..9). Reseeded (22673 lines);
C-free fixpoint holds; goldens identical; 18/18; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 01:35:24 +03:00
df6955e609 Phase 7g: the compiler builds its IR with interpolation, not +
Answering your readability point directly: the compiler's own string-building —
left uglier by the 7c `+` migration, e.g. `emit_bind(("load i32, ptr " + ip))` —
now reads as interpolation:

  emit_bind(("load i32, ptr " + ip))            -> emit_bind(`load i32, ptr {ip}`)
  emit_bind(("icmp eq i32 " + (kv + (", " + itoa(ak)))))
                                                -> emit_bind(`icmp eq i32 {kv}, {itoa(ak)}`)
  perr(("assign to unknown " + t.s))            -> perr(`assign to unknown {t.s}`)

164 concat chains across selfhost converted by a tool that flattens the `+` tree,
keeps call/index parens (only grouping parens are rewritten), and converts only
**brace-free** literals — LLVM IR structure strings full of `{`/`}` stay as `+`
rather than becoming awkward `{{`/`}}`. No new language surface; interpolation
already desugars to the same concat.

Reseeded (22565 lines); C-free fixpoint holds byte-for-byte (the strongest proof
the reconstruction is exact); goldens identical; 18/18.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 01:30:37 +03:00
839a6bf0c0 Phase 7f: len(x) works on strings too (retire slen)
`len` is now polymorphic — a slice's element count OR a string's byte length —
so the C-style `slen` (strlen) is gone: `slen(name)` -> `len(name)`. emit_len
branches on the operand type (slice header vs @strlen). Two reseeds: add the
string branch, then migrate the 19 slen calls and delete slen.

Reseeded (22565 lines); C-free fixpoint holds; goldens identical; 18/18; vocab +
doc-fences clean. (String slicing s[a..b] to replace substr is deferred to its
own phase.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 01:25:57 +03:00