feat(stdlib): namespaced standard library (issue #2)
Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2): 156 namespace methods across Math, Text, List, Ease, Collide, World, Net, Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time. All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds). Compiler (selfhost/): - Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude — bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/ move_toward/smoothstep/lerp/remap/sign/floor/ceil/round. - Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace, and the libc-backed queries. - List.* (complete): insert/remove_at/remove/sort plus the earlier ops. - Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/ circles/rect_circle). - Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte- identical IR) and Mem.* (bytes/words/copy/fill/peek/poke). - Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new runtime primitives; sprite/sprite_scaled aliases), Color.* functions, Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new game-loop frame counter). - Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed. Docs & tooling: - 129 new per-symbol doc pages; gen.py made data-driven (namespaces discovered from the docs, no hardcoded list); new check-impl.py enforces that every implemented namespace method / keyword / type / phase has a doc page, wired into `x test-tools`. Document the previously-undocumented keywords (break/continue/where/entry/new/public + and/or/not tokens). - LSP: namespaced signature help (ns_method_sig) covering every namespace. Tests: 12 new self-host/regression tests + a golden render for the drawing primitives. All suites green (selfhost 21, regression 45, tools 29). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ff15c4e01d
commit
a38195128f
235 changed files with 24676 additions and 7762 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -2,9 +2,9 @@ build
|
||||||
**.zip
|
**.zip
|
||||||
out.ppm
|
out.ppm
|
||||||
|
|
||||||
# self-hosted front-end binaries (built by ./build-cli.sh) and their output
|
# the toolchain binaries (ludicc, ludic, x, ludic-fmt, ludic-lsp) — all built
|
||||||
/ludic
|
# into bin/ by the one-line bootstrap + `bin/x build`; never checked in. The
|
||||||
/ludicc
|
# only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll).
|
||||||
/bin/
|
/bin/
|
||||||
|
|
||||||
# editor toolchain build artifacts
|
# editor toolchain build artifacts
|
||||||
|
|
|
||||||
50
BOOTSTRAP.md
50
BOOTSTRAP.md
|
|
@ -385,7 +385,7 @@ are in the appendix under "Syntax audit".
|
||||||
ordering constraint, not a preference.
|
ordering constraint, not a preference.
|
||||||
|
|
||||||
Today, changing the grammar costs: edit `ludicc.c`, `sed` three examples and
|
Today, changing the grammar costs: edit `ludicc.c`, `sed` three examples and
|
||||||
five runtime files, run `./test.sh`. An afternoon.
|
five runtime files, run `bin/x test`. An afternoon.
|
||||||
|
|
||||||
After the fixpoint, `ludicc` is *written in the syntax it parses*. Every change
|
After the fixpoint, `ludicc` is *written in the syntax it parses*. Every change
|
||||||
becomes a four-step dance: build a compiler that accepts both old and new forms
|
becomes a four-step dance: build a compiler that accepts both old and new forms
|
||||||
|
|
@ -485,7 +485,7 @@ stray `&`; `and`/`or` became reserved words, so `let and = 5` is rejected at the
|
||||||
mistake; the AST op string is now `"and"`/`"or"`, which is **exactly the LLVM
|
mistake; the AST op string is now `"and"`/`"or"`, which is **exactly the LLVM
|
||||||
opcode**, so the lowering ternary collapsed to passing `op` straight through;
|
opcode**, so the lowering ternary collapsed to passing `op` straight through;
|
||||||
and `--fmt` emits the new spelling for free, since it prints the op string.
|
and `--fmt` emits the new spelling for free, since it prints the op string.
|
||||||
Six regression tests in `test.sh` (64 → 70), including one asserting no `.ludic`
|
Six regression tests in `bin/x test` (64 → 70), including one asserting no `.ludic`
|
||||||
source uses the symbols outside a comment. *Fixed R3.*
|
source uses the symbols outside a comment. *Fixed R3.*
|
||||||
|
|
||||||
**S4. Reserve every keyword.** One table, shared by the lexer, parser,
|
**S4. Reserve every keyword.** One table, shared by the lexer, parser,
|
||||||
|
|
@ -531,7 +531,7 @@ file, and give every diagnostic a stable code plus a one-line suggested fix
|
||||||
(`ludicc --explain L0412`). Feeds the LSP, the docs and any model at once.
|
(`ludicc --explain L0412`). Feeds the LSP, the docs and any model at once.
|
||||||
*Cost:* ~250 lines. *Defer to after the fixpoint* — valuable, not ordering-critical.
|
*Cost:* ~250 lines. *Defer to after the fixpoint* — valuable, not ordering-critical.
|
||||||
|
|
||||||
**S9. Documentation hygiene as a build step.** `test.sh` already understands
|
**S9. Documentation hygiene as a build step.** `bin/x test` already understands
|
||||||
` ```ludic ` fences. Extend it so **every fence in every `.md` must compile**,
|
` ```ludic ` fences. Extend it so **every fence in every `.md` must compile**,
|
||||||
and fix R10's stale claims. *Cost:* ~60 lines of shell. Do this early — it is
|
and fix R10's stale claims. *Cost:* ~60 lines of shell. Do this early — it is
|
||||||
cheap and it stops the docs drifting further while the rest of the work lands.
|
cheap and it stops the docs drifting further while the rest of the work lands.
|
||||||
|
|
@ -572,14 +572,24 @@ now `and`, `or`, `not`, all reserved words, with no symbol spellings at all.
|
||||||
|
|
||||||
## 5.7 Status — self-hosting achieved
|
## 5.7 Status — self-hosting achieved
|
||||||
|
|
||||||
Updated 2026-08-27. `./test.sh` = 93/93, `./tools/test-tools.sh` = 28/28,
|
Updated 2026-08-27. `bin/x test` = 93/93, `bin/x test-tools` = 28/28,
|
||||||
`./selfhost/test.sh` = 5/5 including the bootstrap fixpoint.
|
`bin/x selfhost-test` = 5/5 including the bootstrap fixpoint.
|
||||||
|
|
||||||
**Ludic is fully self-hosted.** The compiler is written in Ludic
|
**Ludic is fully self-hosted.** The compiler is written in Ludic
|
||||||
(`selfhost/*.ludic`, ~2,400 lines), compiles every example to byte-identical
|
(`selfhost/*.ludic`, ~2,400 lines), compiles every example to byte-identical
|
||||||
output and its own source to a fixpoint, and is built from a checked-in IR seed
|
output and its own source to a fixpoint, and is built from a checked-in IR seed
|
||||||
with **no C compiler** — the former C compiler has been deleted. Run
|
with **no C compiler** — the former C compiler has been deleted.
|
||||||
`./selfhost/bootstrap-cfree.sh`.
|
|
||||||
|
From a clean checkout, build the compiler and the task-runner in one line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||||
|
```
|
||||||
|
|
||||||
|
Thereafter `bin/x build` rebuilds the entire toolchain into `bin/` (`ludicc`,
|
||||||
|
`ludic`, `x`, `ludic-fmt`, `ludic-lsp`), `bin/x bootstrap-cfree` reproduces the
|
||||||
|
compiler from the seed with no C compiler, and `bin/x help` lists every command.
|
||||||
|
Run `bin/x` from the repository root.
|
||||||
|
|
||||||
| Stage | What | State |
|
| Stage | What | State |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|
@ -589,7 +599,7 @@ with **no C compiler** — the former C compiler has been deleted. Run
|
||||||
| **1** | support libraries in Ludic (`str`, `buf`, `io`) | ✅ done |
|
| **1** | support libraries in Ludic (`str`, `buf`, `io`) | ✅ done |
|
||||||
| **2** | the compiler ported to Ludic (`lex`, `parse`, `emit_*`) | ✅ done |
|
| **2** | the compiler ported to Ludic (`lex`, `parse`, `emit_*`) | ✅ done |
|
||||||
| **3** | the fixpoint (`gen2.ll == gen3.ll`) | ✅ done |
|
| **3** | the fixpoint (`gen2.ll == gen3.ll`) | ✅ done |
|
||||||
| **4** | retire the C as a *live dependency* (IR seed, C-free rebuild) | ✅ done — `selfhost/bootstrap-cfree.sh` |
|
| **4** | retire the C as a *live dependency* (IR seed, C-free rebuild) | ✅ done — `bin/x bootstrap-cfree` |
|
||||||
| **4+** | retire `ludicc.c` entirely (port the game backend) | ✅ **done** — `compiler/` deleted; the compiler is `selfhost/*.ludic` |
|
| **4+** | retire `ludicc.c` entirely (port the game backend) | ✅ **done** — `compiler/` deleted; the compiler is `selfhost/*.ludic` |
|
||||||
|
|
||||||
### What "self-hosting" means here, precisely
|
### What "self-hosting" means here, precisely
|
||||||
|
|
@ -603,7 +613,7 @@ fixed-point. It targets native (macOS/clang) and emits LLVM IR text that clang
|
||||||
assembles, exactly the posture the C `ludicc` has.
|
assembles, exactly the posture the C `ludicc` has.
|
||||||
|
|
||||||
It is written entirely in that subset, which is why it compiles itself. The
|
It is written entirely in that subset, which is why it compiles itself. The
|
||||||
three-generation proof (`selfhost/bootstrap.sh`):
|
three-generation proof (`bin/x bootstrap`):
|
||||||
|
|
||||||
```
|
```
|
||||||
stage0 build/ludicc (C) compiles selfhost.ludic -> gen1 (a Ludic-written compiler)
|
stage0 build/ludicc (C) compiles selfhost.ludic -> gen1 (a Ludic-written compiler)
|
||||||
|
|
@ -622,11 +632,11 @@ binaries that produce the expected output.
|
||||||
|
|
||||||
The self-hosted compiler no longer needs the C `ludicc` to exist. Its own LLVM
|
The self-hosted compiler no longer needs the C `ludicc` to exist. Its own LLVM
|
||||||
IR is checked in as `selfhost/ludicc.seed.ll` — a proven fixed point — and
|
IR is checked in as `selfhost/ludicc.seed.ll` — a proven fixed point — and
|
||||||
`selfhost/bootstrap-cfree.sh` assembles that with clang (an IR assembler, the
|
`bin/x bootstrap-cfree` assembles that with clang (an IR assembler, the
|
||||||
floor Rust and Swift stand on) and rebuilds the compiler, which reproduces its
|
floor Rust and Swift stand on) and rebuilds the compiler, which reproduces its
|
||||||
own IR. **The C source is never invoked.** This is the seed path §8 recommended.
|
own IR. **The C source is never invoked.** This is the seed path §8 recommended.
|
||||||
|
|
||||||
Crucially, the compiler **evolves** without the C compiler: `selfhost/reseed.sh`
|
Crucially, the compiler **evolves** without the C compiler: `bin/x reseed`
|
||||||
uses the *current* seed to build a compiler with new source, then takes that
|
uses the *current* seed to build a compiler with new source, then takes that
|
||||||
compiler's own output as the new seed. New features (this session: `match`,
|
compiler's own output as the new seed. New features (this session: `match`,
|
||||||
bitwise ops, `peek32`/`poke32`) landed and reseeded entirely C-free. The C
|
bitwise ops, `peek32`/`poke32`) landed and reseeded entirely C-free. The C
|
||||||
|
|
@ -644,14 +654,14 @@ It now compiles **every example** — `snake`, `menu`, and the 6-file JRPG
|
||||||
`chronorift` — to output byte-identical to the original C compiler (checked
|
`chronorift` — to output byte-identical to the original C compiler (checked
|
||||||
against golden renders in `selfhost/golden/`), and still compiles its own source
|
against golden renders in `selfhost/golden/`), and still compiles its own source
|
||||||
to a fixpoint. The C compiler (`compiler/`, ~2,700 lines) has been **deleted**.
|
to a fixpoint. The C compiler (`compiler/`, ~2,700 lines) has been **deleted**.
|
||||||
`build.sh` builds `build/ludicc` from the IR seed with clang and drives the
|
`bin/x build` builds `bin/ludicc` from the IR seed with clang, and `bin/x app`
|
||||||
native link (headless, or windowed via `cocoa.ll`).
|
drives the native link (headless, or windowed via `cocoa.ll`).
|
||||||
|
|
||||||
What did not come across: the old C driver's **wasm target, cross-compilation,
|
What did not come across: the old C driver's **wasm target, cross-compilation,
|
||||||
and shared-library** paths. Those are driver features, not codegen — the
|
and shared-library** paths. Those are driver features, not codegen — the
|
||||||
self-host compiler emits native-ABI IR — and re-implementing them on the
|
self-host compiler emits native-ABI IR — and re-implementing them on the
|
||||||
self-hosted toolchain (wasm needs i32 `size_t`; the others are clang flags in
|
self-hosted toolchain (wasm needs i32 `size_t`; the others are clang flags in
|
||||||
`build.sh`) is the remaining follow-up.
|
the `bin/x app` build path) is the remaining follow-up.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -668,7 +678,7 @@ this order — cheapest-and-unblocking first:
|
||||||
4. **A6** `file_stderr` (~40).
|
4. **A6** `file_stderr` (~40).
|
||||||
5. **A2 + A3** `struct` + arrays (~400, landed together).
|
5. **A2 + A3** `struct` + arrays (~400, landed together).
|
||||||
|
|
||||||
Each gets a test in `test.sh` as it lands. The suite is at 64/64; Stage 0 should
|
Each gets a test in `bin/x test` as it lands. The suite is at 64/64; Stage 0 should
|
||||||
leave it green and larger.
|
leave it green and larger.
|
||||||
|
|
||||||
**Explicitly not in Stage 0:** function pointers, 64-bit ints, a `tool` entry
|
**Explicitly not in Stage 0:** function pointers, 64-bit ints, a `tool` entry
|
||||||
|
|
@ -711,8 +721,8 @@ internals.
|
||||||
| Sub-stage | Port | Differential oracle |
|
| Sub-stage | Port | Differential oracle |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 2a | `lex.ludic` | Dump the token stream from both compilers; `diff` over every `.ludic` in the tree. |
|
| 2a | `lex.ludic` | Dump the token stream from both compilers; `diff` over every `.ludic` in the tree. |
|
||||||
| 2b | `parse.ludic` (AST) | **`--fmt` is a free oracle.** The formatter is already a canonical AST printer, and `test.sh` already asserts formatting never changes a program. If both compilers' `--fmt` output is byte-identical on every file, the parsers agree. |
|
| 2b | `parse.ludic` (AST) | **`--fmt` is a free oracle.** The formatter is already a canonical AST printer, and `bin/x test` already asserts formatting never changes a program. If both compilers' `--fmt` output is byte-identical on every file, the parsers agree. |
|
||||||
| 2c | `check.ludic` | Diagnostic text must match on a corpus of deliberately-broken programs. `test.sh` already checks diagnostics — extend that corpus. |
|
| 2c | `check.ludic` | Diagnostic text must match on a corpus of deliberately-broken programs. `bin/x test` already checks diagnostics — extend that corpus. |
|
||||||
| 2d | `emit.ludic` (IR) | **`--emit llvm` must be byte-identical** for every example. This is the strongest oracle available: pass/fail on exact text, no judgement. |
|
| 2d | `emit.ludic` (IR) | **`--emit llvm` must be byte-identical** for every example. This is the strongest oracle available: pass/fail on exact text, no judgement. |
|
||||||
| 2e | `drive.ludic` | Assemble and link via `clang`; compare final binaries. |
|
| 2e | `drive.ludic` | Assemble and link via `clang`; compare final binaries. |
|
||||||
|
|
||||||
|
|
@ -823,7 +833,7 @@ existing framing ("the same floor Rust and Swift stand on") already covers it.
|
||||||
| 0.5 | `and`/`or`/`not` + short-circuit; doc checking (S9) | ✅ done (S1/S2/S4/S5/S6/S7 deferred — full-language polish) |
|
| 0.5 | `and`/`or`/`not` + short-circuit; doc checking (S9) | ✅ done (S1/S2/S4/S5/S6/S7 deferred — full-language polish) |
|
||||||
| 1 | `str`, `buf`, `io` support libraries in Ludic | ✅ done (`selfhost/`) |
|
| 1 | `str`, `buf`, `io` support libraries in Ludic | ✅ done (`selfhost/`) |
|
||||||
| 2 | lexer + parser + AST + IR emitter, in Ludic | ✅ done (`selfhost/`, ~1,300 lines) |
|
| 2 | lexer + parser + AST + IR emitter, in Ludic | ✅ done (`selfhost/`, ~1,300 lines) |
|
||||||
| 3 | the fixpoint (`gen2.ll == gen3.ll`) + harness | ✅ done (`selfhost/bootstrap.sh`) |
|
| 3 | the fixpoint (`gen2.ll == gen3.ll`) + harness | ✅ done (`bin/x bootstrap`) |
|
||||||
| 4 | port the game backend, retire `ludicc.c` | ⛔ out of scope — mechanical continuation |
|
| 4 | port the game backend, retire `ludicc.c` | ⛔ out of scope — mechanical continuation |
|
||||||
|
|
||||||
The self-host compiler is **~1,300 lines of Ludic** covering the compiler-subset.
|
The self-host compiler is **~1,300 lines of Ludic** covering the compiler-subset.
|
||||||
|
|
@ -847,8 +857,8 @@ exist.
|
||||||
|
|
||||||
## Appendix — probe programs
|
## Appendix — probe programs
|
||||||
|
|
||||||
Each was compiled with `./build.sh probe.ludic --headless` and run against the
|
Each was compiled with `bin/x app probe.ludic --headless` and run against the
|
||||||
current tree (`./test.sh` = 64/64).
|
current tree (`bin/x test` = 64/64).
|
||||||
|
|
||||||
**Recursion** ✅ → `55`
|
**Recursion** ✅ → `55`
|
||||||
```ludic
|
```ludic
|
||||||
|
|
|
||||||
49
COMPILING.md
49
COMPILING.md
|
|
@ -6,24 +6,31 @@
|
||||||
> unchanged. `ludicc` now drives clang itself (via an `os_system` intrinsic), so
|
> unchanged. `ludicc` now drives clang itself (via an `os_system` intrinsic), so
|
||||||
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly, and a sibling
|
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly, and a sibling
|
||||||
> command `ludic app.ludic` compiles to a temporary binary and runs it in one
|
> command `ludic app.ludic` compiles to a temporary binary and runs it in one
|
||||||
> step. Build both with `./build-cli.sh`. `build.sh` remains as a convenience
|
> step. The whole toolchain is built by `bin/x build`; `bin/x app` remains as a
|
||||||
> wrapper. `--fmt` is reimplemented as a lex+parse gate (the doc-check hook).
|
> convenience wrapper over the compiler. `--fmt` is reimplemented as a lex+parse
|
||||||
> The `--target`/cross-compile and `--shared` paths are still features of the old
|
> gate (the doc-check hook). The `--target`/cross-compile and `--shared` paths are
|
||||||
> C driver not yet re-implemented on the self-hosted toolchain. See
|
> still features of the old C driver not yet re-implemented on the self-hosted
|
||||||
> BOOTSTRAP.md §5.7.
|
> toolchain. See BOOTSTRAP.md §5.7.
|
||||||
|
>
|
||||||
|
> From a clean checkout, build the compiler and the task-runner in one line, then
|
||||||
|
> let `bin/x` do the rest (run it from the repository root):
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> ./build-cli.sh # build ./ludicc and ./ludic (from the seed)
|
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/x
|
||||||
> ./ludicc examples/snake.ludic -o bin/snake # compile
|
> clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||||
> ./ludic examples/snake.ludic # compile + run
|
> bin/x build # rebuild the whole toolchain into bin/
|
||||||
|
> # (ludicc, ludic, x, ludic-fmt, ludic-lsp)
|
||||||
|
> bin/ludicc examples/snake.ludic -o bin/snake # compile
|
||||||
|
> bin/ludic examples/snake.ludic # compile + run
|
||||||
|
> bin/x help # list every command
|
||||||
> ```
|
> ```
|
||||||
>
|
>
|
||||||
> The binaries are multi-call (one native binary under two names): invoked as
|
> The binaries are multi-call (one native binary under two names): invoked as
|
||||||
> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with
|
> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with
|
||||||
> systems is a game and links windowed by default; `--headless` and `--windowed`
|
> systems is a game and links windowed by default; `--headless` and `--windowed`
|
||||||
> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via
|
> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via
|
||||||
> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them at the
|
> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them in
|
||||||
> repo root, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the
|
> `bin/`, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the
|
||||||
> assembler/linker (default `clang`).
|
> assembler/linker (default `clang`).
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,15 +73,19 @@ the old C driver and are **not yet re-implemented** on the self-hosted toolchain
|
||||||
(see the note at the top). The rows above the line work today via the
|
(see the note at the top). The rows above the line work today via the
|
||||||
self-hosted `ludicc`.
|
self-hosted `ludicc`.
|
||||||
|
|
||||||
`build.sh` wraps the common cases:
|
`bin/x app` wraps the common cases:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples/snake.ludic # -> build/snake (native)
|
bin/x app examples/snake.ludic # -> build/snake (native)
|
||||||
./build.sh examples/lib/combat.ludic --lib # -> build/libcombat.* (library)
|
bin/x app examples/lib/combat.ludic --lib # -> build/libcombat.* (library)
|
||||||
./build.sh examples/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
bin/x app examples/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
||||||
./build.sh examples/snake.ludic --web # -> build/web/ (browser)
|
bin/x app examples/snake.ludic --web # -> build/web/ (browser)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `--lib` and `--web` targets were part of the old C driver and are **not yet
|
||||||
|
re-implemented** on the self-hosted toolchain — `bin/x app` supports the native
|
||||||
|
windowed and `--headless` builds today.
|
||||||
|
|
||||||
## Programs and libraries
|
## Programs and libraries
|
||||||
|
|
||||||
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
|
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
|
||||||
|
|
@ -208,7 +219,7 @@ only the triple changes.
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples/chronorift.ludic --web
|
bin/x app examples/chronorift.ludic --web
|
||||||
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
|
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -296,7 +307,7 @@ node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss
|
||||||
```
|
```
|
||||||
|
|
||||||
Because Ludic is fixed-point and its RNG is seeded, the native headless binary
|
Because Ludic is fixed-point and its RNG is seeded, the native headless binary
|
||||||
and the wasm one must render byte-identical frames from the same input. `test.sh`
|
and the wasm one must render byte-identical frames from the same input. `bin/x test`
|
||||||
asserts exactly that, which is a much stronger check on the backend than
|
asserts exactly that, which is a much stronger check on the backend than
|
||||||
"it started".
|
"it started".
|
||||||
|
|
||||||
|
|
@ -310,13 +321,13 @@ entity allocator, save/load snapshots, the frame loop, the window, and the whole
|
||||||
graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
|
graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
|
||||||
the retained UI.
|
the retained UI.
|
||||||
|
|
||||||
None of it goes through C. `./test.sh` asserts that directly: no C source
|
None of it goes through C. `bin/x test` asserts that directly: no C source
|
||||||
survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
|
survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
|
||||||
build, run and render from IR alone.
|
build, run and render from IR alone.
|
||||||
|
|
||||||
## Every flag
|
## Every flag
|
||||||
|
|
||||||
The self-hosted `ludicc`/`ludic` (built with `./build-cli.sh`) accept:
|
The self-hosted `ludicc`/`ludic` (built with `bin/x build-cli`) accept:
|
||||||
|
|
||||||
```
|
```
|
||||||
<file.ludic> the program to compile (first non-flag argument)
|
<file.ludic> the program to compile (first non-flag argument)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> **Status: EV0 fully shipped; EV1 (spawn/despawn), EV2 (first cut) and EV3
|
> **Status: EV0 fully shipped; EV1 (spawn/despawn), EV2 (first cut) and EV3
|
||||||
> shipped; EV4–EV7 are design.** Implemented, self-hosted to the C-free fixpoint,
|
> shipped; EV4–EV7 are design.** Implemented, self-hosted to the C-free fixpoint,
|
||||||
> and each a `test.sh` check:
|
> and each a `bin/x test` check:
|
||||||
> - **EV0** — `event`/`@On`/`emit` lowered to `@ev_<E>` dispatch (compile-time
|
> - **EV0** — `event`/`@On`/`emit` lowered to `@ev_<E>` dispatch (compile-time
|
||||||
> listeners), **plus the foreign C ABI** (`ludic_on_<E>`, the `%Ev_<E>` payload
|
> listeners), **plus the foreign C ABI** (`ludic_on_<E>`, the `%Ev_<E>` payload
|
||||||
> struct, a fixed-capacity listener array), proven by a C mod in
|
> struct, a fixed-capacity listener array), proven by a C mod in
|
||||||
|
|
@ -566,7 +566,7 @@ work (and how LIFECYCLE/SCENES sequence).
|
||||||
`N_EVENT`/`S_EMIT`; `parse_event` + `@On` annotation + `emit` statement (guarded
|
`N_EVENT`/`S_EMIT`; `parse_event` + `@On` annotation + `emit` statement (guarded
|
||||||
by an identifier-lookahead so a bare `emit(...)` call still parses); registries
|
by an identifier-lookahead so a bare `emit(...)` call still parses); registries
|
||||||
`g_events`/`g_onlisten` (emit_core); `emit_event_fns` (emit_game); `emit_emit`
|
`g_events`/`g_onlisten` (emit_core); `emit_event_fns` (emit_game); `emit_emit`
|
||||||
(emit_stmt). [`examples/events.ludic`](examples/events.ludic) is a `test.sh` check.
|
(emit_stmt). [`examples/events.ludic`](examples/events.ludic) is a `bin/x test` check.
|
||||||
- **EV1 — `@Public` hook promotion.** ✅ *All scopes shipped.* `@Public` on a
|
- **EV1 — `@Public` hook promotion.** ✅ *All scopes shipped.* `@Public` on a
|
||||||
lifecycle hook fires a public event at that hook's site (payload: entity, plus
|
lifecycle hook fires a public event at that hook's site (payload: entity, plus
|
||||||
`EndReason` for despawn); `find_event(name)` doubles as the "is this hook
|
`EndReason` for despawn); `find_event(name)` doubles as the "is this hook
|
||||||
|
|
|
||||||
20
LANGUAGE.md
20
LANGUAGE.md
|
|
@ -752,16 +752,16 @@ ludic app.ludic # compile AND run (forwards the exit code
|
||||||
```
|
```
|
||||||
|
|
||||||
`ludicc` (compile) and `ludic` (compile-and-run) are one multi-call binary built
|
`ludicc` (compile) and `ludic` (compile-and-run) are one multi-call binary built
|
||||||
by `./build-cli.sh`. **[COMPILING.md](COMPILING.md) is the authoritative CLI
|
by `bin/x build-cli`. **[COMPILING.md](COMPILING.md) is the authoritative CLI
|
||||||
reference** — the full flag set (`-o`, `--windowed`, `--headless`, `--emit-llvm`,
|
reference** — the full flag set (`-o`, `--windowed`, `--headless`, `--emit-llvm`,
|
||||||
`--save-temps`, `--run`), the `LUDIC_HOME` / `LUDIC_CC` environment variables,
|
`--save-temps`, `--run`), the `LUDIC_HOME` / `LUDIC_CC` environment variables,
|
||||||
and the IR-to-stdout bootstrap contract (no `-o`, invoked as `ludicc`) that
|
and the IR-to-stdout bootstrap contract (no `-o`, invoked as `ludicc`) that
|
||||||
`build.sh` / `reseed.sh` rely on. The default mode is auto: a file with `handler`s
|
`bin/x app` / `bin/x reseed` rely on. The default mode is auto: a file with `handler`s
|
||||||
links windowed, otherwise headless; an explicit flag always wins.
|
links windowed, otherwise headless; an explicit flag always wins.
|
||||||
|
|
||||||
The retired C driver's `--shared`, `--fmt`, `-c`, cross-compile (`--target`) and
|
The retired C driver's `--shared`, `--fmt`, `-c`, cross-compile (`--target`) and
|
||||||
wasm modes are **not** on the self-hosted toolchain (see "Not yet implemented").
|
wasm modes are **not** on the self-hosted toolchain (see "Not yet implemented").
|
||||||
Source formatting now lives in the standalone `build/ludic-fmt` (below), not a
|
Source formatting now lives in the standalone `bin/ludic-fmt` (below), not a
|
||||||
compiler flag.
|
compiler flag.
|
||||||
|
|
||||||
The self-hosted compiler is intentionally permissive: it has no separate
|
The self-hosted compiler is intentionally permissive: it has no separate
|
||||||
|
|
@ -773,10 +773,10 @@ duplicate types, unknown fields, arity) are future work.
|
||||||
### Editors
|
### Editors
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/build-tools.sh # -> build/ludic-fmt, build/ludic-lsp
|
bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp
|
||||||
build/ludic-fmt -w src/ # format in place (keeps comments)
|
bin/ludic-fmt -w src/ # format in place (keeps comments)
|
||||||
build/ludic-fmt --check . # CI: exit 1 if anything is unformatted
|
bin/ludic-fmt --check . # CI: exit 1 if anything is unformatted
|
||||||
build/ludic-lsp --stdio # the language server, for any editor
|
bin/ludic-lsp --stdio # the language server, for any editor
|
||||||
```
|
```
|
||||||
|
|
||||||
`ludic-fmt` is the source formatter: it works on tokens, so comments and blank
|
`ludic-fmt` is the source formatter: it works on tokens, so comments and blank
|
||||||
|
|
@ -800,7 +800,7 @@ Emacs, Sublime and Zed, are in `tools/editors/` — see
|
||||||
- `examples/snake.ludic` — Snake, no assets — same compiler, proving generality.
|
- `examples/snake.ludic` — Snake, no assets — same compiler, proving generality.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples/snake.ludic && ./build/snake
|
bin/x app examples/snake.ludic && ./build/snake
|
||||||
```
|
```
|
||||||
|
|
||||||
## Not yet implemented
|
## Not yet implemented
|
||||||
|
|
@ -815,7 +815,7 @@ are future work.
|
||||||
slices; fixed inline arrays are not accepted yet. Use `[]T` slices.
|
slices; fixed inline arrays are not accepted yet. Use `[]T` slices.
|
||||||
- **CLI: `--shared`, `--fmt`, and the wasm/cross target** — these were features of
|
- **CLI: `--shared`, `--fmt`, and the wasm/cross target** — these were features of
|
||||||
the retired C driver; the self-hosted `ludicc` does not carry them (source
|
the retired C driver; the self-hosted `ludicc` does not carry them (source
|
||||||
formatting lives in `build/ludic-fmt` instead). Output-path and IR flags are in
|
formatting lives in `bin/ludic-fmt` instead). Output-path and IR flags are in
|
||||||
flux as the CLI front-end is rebuilt — check `ludicc` usage for the current set.
|
flux as the CLI front-end is rebuilt — check `ludicc` usage for the current set.
|
||||||
|
|
||||||
Records (`property` used with `new`) and array types, `break`/`continue`, and
|
Records (`property` used with `new`) and array types, `break`/`continue`, and
|
||||||
|
|
@ -827,7 +827,7 @@ self-hosting; their lowerings are in
|
||||||
|
|
||||||
> **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks
|
> **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks
|
||||||
> compile; [`examples/scenes.ludic`](examples/scenes.ludic) runs and is checked
|
> compile; [`examples/scenes.ludic`](examples/scenes.ludic) runs and is checked
|
||||||
> by `test.sh`. A scene lowers to a `machine` the compiler writes for you: one
|
> by `bin/x test`. A scene lowers to a `machine` the compiler writes for you: one
|
||||||
> implicit active-scene register, states numbered by declaration order, and
|
> implicit active-scene register, states numbered by declaration order, and
|
||||||
> `become` as two direct calls plus a store. Richer scene features (the overlay
|
> `become` as two direct calls plus a store. Richer scene features (the overlay
|
||||||
> stack, scene-owned entities, scene-local state, transition parameters) are
|
> stack, scene-owned entities, scene-local state, transition parameters) are
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
> **Status: LC0–LC1 shipped; LC2–LC6 are design.** The structural attach/detach
|
> **Status: LC0–LC1 shipped; LC2–LC6 are design.** The structural attach/detach
|
||||||
> pair and `@OnDetach` (§4, LC0), and reason-carrying `@OnDespawn` (§5, LC1), are
|
> pair and `@OnDetach` (§4, LC0), and reason-carrying `@OnDespawn` (§5, LC1), are
|
||||||
> implemented and tested ([`examples/detach.ludic`](examples/detach.ludic),
|
> implemented and tested ([`examples/detach.ludic`](examples/detach.ludic),
|
||||||
> [`examples/reason.ludic`](examples/reason.ludic), `test.sh` checks). The
|
> [`examples/reason.ludic`](examples/reason.ludic), `bin/x test` checks). The
|
||||||
> extensions LC2–LC6 are research-informed proposals, not built. This document
|
> extensions LC2–LC6 are research-informed proposals, not built. This document
|
||||||
> distills a survey of lifecycle models across seven systems (§3) into a roadmap
|
> distills a survey of lifecycle models across seven systems (§3) into a roadmap
|
||||||
> for Ludic. §13 lists the open decisions.
|
> for Ludic. §13 lists the open decisions.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ threads + sockets + a GPU) are load-bearing for everything above them.
|
||||||
This document is written against evidence, not memory: `luanti-org/luanti` at
|
This document is written against evidence, not memory: `luanti-org/luanti` at
|
||||||
`main` was cloned and read, and every claim about Ludic below was verified
|
`main` was cloned and read, and every claim about Ludic below was verified
|
||||||
against `compiler/ludicc.c` / `compiler/native.c` or by compiling a probe
|
against `compiler/ludicc.c` / `compiler/native.c` or by compiling a probe
|
||||||
program with `build/ludicc`.
|
program with `bin/ludicc`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1246,7 +1246,7 @@ process that reads it back — proving the thread pool, the compressor and the
|
||||||
round-trip.
|
round-trip.
|
||||||
|
|
||||||
**Note on the new WASM target.** A peer session just landed WebAssembly support
|
**Note on the new WASM target.** A peer session just landed WebAssembly support
|
||||||
(`runtime/web/`, `test.sh` now 64/64), and `@main` is now split into
|
(`runtime/web/`, `bin/x test` now 64/64), and `@main` is now split into
|
||||||
`ludic_boot/frame/alive/teardown` so a browser can drive the loop from
|
`ludic_boot/frame/alive/teardown` so a browser can drive the loop from
|
||||||
`requestAnimationFrame`. Two consequences for this roadmap: (a) threads on
|
`requestAnimationFrame`. Two consequences for this roadmap: (a) threads on
|
||||||
wasm32 mean Web Workers + SharedArrayBuffer, not pthreads, so **G-17 needs a
|
wasm32 mean Web Workers + SharedArrayBuffer, not pthreads, so **G-17 needs a
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ Each is independently shippable and testable, matching how the repo phases work.
|
||||||
|
|
||||||
- **M0 — target axis** (M1) + **revive the OS-owned loop** (M2). *Do these first
|
- **M0 — target axis** (M1) + **revive the OS-owned loop** (M2). *Do these first
|
||||||
and together* — they're the shared compiler plumbing, they un-break the existing
|
and together* — they're the shared compiler plumbing, they un-break the existing
|
||||||
web target (proving the frame-loop split against `run.mjs`/`test.sh` before any
|
web target (proving the frame-loop split against `run.mjs`/`bin/x test` before any
|
||||||
mobile SDK is involved), and they need no mobile toolchain. This is the floor.
|
mobile SDK is involved), and they need no mobile toolchain. This is the floor.
|
||||||
- **M1 — iOS simulator, framebuffer-as-texture** (M3 iOS shim + M4.1). First pixels
|
- **M1 — iOS simulator, framebuffer-as-texture** (M3 iOS shim + M4.1). First pixels
|
||||||
on a phone, GL/Metal binding proven, no signing/device friction yet.
|
on a phone, GL/Metal binding proven, no signing/device friction yet.
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
> and — unlike the original N0/N1 which linked C hosts — every phase now runs as a
|
> and — unlike the original N0/N1 which linked C hosts — every phase now runs as a
|
||||||
> self-contained **pure-Ludic** program (no `.c`, no foreign host): a built-in
|
> self-contained **pure-Ludic** program (no `.c`, no foreign host): a built-in
|
||||||
> loopback transport fills the seam, and each `examples/net_*.ludic` drives and
|
> loopback transport fills the seam, and each `examples/net_*.ludic` drives and
|
||||||
> asserts itself from its own `entry`. See `test.sh` (checks `net_echo` … `net_demo`)
|
> asserts itself from its own `entry`. See `bin/x test` (checks `net_echo` … `net_demo`)
|
||||||
> and `examples/net_demo.ludic` for a full RPC→authority→replicate→reconcile loop.
|
> and `examples/net_demo.ludic` for a full RPC→authority→replicate→reconcile loop.
|
||||||
> clang remains only as the LLVM-IR assembler/linker (no C is compiled), the floor
|
> clang remains only as the LLVM-IR assembler/linker (no C is compiled), the floor
|
||||||
> Rust and Swift stand on.
|
> Rust and Swift stand on.
|
||||||
>
|
>
|
||||||
> _Historical note:_ **N0 + N1 shipped first; N2–N6 were design.** Two phases landed as `test.sh`
|
> _Historical note:_ **N0 + N1 shipped first; N2–N6 were design.** Two phases landed as `bin/x test`
|
||||||
> checks. **N0 (transport seam):** `extern fn` now lowers end to end — a direct
|
> checks. **N0 (transport seam):** `extern fn` now lowers end to end — a direct
|
||||||
> `@<sym>` call plus a `declare`, no networking logic in the compiler — so the whole
|
> `@<sym>` call plus a `declare`, no networking logic in the compiler — so the whole
|
||||||
> transport is two externs (`net_send`/`net_poll`) a host fills. Proven by
|
> transport is two externs (`net_send`/`net_poll`) a host fills. Proven by
|
||||||
|
|
|
||||||
64
README.md
64
README.md
|
|
@ -22,7 +22,7 @@ compiler intrinsics, the same floor Rust and Swift stand on.
|
||||||
lexer, parser and LLVM-IR backend — that compiles every example (including the
|
lexer, parser and LLVM-IR backend — that compiles every example (including the
|
||||||
6-file JRPG) to the byte-exact same binary the original C compiler produced, and
|
6-file JRPG) to the byte-exact same binary the original C compiler produced, and
|
||||||
compiles **its own source** to a fixpoint. It is built from a checked-in IR seed
|
compiles **its own source** to a fixpoint. It is built from a checked-in IR seed
|
||||||
(`selfhost/ludicc.seed.ll`) with clang alone; run `./selfhost/bootstrap-cfree.sh`
|
(`selfhost/ludicc.seed.ll`) with clang alone; run `bin/x bootstrap-cfree`
|
||||||
to rebuild it with no C compiler in the loop. The former C compiler is gone.
|
to rebuild it with no C compiler in the loop. The former C compiler is gone.
|
||||||
|
|
||||||
Neither the compiler nor the games are C. Beneath both sits only the platform's
|
Neither the compiler nor the games are C. Beneath both sits only the platform's
|
||||||
|
|
@ -35,8 +35,8 @@ self-hosted native toolchain.)
|
||||||
|
|
||||||
| Path | What it is |
|
| Path | What it is |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| `selfhost/*.ludic` | **the compiler, written in Ludic** — lexer, parser, and the LLVM-IR backend (ECS storage, queries, spawn, `match`/`machine`, UI, scenes, save/load, fixed-point). Concatenated by `selfhost/build.sh`; built from `selfhost/ludicc.seed.ll` |
|
| `selfhost/*.ludic` | **the compiler, written in Ludic** — lexer, parser, and the LLVM-IR backend (ECS storage, queries, spawn, `match`/`machine`, UI, scenes, save/load, fixed-point). Concatenated by `bin/x selfhost-build`; built from `selfhost/ludicc.seed.ll` |
|
||||||
| `selfhost/bootstrap-cfree.sh` | rebuild the compiler from the IR seed with **no C compiler**, and prove it reproduces its own IR |
|
| `tools/x/*.ludic` | **the task runner, written in Ludic** — one binary (`bin/x`) that builds, tests, bootstraps and reseeds the whole project, replacing every shell script. `bin/x bootstrap-cfree` rebuilds the compiler from the IR seed with **no C compiler** and proves it reproduces its own IR |
|
||||||
| `runtime/native/core.ludic` | the runtime written *in Ludic* for the native path (framebuffer, text, registers, RNG, input) |
|
| `runtime/native/core.ludic` | the runtime written *in Ludic* for the native path (framebuffer, text, registers, RNG, input) |
|
||||||
| `COMPILING.md` | the native pipeline: `ludicc → LLVM IR → exe/dylib`, `module`/`export`, cross-compilation, `rt_*` intrinsics |
|
| `COMPILING.md` | the native pipeline: `ludicc → LLVM IR → exe/dylib`, `module`/`export`, cross-compilation, `rt_*` intrinsics |
|
||||||
| `runtime/native/image.ludic` | PNG decoding, images, sprites, alpha blending and 9-slice — in Ludic |
|
| `runtime/native/image.ludic` | PNG decoding, images, sprites, alpha blending and 9-slice — in Ludic |
|
||||||
|
|
@ -51,58 +51,50 @@ self-hosted native toolchain.)
|
||||||
| `examples/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, model-based) |
|
| `examples/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, model-based) |
|
||||||
| `examples/menu.ludic` | a retained-UI title screen (9-slice, TrueType, focusable buttons) |
|
| `examples/menu.ludic` | a retained-UI title screen (9-slice, TrueType, focusable buttons) |
|
||||||
| `examples/snake.ludic` | a second, unrelated game — proves the language is general (same toolchain, no engine hardcoding) |
|
| `examples/snake.ludic` | a second, unrelated game — proves the language is general (same toolchain, no engine hardcoding) |
|
||||||
| `build.sh` | `./build.sh examples/<name>.ludic` |
|
| `bin/x` | the task runner — `bin/x app examples/<name>.ludic` compiles a program; `bin/x help` lists every command |
|
||||||
| `test.sh` | regression suite: builds the compiler, compiles/runs all examples, checks save/load + diagnostics (`./test.sh`) |
|
| `bin/x test` | regression suite: builds the compiler, compiles/runs all examples, checks save/load + diagnostics |
|
||||||
| `tools/ludic-tools/` | the editor toolchain, in C: `ludic-fmt` (source formatter) and `ludic-lsp` (language server) — one lexer and one vocabulary shared by both |
|
| `tools/ludic-tools/` | the editor toolchain, **in Ludic**: `ludic-fmt` (source formatter) and `ludic-lsp` (language server) — one lexer and one vocabulary shared by both |
|
||||||
| `tools/editors/` | plugins for VS Code and JetBrains, plus configuration for Neovim, Helix, Emacs, Sublime and Zed ([README](tools/editors/README.md)) |
|
| `tools/editors/` | plugins for VS Code and JetBrains, plus configuration for Neovim, Helix, Emacs, Sublime and Zed ([README](tools/editors/README.md)) |
|
||||||
| `tools/test-tools.sh` | regression suite for the toolchain: proves formatting never changes a program, drives the language server over real LSP traffic |
|
| `bin/x test-tools` | regression suite for the toolchain: proves formatting never changes a program, drives the language server over real LSP traffic |
|
||||||
|
|
||||||
## Build & run
|
## Build & run
|
||||||
|
|
||||||
`ludicc` compiles Ludic straight to machine code via LLVM IR. See
|
Everything is driven by `bin/x`, the project's task runner — one native binary,
|
||||||
**[COMPILING.md](COMPILING.md)** for the pipeline, shared libraries
|
written in Ludic and compiled by Ludic, that replaces every build/test/bootstrap
|
||||||
(`--shared`), cross-compilation and the runtime protocol.
|
shell script. Bootstrap it once from a clean checkout (the only step Ludic can't
|
||||||
|
do for itself, since compiling Ludic needs a compiler):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples/chronorift.ludic
|
clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `bin/x build` builds the whole toolchain into `bin/` — including `bin/x`
|
||||||
|
itself — and `bin/x help` lists every command. `ludicc` compiles Ludic straight
|
||||||
|
to machine code via LLVM IR; see **[COMPILING.md](COMPILING.md)** for the
|
||||||
|
pipeline and the runtime protocol.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/x app examples/chronorift.ludic
|
||||||
./build/chronorift # opens a native window
|
./build/chronorift # opens a native window
|
||||||
```
|
```
|
||||||
|
|
||||||
Headless render (for testing / CI):
|
Headless render (for testing / CI):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples/chronorift.ludic --headless
|
bin/x app examples/chronorift.ludic --headless
|
||||||
printf 'ddddwww' | ./build/chronorift_headless # writes out.ppm
|
printf 'ddddwww' | ./build/chronorift_headless # writes out.ppm
|
||||||
sips -s format png out.ppm --out frame.png
|
sips -s format png out.ppm --out frame.png
|
||||||
```
|
```
|
||||||
|
|
||||||
A `module` compiles to a shared library instead of a program:
|
(The shared-library and wasm/web build paths lived in the old C compiler and are
|
||||||
|
not yet re-implemented on the self-hosted native toolchain, so `bin/x app` builds
|
||||||
```bash
|
native windowed and `--headless` targets today. See
|
||||||
./build.sh examples/lib/combat.ludic --lib # -> build/libcombat.dylib
|
**[COMPILING.md](COMPILING.md)** for the pipeline and the runtime protocol.)
|
||||||
```
|
|
||||||
|
|
||||||
In a browser:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build.sh examples/chronorift.ludic --web # -> build/web/
|
|
||||||
python3 -m http.server -d build/web 8000 # open http://localhost:8000/
|
|
||||||
```
|
|
||||||
|
|
||||||
`build/web/` is a self-contained 116 KB directory — the 42 KB module, the loader,
|
|
||||||
a page, and the sprites the compiler saw the game name. Copy it to any static
|
|
||||||
host and it runs; it needs no server-side anything and no special headers. Saves
|
|
||||||
go to `localStorage`, so `save()`/`load()` survive a reload.
|
|
||||||
|
|
||||||
A wasm build needs an LLVM with the WebAssembly backend and `wasm-ld` — Linux
|
|
||||||
`clang`/`lld` have both, Apple's clang has neither (`brew install llvm lld`).
|
|
||||||
See **[COMPILING.md](COMPILING.md#the-web)** for the pipeline, who owns the frame
|
|
||||||
loop, and how assets are bundled.
|
|
||||||
|
|
||||||
## Editor support
|
## Editor support
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/build-tools.sh # -> build/ludic-fmt, build/ludic-lsp
|
bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp
|
||||||
```
|
```
|
||||||
|
|
||||||
`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor:
|
`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor:
|
||||||
|
|
@ -172,7 +164,7 @@ Controls:
|
||||||
- [x] Random encounters + turn-based battle (HP/MP, seeded-RNG damage)
|
- [x] Random encounters + turn-based battle (HP/MP, seeded-RNG damage)
|
||||||
- [x] Party + **local co-op** (P1 Knight, P2 Mage, per-player turns)
|
- [x] Party + **local co-op** (P1 Knight, P2 Mage, per-player turns)
|
||||||
- [x] Leveling (XP → stat growth) and game-over / respawn
|
- [x] Leveling (XP → stat growth) and game-over / respawn
|
||||||
- [x] **Self-hosting**: a Ludic-written compiler compiles its own source to a byte-exact fixpoint, and rebuilds itself from a checked-in IR seed with no C compiler in the loop (`selfhost/`, `./selfhost/bootstrap-cfree.sh`)
|
- [x] **Self-hosting**: a Ludic-written compiler compiles its own source to a byte-exact fixpoint, and rebuilds itself from a checked-in IR seed with no C compiler in the loop (`selfhost/`, `bin/x bootstrap-cfree`)
|
||||||
- [x] Snapshot save / load (full ECS World)
|
- [x] Snapshot save / load (full ECS World)
|
||||||
- [x] Two maps (overworld + dungeon) with map switching via the arch
|
- [x] Two maps (overworld + dungeon) with map switching via the arch
|
||||||
- [x] Boss encounter (Rift Warden) + victory condition
|
- [x] Boss encounter (Rift Warden) + victory condition
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
> **Status: S0 shipped; S1–S6 are design.** The base construct — `scene` /
|
> **Status: S0 shipped; S1–S6 are design.** The base construct — `scene` /
|
||||||
> `layer` / `on enter` / `on exit` / `become`, lowered to the implicit machine of
|
> `layer` / `on enter` / `on exit` / `become`, lowered to the implicit machine of
|
||||||
> §3 and §9 — is implemented and tested ([`examples/scenes.ludic`](examples/scenes.ludic),
|
> §3 and §9 — is implemented and tested ([`examples/scenes.ludic`](examples/scenes.ludic),
|
||||||
> a `test.sh` check). The extensions in §4–§8 (scene-owned entities, richer
|
> a `bin/x test` check). The extensions in §4–§8 (scene-owned entities, richer
|
||||||
> layers, the overlay stack, scene-local state, transition parameters) are still
|
> layers, the overlay stack, scene-local state, transition parameters) are still
|
||||||
> design targets. This document reaches deliberately past the thin sketch so we
|
> design targets. This document reaches deliberately past the thin sketch so we
|
||||||
> can decide the shape before building each one. §11 lists the open decisions.
|
> can decide the shape before building each one. §11 lists the open decisions.
|
||||||
|
|
@ -310,7 +310,7 @@ Each is independently shippable and testable, matching how the repo phases work.
|
||||||
exit`/`become` lowered to the implicit `machine`; the active scene is
|
exit`/`become` lowered to the implicit `machine`; the active scene is
|
||||||
snapshotted per phase so exactly one scene's layers dispatch in any phase.
|
snapshotted per phase so exactly one scene's layers dispatch in any phase.
|
||||||
[`examples/scenes.ludic`](examples/scenes.ludic) compiles, runs, and is checked
|
[`examples/scenes.ludic`](examples/scenes.ludic) compiles, runs, and is checked
|
||||||
by `test.sh`. This is the floor everything else builds on.
|
by `bin/x test`. This is the floor everything else builds on.
|
||||||
- **S1 — `@OnEnter`/`@OnExit` annotation form** (E5, cheap once S0 exists).
|
- **S1 — `@OnEnter`/`@OnExit` annotation form** (E5, cheap once S0 exists).
|
||||||
- **S2 — layer toggle & pause** (E2) on top of the existing `enable`/`disable`.
|
- **S2 — layer toggle & pause** (E2) on top of the existing `enable`/`disable`.
|
||||||
✅ *Toggle shipped* (via EVENTS-DESIGN EV1 layers): `enable layer L` / `disable
|
✅ *Toggle shipped* (via EVENTS-DESIGN EV1 layers): `enable layer L` / `disable
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ the spec and the compiler, then unifies the grammar around two rules. Scope:
|
||||||
**full redesign (Phases 0–5)**. Named-field direction: **colon everywhere**.
|
**full redesign (Phases 0–5)**. Named-field direction: **colon everywhere**.
|
||||||
|
|
||||||
> Status: **Phases 1–5 complete.** Every phase kept the compiler self-hosting to
|
> Status: **Phases 1–5 complete.** Every phase kept the compiler self-hosting to
|
||||||
> a fixpoint (`./test.sh` 14/14), and each syntax migration was proven
|
> a fixpoint (`bin/x test` 14/14), and each syntax migration was proven
|
||||||
> behaviour-preserving (the migrated compiler compiles itself to byte-identical
|
> behaviour-preserving (the migrated compiler compiles itself to byte-identical
|
||||||
> IR; every golden game renders byte-identically). Landed on branch
|
> IR; every golden game renders byte-identically). Landed on branch
|
||||||
> `syntax-redesign-phase2` over a committed baseline on `main`.
|
> `syntax-redesign-phase2` over a committed baseline on `main`.
|
||||||
|
|
@ -173,8 +173,8 @@ machine R_PHASE { var phase: Phase = Phase.KnightMenu
|
||||||
|
|
||||||
## Phase sequence
|
## Phase sequence
|
||||||
|
|
||||||
Each phase is independently shippable and ends green on `./test.sh` +
|
Each phase is independently shippable and ends green on `bin/x test` +
|
||||||
`./selfhost/test.sh` (fixpoint).
|
`bin/x selfhost-test` (fixpoint).
|
||||||
|
|
||||||
### Phase 0 — Doctrine (done here)
|
### Phase 0 — Doctrine (done here)
|
||||||
Rules A and B above; colon-everywhere; `@`-annotations as the single modifier
|
Rules A and B above; colon-everywhere; `@`-annotations as the single modifier
|
||||||
|
|
@ -198,14 +198,14 @@ Made spec ⇄ compiler agree **before** any grammar change. What landed:
|
||||||
- ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section
|
- ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section
|
||||||
updated in [LANGUAGE.md](LANGUAGE.md); scenes/reads-writes/dropped-CLI-flags now
|
updated in [LANGUAGE.md](LANGUAGE.md); scenes/reads-writes/dropped-CLI-flags now
|
||||||
listed there.
|
listed there.
|
||||||
- ✅ **`test.sh` guards drift** — added a `qsmoke qdecl` compile check. Suite
|
- ✅ **`bin/x test` guards drift** — added a `qsmoke qdecl` compile check. Suite
|
||||||
green (14/14 incl. the toolchain agent's CLI smoke tests).
|
green (14/14 incl. the toolchain agent's CLI smoke tests).
|
||||||
- ✅ **CLI flags** (#10) — `--shared`/`--fmt`/wasm noted as dropped-with-the-C-driver
|
- ✅ **CLI flags** (#10) — `--shared`/`--fmt`/wasm noted as dropped-with-the-C-driver
|
||||||
in LANGUAGE.md; `-o`/`--emit-llvm` were being re-added by the toolchain agent
|
in LANGUAGE.md; `-o`/`--emit-llvm` were being re-added by the toolchain agent
|
||||||
(now real, verified in `test.sh`); COMPILING.md updated by that agent.
|
(now real, verified in `bin/x test`); COMPILING.md updated by that agent.
|
||||||
- Deferred (intentionally): `pure`-is-ignored (#5) is undocumented and harmless;
|
- Deferred (intentionally): `pure`-is-ignored (#5) is undocumented and harmless;
|
||||||
it will be folded into `@pure` in Phase 3 rather than churned now.
|
it will be folded into `@pure` in Phase 3 rather than churned now.
|
||||||
- **Not done / by design:** `scenes.ludic` is *not* added to `test.sh` (it can't
|
- **Not done / by design:** `scenes.ludic` is *not* added to `bin/x test` (it can't
|
||||||
compile yet — a positive test would fail; the header note + LANGUAGE.md warning
|
compile yet — a positive test would fail; the header note + LANGUAGE.md warning
|
||||||
cover the drift instead).
|
cover the drift instead).
|
||||||
|
|
||||||
|
|
@ -226,7 +226,7 @@ Landed on branch `syntax-redesign-phase2` (baseline committed on `main` first).
|
||||||
seed** and every golden game rendering identically. ~1100 boundaries across the
|
seed** and every golden game rendering identically. ~1100 boundaries across the
|
||||||
corpus (examples, runtime, and the 25 self-host fragments).
|
corpus (examples, runtime, and the 25 self-host fragments).
|
||||||
- ✅ **Reseeded** to the strict compiler (19557 lines); C-free bootstrap fixpoint
|
- ✅ **Reseeded** to the strict compiler (19557 lines); C-free bootstrap fixpoint
|
||||||
holds; `./test.sh` 14/14; all goldens byte-identical; qdecl runs correctly.
|
holds; `bin/x test` 14/14; all goldens byte-identical; qdecl runs correctly.
|
||||||
- ✅ **Docs updated** — Rule B documented in LANGUAGE.md §Statements; BOOTSTRAP.md
|
- ✅ **Docs updated** — Rule B documented in LANGUAGE.md §Statements; BOOTSTRAP.md
|
||||||
R1 (which advertised no-separator juxtaposition as legal) and its stale code
|
R1 (which advertised no-separator juxtaposition as legal) and its stale code
|
||||||
fences updated; `check-docs` (now a live strict parse gate) green across all docs.
|
fences updated; `check-docs` (now a live strict parse gate) green across all docs.
|
||||||
|
|
@ -248,7 +248,7 @@ agree). Flagged to the toolchain owners.
|
||||||
The `=` is now assignment/const/default/extern-binding only. Migration tool:
|
The `=` is now assignment/const/default/extern-binding only. Migration tool:
|
||||||
[migrate_records.c](tools/ludic-tools/migrate_records.c) (spawn-context aware).
|
[migrate_records.c](tools/ludic-tools/migrate_records.c) (spawn-context aware).
|
||||||
Records live only in games, so the seed was unaffected; verified every golden
|
Records live only in games, so the seed was unaffected; verified every golden
|
||||||
byte-identical, old `=` form now rejected, reseeded, `test.sh` 14/14. Doc examples
|
byte-identical, old `=` form now rejected, reseeded, `bin/x test` 14/14. Doc examples
|
||||||
updated (LANGUAGE.md, BOOTSTRAP.md R2).
|
updated (LANGUAGE.md, BOOTSTRAP.md R2).
|
||||||
|
|
||||||
**3b — ui props → `key: value` ✅ DONE.** `panel id=Root w=288` → `panel id: Root
|
**3b — ui props → `key: value` ✅ DONE.** `panel id=Root w=288` → `panel id: Root
|
||||||
|
|
@ -257,7 +257,7 @@ Chose the **colonized** form over parenthesized named-args: it satisfies Rule A
|
||||||
(the `=` overload is gone) with minimal churn, needs no new grammar, and `emit_ui`
|
(the `=` overload is gone) with minimal churn, needs no new grammar, and `emit_ui`
|
||||||
(which reads the AST) and `ludic-fmt` (which formats `:` correctly by default)
|
(which reads the AST) and `ludic-fmt` (which formats `:` correctly by default)
|
||||||
were both untouched. Migration: [migrate_ui.c](tools/ludic-tools/migrate_ui.c).
|
were both untouched. Migration: [migrate_ui.c](tools/ludic-tools/migrate_ui.c).
|
||||||
menu golden byte-identical, old `=` form rejected, reseeded, `test.sh` 14/14.
|
menu golden byte-identical, old `=` form rejected, reseeded, `bin/x test` 14/14.
|
||||||
(The parenthesized form `panel(id: Root, w: 288)` remains a possible future
|
(The parenthesized form `panel(id: Root, w: 288)` remains a possible future
|
||||||
refinement if the language ever gains named call arguments.)
|
refinement if the language ever gains named call arguments.)
|
||||||
|
|
||||||
|
|
@ -277,7 +277,7 @@ prefix forms now rejected. Behavior-identical: the export flag is parse-only in
|
||||||
the self-hosted emitter (it emits `@fn_<name>` for every function and never reads
|
the self-hosted emitter (it emits `@fn_<name>` for every function and never reads
|
||||||
the flag — the C-ABI-export capability is vestigial, a pre-existing gap), so
|
the flag — the C-ABI-export capability is vestigial, a pre-existing gap), so
|
||||||
`@export` and the old `export` produce byte-identical IR. Reseeded, fixpoint
|
`@export` and the old `export` produce byte-identical IR. Reseeded, fixpoint
|
||||||
holds, `test.sh` 14/14, goldens byte-identical.
|
holds, `bin/x test` 14/14, goldens byte-identical.
|
||||||
|
|
||||||
**Phase 3 is complete.** The `=`/`:` overload (finding #2) and the modifier-zoo
|
**Phase 3 is complete.** The `=`/`:` overload (finding #2) and the modifier-zoo
|
||||||
(findings #5, #6) are resolved; `:` associates and `=` binds throughout.
|
(findings #5, #6) are resolved; `:` associates and `=` binds throughout.
|
||||||
|
|
@ -359,7 +359,7 @@ Added an **annotation DSL**: `@Queries(these: [Prop{constraint}, …], on: Model
|
||||||
a handler desugars to the existing `S_QUERY` loop (each property binds by its own
|
a handler desugars to the existing `S_QUERY` loop (each property binds by its own
|
||||||
name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}`
|
name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}`
|
||||||
tag), and `@Handles(…)` on a program parses as documentation. See
|
tag), and `@Handles(…)` on a program parses as documentation. See
|
||||||
[examples/annotations.ludic](examples/annotations.ludic); test.sh 15/15. All thirteen findings are resolved or resolved by an
|
[examples/annotations.ludic](examples/annotations.ludic); bin/x test 15/15. All thirteen findings are resolved or resolved by an
|
||||||
explicit, documented decision.
|
explicit, documented decision.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
26
build-cli.sh
26
build-cli.sh
|
|
@ -1,26 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# build-cli.sh — build the self-hosted Ludic front-end binaries.
|
|
||||||
#
|
|
||||||
# Both `ludicc` and `ludic` are the SAME native binary, compiled from the Ludic
|
|
||||||
# compiler's own IR seed (selfhost/ludicc.seed.ll) with clang alone — no C
|
|
||||||
# compiler. The binary is multi-call: it looks at argv[0], so invoked as
|
|
||||||
# `ludicc` it compiles, and invoked as `ludic` it compiles and runs.
|
|
||||||
#
|
|
||||||
# ./build-cli.sh
|
|
||||||
# ./ludicc examples/snake.ludic -o build/snake # compile
|
|
||||||
# ./ludic examples/snake.ludic # compile + play
|
|
||||||
#
|
|
||||||
# The binaries locate the runtime (runtime/native/cocoa.ll) via $LUDIC_HOME,
|
|
||||||
# defaulting to the directory they sit in — so keep them at the repo root, or
|
|
||||||
# set LUDIC_HOME and put them on your PATH.
|
|
||||||
set -eu
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
CC="${LUDIC_CC:-clang}"
|
|
||||||
mkdir -p build
|
|
||||||
|
|
||||||
echo "cc: selfhost/ludicc.seed.ll -> ludicc/ludic (from the IR seed, no C compiler)"
|
|
||||||
$CC selfhost/ludicc.seed.ll -o build/ludicc
|
|
||||||
cp build/ludicc ludicc
|
|
||||||
cp build/ludicc ludic
|
|
||||||
chmod +x ludicc ludic
|
|
||||||
echo "done. built ./ludicc and ./ludic"
|
|
||||||
52
build.sh
52
build.sh
|
|
@ -1,52 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# build.sh — compile a Ludic program with the self-hosted Ludic compiler.
|
|
||||||
#
|
|
||||||
# The compiler is written in Ludic (selfhost/*.ludic). It is built from the
|
|
||||||
# checked-in LLVM-IR seed (selfhost/ludicc.seed.ll) with clang alone — no C
|
|
||||||
# compiler is involved anywhere. clang then assembles the emitted IR and links
|
|
||||||
# it; a windowed build also links the hand-written platform layer (cocoa.ll).
|
|
||||||
#
|
|
||||||
# ./build.sh examples/snake.ludic # windowed native app
|
|
||||||
# ./build.sh examples/snake.ludic --headless # headless (renders out.ppm)
|
|
||||||
set -u
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
CC="${LUDIC_CC:-clang}"
|
|
||||||
mkdir -p build
|
|
||||||
|
|
||||||
# the compiler itself: assembled from the IR seed (C-free)
|
|
||||||
if [ ! -x build/ludicc ] || [ selfhost/ludicc.seed.ll -nt build/ludicc ]; then
|
|
||||||
echo "cc: selfhost/ludicc.seed.ll -> build/ludicc (from the IR seed, no C compiler)"
|
|
||||||
$CC selfhost/ludicc.seed.ll -o build/ludicc || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SRC=""; MODE=windowed; SAVE=0
|
|
||||||
for a in "$@"; do
|
|
||||||
case "$a" in
|
|
||||||
--headless) MODE=headless ;;
|
|
||||||
--windowed) MODE=windowed ;;
|
|
||||||
--save-temps) SAVE=1 ;;
|
|
||||||
-*) ;;
|
|
||||||
*) SRC="$a" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[ -n "$SRC" ] || { echo "usage: ./build.sh <file.ludic> [--headless]"; exit 1; }
|
|
||||||
NAME="$(basename "${SRC%.ludic}")"
|
|
||||||
|
|
||||||
case "$MODE" in
|
|
||||||
windowed)
|
|
||||||
OUT="build/$NAME"
|
|
||||||
echo "ludicc: $SRC -> $OUT (Ludic-written compiler -> LLVM IR -> windowed binary)"
|
|
||||||
build/ludicc --windowed "$SRC" > "$OUT.ll" || exit 1
|
|
||||||
$CC -O2 "$OUT.ll" runtime/native/cocoa.ll -framework Cocoa -Wl,-rpath,@loader_path -o "$OUT" || exit 1
|
|
||||||
[ "$SAVE" = 1 ] || rm -f "$OUT.ll"
|
|
||||||
echo "done. run: ./$OUT (from the repo root, so assets/ resolves)"
|
|
||||||
;;
|
|
||||||
headless)
|
|
||||||
OUT="build/${NAME}_headless"
|
|
||||||
echo "ludicc: $SRC -> $OUT (renders the last frame to out.ppm)"
|
|
||||||
build/ludicc --headless "$SRC" > "$OUT.ll" || exit 1
|
|
||||||
$CC -O2 "$OUT.ll" -o "$OUT" || exit 1
|
|
||||||
[ "$SAVE" = 1 ] || rm -f "$OUT.ll"
|
|
||||||
echo "done. run: printf 'ddss' | ./$OUT && open out.ppm"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
7
docs/language/collide/_section.md
Normal file
7
docs/language/collide/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: collide
|
||||||
|
title: Collide
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
2D overlap tests on integer coordinates (pixels or tiles). Rectangles are <code>(x, y, w, h)</code> from the top-left; circles are <code>(x, y, r)</code>. Each returns a bool. Squared distances are computed in 64-bit so large coordinates never overflow.
|
||||||
22
docs/language/collide/collide-circles.md
Normal file
22
docs/language/collide/collide-circles.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: collide-circles
|
||||||
|
name: Collide.circles
|
||||||
|
category: collide
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Collide.circles
|
||||||
|
sig: Collide.circles(ax, ay, ar, bx, by, br) -> bool
|
||||||
|
tip: Do two circles overlap?
|
||||||
|
order: 2
|
||||||
|
ns: Collide
|
||||||
|
member: circles
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>true</code> when circles centred at <code>(ax, ay)</code> and <code>(bx, by)</code> with radii <code>ar</code> and <code>br</code> overlap — that is, when the distance between centres is at most <code>ar + br</code>. It compares squared distances internally, so there is no square root and no precision loss.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
if Collide.circles(px, py, 8, ex, ey, 8) { collide() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/collide/collide-point_rect.md
Normal file
22
docs/language/collide/collide-point_rect.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: collide-point_rect
|
||||||
|
name: Collide.point_rect
|
||||||
|
category: collide
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Collide.point_rect
|
||||||
|
sig: Collide.point_rect(px, py, rx, ry, rw, rh) -> bool
|
||||||
|
tip: Is a point inside a rectangle?
|
||||||
|
order: 1
|
||||||
|
ns: Collide
|
||||||
|
member: point_rect
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>true</code> when the point <code>(px, py)</code> lies within the rectangle <code>(rx, ry, rw, rh)</code> — inclusive on the top-left edge, exclusive on the bottom-right. Use it for mouse or touch hit-testing against a button or a world region.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
if Collide.point_rect(mx, my, bx, by, bw, bh) { press() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/collide/collide-rect_circle.md
Normal file
22
docs/language/collide/collide-rect_circle.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: collide-rect_circle
|
||||||
|
name: Collide.rect_circle
|
||||||
|
category: collide
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Collide.rect_circle
|
||||||
|
sig: Collide.rect_circle(rx, ry, rw, rh, cx, cy, cr) -> bool
|
||||||
|
tip: Does a rectangle overlap a circle?
|
||||||
|
order: 3
|
||||||
|
ns: Collide
|
||||||
|
member: rect_circle
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>true</code> when the rectangle <code>(rx, ry, rw, rh)</code> overlaps the circle centred at <code>(cx, cy)</code> with radius <code>cr</code>. It finds the point on the rectangle nearest the circle's centre and checks whether it falls within the radius — the correct test for a round actor against a blocky tile.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
if Collide.rect_circle(tx, ty, 16, 16, bx, by, 6) { block() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/collide/collide-rects.md
Normal file
22
docs/language/collide/collide-rects.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: collide-rects
|
||||||
|
name: Collide.rects
|
||||||
|
category: collide
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Collide.rects
|
||||||
|
sig: Collide.rects(ax, ay, aw, ah, bx, by, bw, bh) -> bool
|
||||||
|
tip: Do two rectangles overlap?
|
||||||
|
order: 0
|
||||||
|
ns: Collide
|
||||||
|
member: rects
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>true</code> when the axis-aligned rectangles <code>(ax, ay, aw, ah)</code> and <code>(bx, by, bw, bh)</code> overlap. Edges that merely touch do not count as overlapping. This is the standard broad-phase test for two hitboxes, tiles, or UI regions.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
if Collide.rects(px, py, 16, 16, ex, ey, 16, 16) { hit() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
7
docs/language/color/_section.md
Normal file
7
docs/language/color/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: color
|
||||||
|
title: Color functions
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Building and blending colors at runtime. Colors are <code>0x00RRGGBB</code> ints; these pack channels and transform an existing color. The named palette constants (<code>Color.Charcoal</code>, …) are a separate compile-time set.
|
||||||
24
docs/language/color/color-darken.md
Normal file
24
docs/language/color/color-darken.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: color-darken
|
||||||
|
name: Color.darken
|
||||||
|
category: color
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Color.darken
|
||||||
|
sig: Color.darken(c, amount) -> int
|
||||||
|
tip: Scale a color toward black.
|
||||||
|
order: 3
|
||||||
|
ns: Color
|
||||||
|
member: darken
|
||||||
|
---
|
||||||
|
|
||||||
|
Multiplies each channel of <code>c</code> by <code>1 - amount</code> (amount a fixed 0..1), returning a darker color. <code>Color.darken(c, 0.0)</code> is unchanged and <code>1.0</code> is black. Handy for shadows and pressed-button states.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler DrawWorld phase Render {
|
||||||
|
let c = Color.darken(Color.Red, 0.3)
|
||||||
|
Screen.clear(c)
|
||||||
|
Screen.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/language/color/color-lerp.md
Normal file
24
docs/language/color/color-lerp.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: color-lerp
|
||||||
|
name: Color.lerp
|
||||||
|
category: color
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Color.lerp
|
||||||
|
sig: Color.lerp(c0, c1, t) -> int
|
||||||
|
tip: Blend between two colors.
|
||||||
|
order: 2
|
||||||
|
ns: Color
|
||||||
|
member: lerp
|
||||||
|
---
|
||||||
|
|
||||||
|
Interpolates each channel of <code>c0</code> and <code>c1</code> by the fixed amount <code>t</code> in <code>0.0</code>..<code>1.0</code>, returning the blended color. Use it for gradients, fades between two tints, or a flash that returns to base.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler DrawWorld phase Render {
|
||||||
|
let c = Color.lerp(Color.Black, Color.White, 0.5)
|
||||||
|
Screen.clear(c)
|
||||||
|
Screen.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/language/color/color-lighten.md
Normal file
24
docs/language/color/color-lighten.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: color-lighten
|
||||||
|
name: Color.lighten
|
||||||
|
category: color
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Color.lighten
|
||||||
|
sig: Color.lighten(c, amount) -> int
|
||||||
|
tip: Scale a color toward white.
|
||||||
|
order: 4
|
||||||
|
ns: Color
|
||||||
|
member: lighten
|
||||||
|
---
|
||||||
|
|
||||||
|
Moves each channel of <code>c</code> a fraction <code>amount</code> of the way to 255, returning a lighter color — the counterpart to <code>Color.darken</code>. Good for highlights and hover states.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler DrawWorld phase Render {
|
||||||
|
let c = Color.lighten(Color.Blue, 0.3)
|
||||||
|
Screen.clear(c)
|
||||||
|
Screen.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/language/color/color-rgb.md
Normal file
24
docs/language/color/color-rgb.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: color-rgb
|
||||||
|
name: Color.rgb
|
||||||
|
category: color
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Color.rgb
|
||||||
|
sig: Color.rgb(r, g, b) -> int
|
||||||
|
tip: Build a color from red, green, blue.
|
||||||
|
order: 0
|
||||||
|
ns: Color
|
||||||
|
member: rgb
|
||||||
|
---
|
||||||
|
|
||||||
|
Packs three 0..255 channel values into a single <code>0xRRGGBB</code> color int. Use it to build a color from computed channels rather than a fixed literal or a palette name.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler DrawWorld phase Render {
|
||||||
|
let c = Color.rgb(255, 128, 64)
|
||||||
|
Screen.clear(c)
|
||||||
|
Screen.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/language/color/color-rgba.md
Normal file
24
docs/language/color/color-rgba.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: color-rgba
|
||||||
|
name: Color.rgba
|
||||||
|
category: color
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Color.rgba
|
||||||
|
sig: Color.rgba(r, g, b, a) -> int
|
||||||
|
tip: Build a color with an alpha byte.
|
||||||
|
order: 1
|
||||||
|
ns: Color
|
||||||
|
member: rgba
|
||||||
|
---
|
||||||
|
|
||||||
|
Packs <code>r</code>, <code>g</code>, <code>b</code>, and an alpha <code>a</code> (0..255) into a <code>0xAARRGGBB</code> int. The software renderer draws opaque, but the alpha byte is preserved for your own blending or storage.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler DrawWorld phase Render {
|
||||||
|
let c = Color.rgba(255, 128, 64, 200)
|
||||||
|
Screen.clear(c)
|
||||||
|
Screen.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/language/color/color-with_alpha.md
Normal file
24
docs/language/color/color-with_alpha.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: color-with_alpha
|
||||||
|
name: Color.with_alpha
|
||||||
|
category: color
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Color.with_alpha
|
||||||
|
sig: Color.with_alpha(c, a) -> int
|
||||||
|
tip: Replace a color's alpha byte.
|
||||||
|
order: 5
|
||||||
|
ns: Color
|
||||||
|
member: with_alpha
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>c</code> with its alpha byte set to <code>a</code> (0..255), leaving the RGB channels untouched.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler DrawWorld phase Render {
|
||||||
|
let c = Color.with_alpha(Color.Green, 128)
|
||||||
|
Screen.clear(c)
|
||||||
|
Screen.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/control/kw-break.md
Normal file
27
docs/language/control/kw-break.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: kw-break
|
||||||
|
name: break
|
||||||
|
category: control
|
||||||
|
kind: keyword
|
||||||
|
tokens: break
|
||||||
|
sig: break
|
||||||
|
tip: Leave the enclosing loop immediately.
|
||||||
|
order: 9
|
||||||
|
---
|
||||||
|
|
||||||
|
<code>break</code> exits the innermost <code>while</code> or <code>for</code> loop at once, skipping the rest of the body and any remaining iterations, and continues with the code after the loop. Use it to stop early the moment you have what you need — the first match in a scan, a collision that ends the sweep, or a guard inside <code>while true</code> that decides when to leave. It affects only the loop that contains it; an outer loop keeps running.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program FindFirst {
|
||||||
|
var found: int = 0 - 1
|
||||||
|
|
||||||
|
handler Scan phase Update {
|
||||||
|
for i in 0 .. 10 {
|
||||||
|
if i * i > 20 {
|
||||||
|
found = i
|
||||||
|
break # stop at the first i whose square exceeds 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
25
docs/language/control/kw-continue.md
Normal file
25
docs/language/control/kw-continue.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: kw-continue
|
||||||
|
name: continue
|
||||||
|
category: control
|
||||||
|
kind: keyword
|
||||||
|
tokens: continue
|
||||||
|
sig: continue
|
||||||
|
tip: Skip to the next iteration of the loop.
|
||||||
|
order: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
<code>continue</code> abandons the rest of the current loop pass and jumps straight to the next one — the next condition check in a <code>while</code>, or the next index in a <code>for</code>. Use it to skip elements that do not apply without nesting the rest of the body inside an <code>if</code>: filter out empty slots, ignore inactive entities, or step over even numbers. Like <code>break</code>, it acts on the innermost loop only.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program SumOdds {
|
||||||
|
var total: int = 0
|
||||||
|
|
||||||
|
handler Add phase Update {
|
||||||
|
for i in 0 .. 10 {
|
||||||
|
if i % 2 == 0 { continue } # skip even numbers
|
||||||
|
total = total + i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
25
docs/language/control/kw-where.md
Normal file
25
docs/language/control/kw-where.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: kw-where
|
||||||
|
name: where
|
||||||
|
category: control
|
||||||
|
kind: keyword
|
||||||
|
tokens: where
|
||||||
|
sig: for (Binds) in query [Comps] where cond { … }
|
||||||
|
tip: Filter a query loop to entities that satisfy a condition.
|
||||||
|
order: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
<code>where</code> attaches a boolean condition to a <code>query</code> loop, so the body runs only for entities whose components satisfy it — the rows that fail the test are skipped entirely. The condition is an ordinary expression over the bound components, such as <code>where Battle.hp <= 0 and Battle.side == 1</code>. It is the imperative twin of a component filter written inside a <code>@Queries</code> annotation, and keeps the guard next to the loop instead of repeating an <code>if</code> at the top of the body.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Cleanup {
|
||||||
|
property Health { hp: int = 0 }
|
||||||
|
model Foe { Health }
|
||||||
|
|
||||||
|
handler ReapDead phase LateUpdate {
|
||||||
|
for (Health) in query [Health, {Foe}] where Health.hp <= 0 {
|
||||||
|
# only dead foes reach here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
7
docs/language/ease/_section.md
Normal file
7
docs/language/ease/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: ease
|
||||||
|
title: Ease
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Tween curves — the "juice" layer. Each takes a normalized amount <code>t</code> in <code>0.0</code>..<code>1.0</code> and returns an eased <code>fixed</code>, ready to feed to <code>Math.lerp</code>. All deterministic fixed-point.
|
||||||
22
docs/language/ease/ease-back.md
Normal file
22
docs/language/ease/ease-back.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: ease-back
|
||||||
|
name: Ease.back
|
||||||
|
category: ease
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Ease.back
|
||||||
|
sig: Ease.back(t) -> fixed
|
||||||
|
tip: Ease in with a small backward anticipation.
|
||||||
|
order: 3
|
||||||
|
ns: Ease
|
||||||
|
member: back
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns an ease-in that first dips slightly below <code>0</code> before shooting forward — the anticipation windup that gives a motion snap. Because it undershoots, the eased value can go negative near the start, so use it where a little overshoot reads as lively rather than wrong.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let s = Math.lerp(rest, poke, Ease.back(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/ease/ease-bounce.md
Normal file
22
docs/language/ease/ease-bounce.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: ease-bounce
|
||||||
|
name: Ease.bounce
|
||||||
|
category: ease
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Ease.bounce
|
||||||
|
sig: Ease.bounce(t) -> fixed
|
||||||
|
tip: Ease out with a settling bounce.
|
||||||
|
order: 4
|
||||||
|
ns: Ease
|
||||||
|
member: bounce
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns an ease-out that overshoots and bounces a few times before settling at <code>1.0</code>, like a ball dropping to the floor. Great for coins landing, badges popping, or a menu that lands with character.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let y = Math.lerp(top, floor_y, Ease.bounce(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/ease/ease-in.md
Normal file
22
docs/language/ease/ease-in.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: ease-in
|
||||||
|
name: Ease.in
|
||||||
|
category: ease
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Ease.in
|
||||||
|
sig: Ease.in(t) -> fixed
|
||||||
|
tip: Accelerate from rest (quadratic ease-in).
|
||||||
|
order: 0
|
||||||
|
ns: Ease
|
||||||
|
member: in
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>t * t</code> — motion that starts slow and speeds up. Feed the result to <code>Math.lerp</code> as the blend amount so a value accelerates into its target. The input <code>t</code> is expected in <code>0.0</code>..<code>1.0</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let x = Math.lerp(start_x, end_x, Ease.in(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/ease/ease-in_out.md
Normal file
22
docs/language/ease/ease-in_out.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: ease-in_out
|
||||||
|
name: Ease.in_out
|
||||||
|
category: ease
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Ease.in_out
|
||||||
|
sig: Ease.in_out(t) -> fixed
|
||||||
|
tip: Ease in and then out (smoothstep).
|
||||||
|
order: 2
|
||||||
|
ns: Ease
|
||||||
|
member: in_out
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the smooth <code>3t^2 - 2t^3</code> S-curve: slow at both ends, fastest in the middle. Use it for a polished there-and-back or point-to-point move that neither jerks off the mark nor slams into the target.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let a = Math.lerp(0.0, 1.0, Ease.in_out(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/ease/ease-out.md
Normal file
22
docs/language/ease/ease-out.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: ease-out
|
||||||
|
name: Ease.out
|
||||||
|
category: ease
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Ease.out
|
||||||
|
sig: Ease.out(t) -> fixed
|
||||||
|
tip: Decelerate to rest (quadratic ease-out).
|
||||||
|
order: 1
|
||||||
|
ns: Ease
|
||||||
|
member: out
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>t * (2 - t)</code> — motion that starts fast and slows as it arrives. The most natural-feeling easing for things settling into place, like a panel sliding in or a camera coming to rest.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let y = Math.lerp(top, rest_y, Ease.out(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
7
docs/language/list/_section.md
Normal file
7
docs/language/list/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: list
|
||||||
|
title: List
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Operations over <code>[]T</code> slices — length, ends access, push/pop, swap, search, and in-place reverse. A slice is a shared growable buffer, so these mutate it in place and every holder sees the change. Arguments are positional.
|
||||||
27
docs/language/list/list-clear.md
Normal file
27
docs/language/list/list-clear.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: list-clear
|
||||||
|
name: List.clear
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.clear
|
||||||
|
sig: List.clear(s) -> void
|
||||||
|
tip: Remove all elements, keeping capacity.
|
||||||
|
order: 2
|
||||||
|
ns: List
|
||||||
|
member: clear
|
||||||
|
---
|
||||||
|
|
||||||
|
Empties <code>s</code> by setting its length back to zero. The backing buffer is kept, so refilling the slice afterward reuses the same memory without reallocating. Use it to reset a per-frame list — collisions, visible tiles, particles to draw — at the start of each update instead of building a new slice every frame.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to empty
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program FrameHits {
|
||||||
|
var hits: []int = new []int
|
||||||
|
|
||||||
|
handler BeginFrame phase Update {
|
||||||
|
List.clear(hits) # start the frame with an empty list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
31
docs/language/list/list-contains.md
Normal file
31
docs/language/list/list-contains.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
id: list-contains
|
||||||
|
name: List.contains
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.contains
|
||||||
|
sig: List.contains(s, v) -> bool
|
||||||
|
tip: Whether a value appears in a slice.
|
||||||
|
order: 7
|
||||||
|
ns: List
|
||||||
|
member: contains
|
||||||
|
---
|
||||||
|
|
||||||
|
Scans <code>s</code> and returns <code>true</code> if any element equals <code>v</code>. Comparison is by value for scalar elements (ints, entities, booleans) and by identity for reference elements. Use it for membership tests — has this tile been visited, is this entity already in the target list, is the achievement unlocked. It is a linear scan, so for very large or hot-path sets a dedicated structure will be faster.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to search
|
||||||
|
- `v` — the value to look for
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Visited {
|
||||||
|
var seen: []int = new []int
|
||||||
|
|
||||||
|
handler Step phase Update {
|
||||||
|
let tile = 42
|
||||||
|
if not List.contains(seen, tile) {
|
||||||
|
List.push(seen, tile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
29
docs/language/list/list-first.md
Normal file
29
docs/language/list/list-first.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
id: list-first
|
||||||
|
name: List.first
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.first
|
||||||
|
sig: List.first(s) -> T
|
||||||
|
tip: The first element of a slice.
|
||||||
|
order: 3
|
||||||
|
ns: List
|
||||||
|
member: first
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns element <code>0</code> of <code>s</code> — the same as <code>s[0]</code>, named for readability. The slice must hold at least one element; calling it on an empty slice reads out of bounds, so guard with <code>List.len(s) > 0</code> when that is possible. Use it to peek at the head of a queue or the front of an ordered list.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — a non-empty slice
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Queue {
|
||||||
|
var waiting: []int = new []int
|
||||||
|
|
||||||
|
handler Serve phase Update {
|
||||||
|
if List.len(waiting) > 0 {
|
||||||
|
let next = List.first(waiting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
31
docs/language/list/list-index_of.md
Normal file
31
docs/language/list/list-index_of.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
id: list-index_of
|
||||||
|
name: List.index_of
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.index_of
|
||||||
|
sig: List.index_of(s, v) -> int
|
||||||
|
tip: The index of a value in a slice, or -1 if absent.
|
||||||
|
order: 8
|
||||||
|
ns: List
|
||||||
|
member: index_of
|
||||||
|
---
|
||||||
|
|
||||||
|
Scans <code>s</code> and returns the index of the first element equal to <code>v</code>, or <code>-1</code> if none match. It uses the same value/identity comparison as <code>List.contains</code>, but tells you where the match is so you can act on it — swap it out, read a parallel slice at the same index, or guard with <code>if idx >= 0</code>. Only the first match is reported.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to search
|
||||||
|
- `v` — the value to locate
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program FindSlot {
|
||||||
|
var ids: []int = new []int
|
||||||
|
|
||||||
|
handler Locate phase Update {
|
||||||
|
let slot = List.index_of(ids, 99)
|
||||||
|
if slot >= 0 {
|
||||||
|
List.swap(ids, slot, List.len(ids) - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/list/list-insert.md
Normal file
22
docs/language/list/list-insert.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: list-insert
|
||||||
|
name: List.insert
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.insert
|
||||||
|
sig: List.insert(s, i, v) -> void
|
||||||
|
tip: Insert an element at an index, shifting the rest up.
|
||||||
|
order: 10
|
||||||
|
ns: List
|
||||||
|
member: insert
|
||||||
|
---
|
||||||
|
|
||||||
|
Inserts <code>v</code> at index <code>i</code>, moving the elements at and after <code>i</code> one place toward the end and growing the backing buffer if needed. Use it to keep a list ordered as you build it, or to splice an item into the middle. Appending is cheaper with <code>List.push</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
List.insert(queue, 0, next_id) # push to the front
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
29
docs/language/list/list-last.md
Normal file
29
docs/language/list/list-last.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
id: list-last
|
||||||
|
name: List.last
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.last
|
||||||
|
sig: List.last(s) -> T
|
||||||
|
tip: The last element of a slice.
|
||||||
|
order: 4
|
||||||
|
ns: List
|
||||||
|
member: last
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the final element of <code>s</code> — the value at index <code>List.len(s) - 1</code>. Like <code>List.first</code>, it assumes the slice is non-empty, so guard with a length check when the slice might be empty. Use it to read the most recently pushed item without removing it; use <code>List.pop</code> when you want to take it off.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — a non-empty slice
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Trail {
|
||||||
|
var points: []int = new []int
|
||||||
|
|
||||||
|
handler Head phase Update {
|
||||||
|
if List.len(points) > 0 {
|
||||||
|
let newest = List.last(points)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/list/list-len.md
Normal file
27
docs/language/list/list-len.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: list-len
|
||||||
|
name: List.len
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.len
|
||||||
|
sig: List.len(s) -> int
|
||||||
|
tip: The number of elements in a slice.
|
||||||
|
order: 0
|
||||||
|
ns: List
|
||||||
|
member: len
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns how many elements <code>s</code> currently holds. It reads the slice's length, which changes as you <code>List.push</code> and <code>List.pop</code>, and is the same value as the bare <code>len(s)</code>. Use it to bound a loop, test for emptiness, or find the last index with <code>List.len(s) - 1</code>.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to measure
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program CountEnemies {
|
||||||
|
var enemies: []int = new []int
|
||||||
|
|
||||||
|
handler Report phase Update {
|
||||||
|
let n = List.len(enemies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
29
docs/language/list/list-pop.md
Normal file
29
docs/language/list/list-pop.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
id: list-pop
|
||||||
|
name: List.pop
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.pop
|
||||||
|
sig: List.pop(s) -> T
|
||||||
|
tip: Remove and return the last element.
|
||||||
|
order: 5
|
||||||
|
ns: List
|
||||||
|
member: pop
|
||||||
|
---
|
||||||
|
|
||||||
|
Removes the final element of <code>s</code> and returns it, shortening the slice by one. Together with <code>List.push</code> this makes a slice work as a stack: push to add, pop to take the most recent back off. The slice must be non-empty. Use it to unwind a history, process a work list, or undo the last action.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — a non-empty slice
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program UndoStack {
|
||||||
|
var history: []int = new []int
|
||||||
|
|
||||||
|
handler Undo phase Update {
|
||||||
|
if List.len(history) > 0 {
|
||||||
|
let last_action = List.pop(history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/list/list-push.md
Normal file
28
docs/language/list/list-push.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: list-push
|
||||||
|
name: List.push
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.push
|
||||||
|
sig: List.push(s, v) -> void
|
||||||
|
tip: Append an element to the end of a slice.
|
||||||
|
order: 1
|
||||||
|
ns: List
|
||||||
|
member: push
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds <code>v</code> to the end of <code>s</code>, growing the backing buffer if needed, so the slice's length increases by one. It is the same operation as the bare <code>push(s, v)</code>. Because a slice is shared, the new element is visible to every holder. Use it to collect spawned entities, build a list of hits, or fill an inventory.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to append to
|
||||||
|
- `v` — the element to add (matching the slice's element type)
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Collect {
|
||||||
|
var picked: []int = new []int
|
||||||
|
|
||||||
|
handler Grab phase Update {
|
||||||
|
List.push(picked, 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/list/list-remove.md
Normal file
22
docs/language/list/list-remove.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: list-remove
|
||||||
|
name: List.remove
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.remove
|
||||||
|
sig: List.remove(s, v) -> void
|
||||||
|
tip: Remove the first element equal to a value.
|
||||||
|
order: 12
|
||||||
|
ns: List
|
||||||
|
member: remove
|
||||||
|
---
|
||||||
|
|
||||||
|
Scans for the first element equal to <code>v</code> and removes it (shifting the rest down); if no element matches, the slice is unchanged. Comparison is by value for scalars and by identity for reference elements, matching <code>List.contains</code>. Only the first match is removed.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
List.remove(active, dead_entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/list/list-remove_at.md
Normal file
22
docs/language/list/list-remove_at.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: list-remove_at
|
||||||
|
name: List.remove_at
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.remove_at
|
||||||
|
sig: List.remove_at(s, i) -> void
|
||||||
|
tip: Remove the element at an index, shifting the rest down.
|
||||||
|
order: 11
|
||||||
|
ns: List
|
||||||
|
member: remove_at
|
||||||
|
---
|
||||||
|
|
||||||
|
Removes the element at index <code>i</code>, moving everything after it one place toward the front and shortening the slice by one. Order is preserved. When order does not matter, swapping the item to the end and calling <code>List.pop</code> is cheaper.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
List.remove_at(effects, expired)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/list/list-reverse.md
Normal file
28
docs/language/list/list-reverse.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: list-reverse
|
||||||
|
name: List.reverse
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.reverse
|
||||||
|
sig: List.reverse(s) -> void
|
||||||
|
tip: Reverse the order of a slice in place.
|
||||||
|
order: 9
|
||||||
|
ns: List
|
||||||
|
member: reverse
|
||||||
|
---
|
||||||
|
|
||||||
|
Reverses <code>s</code> so its first element becomes last and vice versa, mutating the slice in place rather than returning a copy. Use it to flip an order you built back-to-front — a path traced from goal to start, an undo stack you want oldest-first, or a list you appended to and now want to read in the opposite direction.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to reverse
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program PathOrder {
|
||||||
|
var path: []int = new []int
|
||||||
|
|
||||||
|
handler Finalize phase Update {
|
||||||
|
# path was built from the goal back to the start
|
||||||
|
List.reverse(path) # now start -> goal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/list/list-sort.md
Normal file
22
docs/language/list/list-sort.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: list-sort
|
||||||
|
name: List.sort
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.sort
|
||||||
|
sig: List.sort(s) -> void
|
||||||
|
tip: Sort a slice in ascending order, in place.
|
||||||
|
order: 13
|
||||||
|
ns: List
|
||||||
|
member: sort
|
||||||
|
---
|
||||||
|
|
||||||
|
Sorts the elements of <code>s</code> in ascending order in place, using an insertion sort. It is intended for slices of scalar elements (ints, entities, fixed) where <code><</code> is meaningful; reference elements are ordered by identity, which is rarely useful. Insertion sort is simple and fast for the small, nearly-sorted lists games usually hold.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
List.sort(scores)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
32
docs/language/list/list-swap.md
Normal file
32
docs/language/list/list-swap.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
id: list-swap
|
||||||
|
name: List.swap
|
||||||
|
category: list
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: List.swap
|
||||||
|
sig: List.swap(s, i, j) -> void
|
||||||
|
tip: Exchange the elements at two indices.
|
||||||
|
order: 6
|
||||||
|
ns: List
|
||||||
|
member: swap
|
||||||
|
---
|
||||||
|
|
||||||
|
Exchanges the elements at indices <code>i</code> and <code>j</code> in <code>s</code>, in place. Both indices must be within the slice. Swapping is the building block of shuffles, sorts, and moving an item to the end before <code>List.pop</code> to remove it cheaply without shifting the rest.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `s` — the slice to modify
|
||||||
|
- `i` — the first index
|
||||||
|
- `j` — the second index
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program RemoveFast {
|
||||||
|
var actors: []int = new []int
|
||||||
|
|
||||||
|
handler Kill phase Update {
|
||||||
|
let dead = 2
|
||||||
|
let last = List.len(actors) - 1
|
||||||
|
List.swap(actors, dead, last) # move it to the end...
|
||||||
|
let removed = List.pop(actors) # ...then drop it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
7
docs/language/math/_section.md
Normal file
7
docs/language/math/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: math
|
||||||
|
title: Math
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Deterministic fixed-point math. Every function is computed in Q16.16 with plain integer arithmetic, so results are bit-identical on every platform and every run — the same guarantee the rest of the runtime gives. Arguments are positional.
|
||||||
28
docs/language/math/math-abs.md
Normal file
28
docs/language/math/math-abs.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: math-abs
|
||||||
|
name: Math.abs
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.abs
|
||||||
|
sig: Math.abs(x) -> int
|
||||||
|
tip: The magnitude of a value, dropping its sign.
|
||||||
|
order: 2
|
||||||
|
ns: Math
|
||||||
|
member: abs
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the absolute value of <code>x</code> — its distance from zero, always non-negative. It preserves the type of its argument, so <code>Math.abs</code> of a <code>fixed</code> stays <code>fixed</code>. Reach for it when you care about how far apart two things are regardless of direction, such as the gap between a target and current position, or the speed behind a signed velocity. The bare form <code>abs(x)</code> is kept as a convenience alias.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x` — the value whose magnitude you want
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program ChaseDistance {
|
||||||
|
var target: int = 40
|
||||||
|
var here: int = 12
|
||||||
|
|
||||||
|
handler Report phase Update {
|
||||||
|
let gap = Math.abs(target - here) # 28, regardless of which is larger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-acos.md
Normal file
22
docs/language/math/math-acos.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-acos
|
||||||
|
name: Math.acos
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.acos
|
||||||
|
sig: Math.acos(x) -> fixed
|
||||||
|
tip: The arccosine of a value, in radians.
|
||||||
|
order: 28
|
||||||
|
ns: Math
|
||||||
|
member: acos
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the angle in radians whose cosine is <code>x</code>, in <code>0</code>..<code>pi</code>. The input should be in <code>-1.0</code>..<code>1.0</code>. Handy for recovering the angle between two normalized directions from their dot product.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let between = Math.acos(dot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-asin.md
Normal file
22
docs/language/math/math-asin.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-asin
|
||||||
|
name: Math.asin
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.asin
|
||||||
|
sig: Math.asin(x) -> fixed
|
||||||
|
tip: The arcsine of a value, in radians.
|
||||||
|
order: 27
|
||||||
|
ns: Math
|
||||||
|
member: asin
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the angle in radians whose sine is <code>x</code>, in <code>-pi/2</code>..<code>pi/2</code>. The input should be in <code>-1.0</code>..<code>1.0</code>. Built from <code>Math.atan2</code> and <code>Math.sqrt</code>, so it is deterministic.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let angle = Math.asin(height / radius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-atan2.md
Normal file
22
docs/language/math/math-atan2.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-atan2
|
||||||
|
name: Math.atan2
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.atan2
|
||||||
|
sig: Math.atan2(y, x) -> fixed
|
||||||
|
tip: The angle of the vector (x, y), in radians.
|
||||||
|
order: 26
|
||||||
|
ns: Math
|
||||||
|
member: atan2
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the angle in radians from the positive x-axis to the point <code>(x, y)</code>, in the full range <code>-pi</code>..<code>pi</code> — the standard way to get a heading from a direction vector. Note the argument order is <code>y</code> then <code>x</code>. Deterministic fixed-point, accurate to within about a degree.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let heading = Math.atan2(target_y - y, target_x - x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
29
docs/language/math/math-ceil.md
Normal file
29
docs/language/math/math-ceil.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
id: math-ceil
|
||||||
|
name: Math.ceil
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.ceil
|
||||||
|
sig: Math.ceil(x) -> int
|
||||||
|
tip: Round a fixed value up to the nearest whole int.
|
||||||
|
order: 6
|
||||||
|
ns: Math
|
||||||
|
member: ceil
|
||||||
|
---
|
||||||
|
|
||||||
|
Takes a <code>fixed</code> value and returns the smallest <code>int</code> not less than it — rounding toward positive infinity, so <code>Math.ceil(2.1)</code> is <code>3</code> and <code>Math.ceil(4.0)</code> stays <code>4</code>. Use it when you need to cover a fractional amount with whole units: how many full tiles a span touches, or how many rows a variable-height list needs.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x` — the `fixed` value to round up
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program RowsNeeded {
|
||||||
|
const ROW_HEIGHT: fixed = 12.0
|
||||||
|
|
||||||
|
var content_height: fixed = 30.0
|
||||||
|
|
||||||
|
handler Layout phase Update {
|
||||||
|
let rows = Math.ceil(content_height / ROW_HEIGHT) # 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
31
docs/language/math/math-clamp.md
Normal file
31
docs/language/math/math-clamp.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
id: math-clamp
|
||||||
|
name: Math.clamp
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.clamp
|
||||||
|
sig: Math.clamp(v, lo, hi) -> int
|
||||||
|
tip: Constrain a value to the range [lo, hi].
|
||||||
|
order: 3
|
||||||
|
ns: Math
|
||||||
|
member: clamp
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>v</code> held inside the inclusive range <code>lo</code>..<code>hi</code>: below <code>lo</code> it returns <code>lo</code>, above <code>hi</code> it returns <code>hi</code>, otherwise <code>v</code> unchanged. It is the single-call form of <code>Math.max(lo, Math.min(v, hi))</code> and works for <code>int</code> and <code>fixed</code> alike. Use it to keep a camera inside the world, a slider within its track, or a stat within its legal bounds. The bare form <code>clamp(v, lo, hi)</code> is kept as a convenience alias.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `v` — the value to constrain
|
||||||
|
- `lo` — the lowest allowed result (inclusive)
|
||||||
|
- `hi` — the highest allowed result (inclusive)
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program CameraBounds {
|
||||||
|
const WORLD_WIDTH: int = 320
|
||||||
|
|
||||||
|
var camera_x: int = 0
|
||||||
|
|
||||||
|
handler Follow phase Update {
|
||||||
|
camera_x = Math.clamp(camera_x, 0, WORLD_WIDTH - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-cos.md
Normal file
22
docs/language/math/math-cos.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-cos
|
||||||
|
name: Math.cos
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.cos
|
||||||
|
sig: Math.cos(radians) -> fixed
|
||||||
|
tip: Cosine of an angle in radians.
|
||||||
|
order: 13
|
||||||
|
ns: Math
|
||||||
|
member: cos
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the cosine of <code>radians</code> as a <code>fixed</code> in <code>-1.0</code>..<code>1.0</code>, computed as <code>Math.sin(radians + pi/2)</code> off the same deterministic table. Use <code>Math.cos</code> and <code>Math.sin</code> together to convert an angle into an <code>(x, y)</code> direction — for orbits, aiming, or steering.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let x = center_x + Math.cos(angle) * radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-deg_to_rad.md
Normal file
22
docs/language/math/math-deg_to_rad.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-deg_to_rad
|
||||||
|
name: Math.deg_to_rad
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.deg_to_rad
|
||||||
|
sig: Math.deg_to_rad(degrees) -> fixed
|
||||||
|
tip: Convert degrees to radians.
|
||||||
|
order: 18
|
||||||
|
ns: Math
|
||||||
|
member: deg_to_rad
|
||||||
|
---
|
||||||
|
|
||||||
|
Converts an angle from degrees to radians (multiplying by <code>pi/180</code>), since <code>Math.sin</code>/<code>Math.cos</code>/<code>Math.tan</code> all take radians. Use it when your data or design speaks in degrees — a 90-degree turn, a 45-degree cone — and you need to feed it to the trig functions.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let r = Math.sin(Math.deg_to_rad(90.0)) # ~1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-dist.md
Normal file
22
docs/language/math/math-dist.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-dist
|
||||||
|
name: Math.dist
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.dist
|
||||||
|
sig: Math.dist(x0, y0, x1, y1) -> fixed
|
||||||
|
tip: Distance between two points.
|
||||||
|
order: 16
|
||||||
|
ns: Math
|
||||||
|
member: dist
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the straight-line distance between <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>sqrt(dx*dx + dy*dy)</code> in fixed-point. Use it for proximity and range checks. When you only need to compare distances (which is nearer? within radius?), prefer <code>Math.dist2</code>, which skips the square root.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
if Math.dist(px, py, ex, ey) < attack_range { strike() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/language/math/math-dist2.md
Normal file
23
docs/language/math/math-dist2.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: math-dist2
|
||||||
|
name: Math.dist2
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.dist2
|
||||||
|
sig: Math.dist2(x0, y0, x1, y1) -> fixed
|
||||||
|
tip: Squared distance between two points.
|
||||||
|
order: 17
|
||||||
|
ns: Math
|
||||||
|
member: dist2
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the squared distance between <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>dx*dx + dy*dy</code>, with no square root. Comparing squared distances gives the same ordering as comparing real distances, so this is the cheaper choice for "nearest" and "within radius" tests: compare against <code>radius * radius</code> instead of calling <code>Math.dist</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let r2 = radius * radius
|
||||||
|
if Math.dist2(px, py, ex, ey) < r2 { in_range() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
29
docs/language/math/math-floor.md
Normal file
29
docs/language/math/math-floor.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
id: math-floor
|
||||||
|
name: Math.floor
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.floor
|
||||||
|
sig: Math.floor(x) -> int
|
||||||
|
tip: Round a fixed value down to the nearest whole int.
|
||||||
|
order: 5
|
||||||
|
ns: Math
|
||||||
|
member: floor
|
||||||
|
---
|
||||||
|
|
||||||
|
Takes a <code>fixed</code> value and returns the largest <code>int</code> not greater than it — rounding toward negative infinity, so <code>Math.floor(2.7)</code> is <code>2</code> and <code>Math.floor(-0.2)</code> is <code>-1</code>. This is the fixed-to-int conversion you reach for when turning a smooth position into a whole tile or pixel index. It is the same operation as the bare <code>flr(x)</code>.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x` — the `fixed` value to round down
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program TileIndex {
|
||||||
|
const TILE_SIZE: fixed = 16.0
|
||||||
|
|
||||||
|
var world_x: fixed = 40.5
|
||||||
|
|
||||||
|
handler Locate phase Update {
|
||||||
|
let column = Math.floor(world_x / TILE_SIZE) # 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-hypot.md
Normal file
22
docs/language/math/math-hypot.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-hypot
|
||||||
|
name: Math.hypot
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.hypot
|
||||||
|
sig: Math.hypot(x, y) -> fixed
|
||||||
|
tip: Length of the vector (x, y).
|
||||||
|
order: 15
|
||||||
|
ns: Math
|
||||||
|
member: hypot
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>sqrt(x*x + y*y)</code> — the length of the 2D vector <code>(x, y)</code>, or equivalently the hypotenuse of a right triangle with those legs. It is the one-call form for a magnitude and reads more clearly than spelling out the squares and root. To normalize a vector, divide each component by its <code>Math.hypot</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let length = Math.hypot(velocity_x, velocity_y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
31
docs/language/math/math-inverse_lerp.md
Normal file
31
docs/language/math/math-inverse_lerp.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
id: math-inverse_lerp
|
||||||
|
name: Math.inverse_lerp
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.inverse_lerp
|
||||||
|
sig: Math.inverse_lerp(a, b, v) -> fixed
|
||||||
|
tip: Find where a value sits between two endpoints as a 0..1 fraction.
|
||||||
|
order: 9
|
||||||
|
ns: Math
|
||||||
|
member: inverse_lerp
|
||||||
|
---
|
||||||
|
|
||||||
|
The inverse of <code>Math.lerp</code>: given endpoints <code>a</code> and <code>b</code> and a value <code>v</code>, it returns the fraction <code>(v - a) / (b - a)</code> — <code>0.0</code> when <code>v</code> equals <code>a</code>, <code>1.0</code> when it equals <code>b</code>. Use it to turn a raw quantity into a normalized amount: how full a health bar is, how far a timer has run, or the <code>t</code> to feed into another <code>Math.lerp</code>. The endpoints must differ, since equal ones would divide by zero.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `a` — the endpoint that maps to `0.0`
|
||||||
|
- `b` — the endpoint that maps to `1.0`
|
||||||
|
- `v` — the value to locate between them
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program HealthFraction {
|
||||||
|
const MAX_HP: fixed = 200.0
|
||||||
|
|
||||||
|
var hp: fixed = 150.0
|
||||||
|
|
||||||
|
handler Draw phase Render {
|
||||||
|
let fraction = Math.inverse_lerp(0.0, MAX_HP, hp) # 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
30
docs/language/math/math-lerp.md
Normal file
30
docs/language/math/math-lerp.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
id: math-lerp
|
||||||
|
name: Math.lerp
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.lerp
|
||||||
|
sig: Math.lerp(a, b, t) -> fixed
|
||||||
|
tip: Blend between two values by a 0..1 amount.
|
||||||
|
order: 8
|
||||||
|
ns: Math
|
||||||
|
member: lerp
|
||||||
|
---
|
||||||
|
|
||||||
|
Linear interpolation: returns <code>a</code> when <code>t</code> is <code>0.0</code>, <code>b</code> when <code>t</code> is <code>1.0</code>, and a proportional blend in between — the exact value <code>a + (b - a) * t</code>. All three arguments are <code>fixed</code> and the result is <code>fixed</code>. It is the workhorse behind smooth motion: ease a camera toward a target, fade a value over time, or sample a gradient. Values of <code>t</code> outside <code>0..1</code> extrapolate past the endpoints.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `a` — the value returned at `t = 0.0`
|
||||||
|
- `b` — the value returned at `t = 1.0`
|
||||||
|
- `t` — the blend amount, normally `0.0`..`1.0`
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program EaseCamera {
|
||||||
|
var camera_x: fixed = 0.0
|
||||||
|
var target_x: fixed = 100.0
|
||||||
|
|
||||||
|
handler Follow phase Update {
|
||||||
|
camera_x = Math.lerp(camera_x, target_x, 0.1) # glides a tenth of the way each frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/math/math-max.md
Normal file
28
docs/language/math/math-max.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: math-max
|
||||||
|
name: Math.max
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.max
|
||||||
|
sig: Math.max(a, b) -> int
|
||||||
|
tip: The larger of two values.
|
||||||
|
order: 1
|
||||||
|
ns: Math
|
||||||
|
member: max
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns whichever of <code>a</code> and <code>b</code> is larger. Like <code>Math.min</code>, it works for both <code>int</code> and <code>fixed</code> operands. Use it to hold a value at a floor — for example keeping a countdown at zero instead of going negative, or a health bar from dropping below the minimum. The bare form <code>max(a, b)</code> is kept as a convenience alias.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `a` — the first value
|
||||||
|
- `b` — the second value
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Countdown {
|
||||||
|
var timer: int = 60
|
||||||
|
|
||||||
|
handler Tick phase Update {
|
||||||
|
timer = Math.max(timer - 1, 0) # stops at 0, never negative
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/math/math-min.md
Normal file
28
docs/language/math/math-min.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: math-min
|
||||||
|
name: Math.min
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.min
|
||||||
|
sig: Math.min(a, b) -> int
|
||||||
|
tip: The smaller of two values.
|
||||||
|
order: 0
|
||||||
|
ns: Math
|
||||||
|
member: min
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns whichever of <code>a</code> and <code>b</code> is smaller. It works on plain <code>int</code> values and on <code>fixed</code> values alike — both are stored as signed 32-bit words, so the comparison keeps their order either way. Use it to cap a value at a ceiling, keep a cursor inside a list, or take the nearer of two distances. The bare form <code>min(a, b)</code> is kept as a convenience alias.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `a` — the first value
|
||||||
|
- `b` — the second value
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program ClampScore {
|
||||||
|
var score: int = 0
|
||||||
|
|
||||||
|
handler AddPoint phase Update {
|
||||||
|
score = Math.min(score + 1, 100) # never climbs past 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-move_toward.md
Normal file
22
docs/language/math/math-move_toward.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-move_toward
|
||||||
|
name: Math.move_toward
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.move_toward
|
||||||
|
sig: Math.move_toward(from, to, delta) -> fixed
|
||||||
|
tip: Step from one value toward another by at most delta.
|
||||||
|
order: 24
|
||||||
|
ns: Math
|
||||||
|
member: move_toward
|
||||||
|
---
|
||||||
|
|
||||||
|
Moves <code>from</code> toward <code>to</code> by at most <code>delta</code>, never overshooting — when the gap is smaller than <code>delta</code> it returns exactly <code>to</code>. Unlike <code>Math.lerp</code>, which eases by a fraction of the remaining distance, this moves at a constant rate, which is what you want for turrets, meters, and UI values that should approach a target steadily and then stop.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
health_shown = Math.move_toward(health_shown, health, 2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-ping_pong.md
Normal file
22
docs/language/math/math-ping_pong.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-ping_pong
|
||||||
|
name: Math.ping_pong
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.ping_pong
|
||||||
|
sig: Math.ping_pong(t, len) -> int
|
||||||
|
tip: Bounce a counter back and forth in [0, len].
|
||||||
|
order: 22
|
||||||
|
ns: Math
|
||||||
|
member: ping_pong
|
||||||
|
---
|
||||||
|
|
||||||
|
Maps an ever-increasing counter <code>t</code> to a value that rises from <code>0</code> to <code>len</code> and back, bouncing forever — the triangle wave to <code>Math.wrap</code>'s sawtooth. Feed it a frame counter to drive a patrol, a pulsing highlight, or a back-and-forth sweep without tracking direction yourself.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let phase = Math.ping_pong(frame, 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-posmod.md
Normal file
22
docs/language/math/math-posmod.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-posmod
|
||||||
|
name: Math.posmod
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.posmod
|
||||||
|
sig: Math.posmod(a, m) -> int
|
||||||
|
tip: Modulo that is always non-negative.
|
||||||
|
order: 20
|
||||||
|
ns: Math
|
||||||
|
member: posmod
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>a</code> modulo <code>m</code>, but always in the range <code>0</code>..<code>m-1</code> — unlike the <code>%</code> operator, which keeps the sign of <code>a</code> (so <code>-1 % 5</code> is <code>-1</code>, while <code>Math.posmod(-1, 5)</code> is <code>4</code>). It is what you want for wrapping indices and grid coordinates that can go negative.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let col = Math.posmod(x, grid_width) # never negative
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-rad_to_deg.md
Normal file
22
docs/language/math/math-rad_to_deg.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-rad_to_deg
|
||||||
|
name: Math.rad_to_deg
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.rad_to_deg
|
||||||
|
sig: Math.rad_to_deg(radians) -> fixed
|
||||||
|
tip: Convert radians to degrees.
|
||||||
|
order: 19
|
||||||
|
ns: Math
|
||||||
|
member: rad_to_deg
|
||||||
|
---
|
||||||
|
|
||||||
|
Converts an angle from radians to degrees (multiplying by <code>180/pi</code>) — the inverse of <code>Math.deg_to_rad</code>. Use it to present an internally-radians angle in the degrees a player or designer expects, for a HUD readout or a debug overlay.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let heading_deg = Math.rad_to_deg(heading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
34
docs/language/math/math-remap.md
Normal file
34
docs/language/math/math-remap.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
id: math-remap
|
||||||
|
name: Math.remap
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.remap
|
||||||
|
sig: Math.remap(v, in0, in1, out0, out1) -> fixed
|
||||||
|
tip: Rescale a value from one range into another.
|
||||||
|
order: 10
|
||||||
|
ns: Math
|
||||||
|
member: remap
|
||||||
|
---
|
||||||
|
|
||||||
|
Maps <code>v</code> from the input range <code>in0</code>..<code>in1</code> onto the output range <code>out0</code>..<code>out1</code>, keeping its relative position — equivalent to <code>Math.lerp(out0, out1, Math.inverse_lerp(in0, in1, v))</code>. All arguments are <code>fixed</code>. It is the one call that converts between units: a health value into a bar width, a noise sample in <code>-1..1</code> into a <code>0..255</code> shade, or a mouse position into a world coordinate. Swap the output ends to invert the mapping.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `v` — the value to rescale
|
||||||
|
- `in0` — the start of the input range
|
||||||
|
- `in1` — the end of the input range
|
||||||
|
- `out0` — the start of the output range
|
||||||
|
- `out1` — the end of the output range
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program HealthBar {
|
||||||
|
const MAX_HP: fixed = 200.0
|
||||||
|
const BAR_WIDTH: fixed = 64.0
|
||||||
|
|
||||||
|
var hp: fixed = 150.0
|
||||||
|
|
||||||
|
handler Draw phase Render {
|
||||||
|
let width = Math.remap(hp, 0.0, MAX_HP, 0.0, BAR_WIDTH) # 48.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/math/math-round.md
Normal file
27
docs/language/math/math-round.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: math-round
|
||||||
|
name: Math.round
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.round
|
||||||
|
sig: Math.round(x) -> int
|
||||||
|
tip: Round a fixed value to the nearest whole int.
|
||||||
|
order: 7
|
||||||
|
ns: Math
|
||||||
|
member: round
|
||||||
|
---
|
||||||
|
|
||||||
|
Takes a <code>fixed</code> value and returns the nearest <code>int</code>, with halves rounding up: <code>Math.round(2.5)</code> is <code>3</code> and <code>Math.round(2.4)</code> is <code>2</code>. Choose it over <code>Math.floor</code> when you want the closest whole value rather than always the lower one — snapping a smoothly-moving sprite to a pixel, or reporting a fractional total as a clean number.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x` — the `fixed` value to round to nearest
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program SnapToPixel {
|
||||||
|
var smooth_x: fixed = 10.5
|
||||||
|
|
||||||
|
handler Draw phase Render {
|
||||||
|
let px = Math.round(smooth_x) # 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/math/math-sign.md
Normal file
28
docs/language/math/math-sign.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: math-sign
|
||||||
|
name: Math.sign
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.sign
|
||||||
|
sig: Math.sign(x) -> int
|
||||||
|
tip: The sign of a value as -1, 0, or 1.
|
||||||
|
order: 4
|
||||||
|
ns: Math
|
||||||
|
member: sign
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>-1</code> when <code>x</code> is negative, <code>1</code> when it is positive, and <code>0</code> when it is exactly zero. It works on both <code>int</code> and <code>fixed</code> values, since zero and sign are the same in Q16.16. Multiply a step by <code>Math.sign(dx)</code> to move one unit toward a target, or read the sign of a velocity to decide which way a sprite faces.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x` — the value to test
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program FaceTarget {
|
||||||
|
var x: int = 8
|
||||||
|
var target: int = 20
|
||||||
|
|
||||||
|
handler StepToward phase Update {
|
||||||
|
x = x + Math.sign(target - x) # +1 while target is to the right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-sin.md
Normal file
22
docs/language/math/math-sin.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-sin
|
||||||
|
name: Math.sin
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.sin
|
||||||
|
sig: Math.sin(radians) -> fixed
|
||||||
|
tip: Sine of an angle in radians.
|
||||||
|
order: 12
|
||||||
|
ns: Math
|
||||||
|
member: sin
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the sine of <code>radians</code> as a <code>fixed</code> in <code>-1.0</code>..<code>1.0</code>. It is table-driven (a 256-entry Q16.16 sine table with linear interpolation) so it is fast and fully deterministic. Angles wrap, so any value works — no need to reduce into a range first. Pair it with <code>Math.cos</code> to turn an angle into a direction.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let y = center_y + Math.sin(angle) * radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-smoothstep.md
Normal file
22
docs/language/math/math-smoothstep.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-smoothstep
|
||||||
|
name: Math.smoothstep
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.smoothstep
|
||||||
|
sig: Math.smoothstep(e0, e1, x) -> fixed
|
||||||
|
tip: A smooth 0..1 ramp between two edges.
|
||||||
|
order: 25
|
||||||
|
ns: Math
|
||||||
|
member: smoothstep
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>0.0</code> when <code>x</code> is at or below <code>e0</code>, <code>1.0</code> at or above <code>e1</code>, and a smooth S-curve (ease-in and ease-out) in between — the classic <code>t*t*(3 - 2t)</code> Hermite ramp. Use it for fades, dissolves, and easing a normalized amount before feeding it to <code>Math.lerp</code>, when a straight linear ramp looks too abrupt.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let a = Math.smoothstep(0.0, 1.0, fade_t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-snapped.md
Normal file
22
docs/language/math/math-snapped.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-snapped
|
||||||
|
name: Math.snapped
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.snapped
|
||||||
|
sig: Math.snapped(v, step) -> fixed
|
||||||
|
tip: Round a value to the nearest multiple of step.
|
||||||
|
order: 23
|
||||||
|
ns: Math
|
||||||
|
member: snapped
|
||||||
|
---
|
||||||
|
|
||||||
|
Rounds <code>v</code> to the nearest multiple of <code>step</code> (both <code>fixed</code>) — for snapping a position to a grid, quantizing an angle to 8 directions, or rounding a value to a tidy increment. <code>Math.snapped(x, 16.0)</code> snaps <code>x</code> to a 16-unit grid.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let gx = Math.snapped(world_x, 16.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-sqrt.md
Normal file
22
docs/language/math/math-sqrt.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-sqrt
|
||||||
|
name: Math.sqrt
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.sqrt
|
||||||
|
sig: Math.sqrt(x) -> fixed
|
||||||
|
tip: The square root of a fixed value.
|
||||||
|
order: 11
|
||||||
|
ns: Math
|
||||||
|
member: sqrt
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the non-negative square root of <code>x</code>, computed in Q16.16 by a deterministic integer algorithm — so it is bit-identical on every platform, the same guarantee the rest of the runtime gives. Negative inputs return <code>0</code>. Use it for lengths and distances; when you have two legs of a right triangle reach for <code>Math.hypot</code>, and when you only need to compare magnitudes prefer the cheaper squared distance <code>Math.dist2</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let speed = Math.sqrt(vx * vx + vy * vy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-tan.md
Normal file
22
docs/language/math/math-tan.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-tan
|
||||||
|
name: Math.tan
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.tan
|
||||||
|
sig: Math.tan(radians) -> fixed
|
||||||
|
tip: Tangent of an angle in radians.
|
||||||
|
order: 14
|
||||||
|
ns: Math
|
||||||
|
member: tan
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the tangent of <code>radians</code>, computed as <code>Math.sin(radians) / Math.cos(radians)</code> in fixed-point. Near odd multiples of <code>pi/2</code> the cosine approaches zero and the result grows without bound, so guard those angles. For most gameplay, <code>Math.sin</code>/<code>Math.cos</code> are what you want; <code>tan</code> is here for slopes and field-of-view math.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let slope = Math.tan(pitch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/math/math-wrap.md
Normal file
22
docs/language/math/math-wrap.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: math-wrap
|
||||||
|
name: Math.wrap
|
||||||
|
category: math
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Math.wrap
|
||||||
|
sig: Math.wrap(v, lo, hi) -> int
|
||||||
|
tip: Wrap a value into the range [lo, hi).
|
||||||
|
order: 21
|
||||||
|
ns: Math
|
||||||
|
member: wrap
|
||||||
|
---
|
||||||
|
|
||||||
|
Wraps <code>v</code> into the half-open range <code>lo</code>..<code>hi</code>, so values that run off one end reappear at the other — the toroidal counterpart to <code>Math.clamp</code>. Use it for wrap-around movement, cycling through a menu, or keeping a scrolling offset in bounds.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Step phase Update {
|
||||||
|
let x = Math.wrap(player_x, 0, world_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
7
docs/language/mem/_section.md
Normal file
7
docs/language/mem/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: mem
|
||||||
|
title: Mem
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Raw memory — allocate byte and word buffers, copy and fill regions, and peek/poke individual bytes. The low-level escape hatch.
|
||||||
14
docs/language/mem/mem-bytes.md
Normal file
14
docs/language/mem/mem-bytes.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: mem-bytes
|
||||||
|
name: Mem.bytes
|
||||||
|
category: mem
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Mem.bytes
|
||||||
|
sig: Mem.bytes(n) -> ptr
|
||||||
|
tip: Allocate n bytes.
|
||||||
|
order: 0
|
||||||
|
ns: Mem
|
||||||
|
member: bytes
|
||||||
|
---
|
||||||
|
|
||||||
|
Allocates an <code>n</code>-byte buffer and returns a pointer to it.
|
||||||
14
docs/language/mem/mem-copy.md
Normal file
14
docs/language/mem/mem-copy.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: mem-copy
|
||||||
|
name: Mem.copy
|
||||||
|
category: mem
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Mem.copy
|
||||||
|
sig: Mem.copy(dst, src, n) -> void
|
||||||
|
tip: Copy n bytes between buffers.
|
||||||
|
order: 2
|
||||||
|
ns: Mem
|
||||||
|
member: copy
|
||||||
|
---
|
||||||
|
|
||||||
|
Copies <code>n</code> bytes from <code>src</code> to <code>dst</code> (regions must not overlap).
|
||||||
14
docs/language/mem/mem-fill.md
Normal file
14
docs/language/mem/mem-fill.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: mem-fill
|
||||||
|
name: Mem.fill
|
||||||
|
category: mem
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Mem.fill
|
||||||
|
sig: Mem.fill(buf, value, n) -> void
|
||||||
|
tip: Set n bytes to a value.
|
||||||
|
order: 3
|
||||||
|
ns: Mem
|
||||||
|
member: fill
|
||||||
|
---
|
||||||
|
|
||||||
|
Sets the first <code>n</code> bytes of <code>buf</code> to the byte <code>value</code>.
|
||||||
14
docs/language/mem/mem-peek.md
Normal file
14
docs/language/mem/mem-peek.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: mem-peek
|
||||||
|
name: Mem.peek
|
||||||
|
category: mem
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Mem.peek
|
||||||
|
sig: Mem.peek(buf, i) -> int
|
||||||
|
tip: Read one byte.
|
||||||
|
order: 4
|
||||||
|
ns: Mem
|
||||||
|
member: peek
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the byte at <code>buf[i]</code> as an int in 0..255.
|
||||||
14
docs/language/mem/mem-poke.md
Normal file
14
docs/language/mem/mem-poke.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: mem-poke
|
||||||
|
name: Mem.poke
|
||||||
|
category: mem
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Mem.poke
|
||||||
|
sig: Mem.poke(buf, i, value) -> void
|
||||||
|
tip: Write one byte.
|
||||||
|
order: 5
|
||||||
|
ns: Mem
|
||||||
|
member: poke
|
||||||
|
---
|
||||||
|
|
||||||
|
Stores the low byte of <code>value</code> at <code>buf[i]</code>.
|
||||||
14
docs/language/mem/mem-words.md
Normal file
14
docs/language/mem/mem-words.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: mem-words
|
||||||
|
name: Mem.words
|
||||||
|
category: mem
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Mem.words
|
||||||
|
sig: Mem.words(n) -> words
|
||||||
|
tip: Allocate n 32-bit words.
|
||||||
|
order: 1
|
||||||
|
ns: Mem
|
||||||
|
member: words
|
||||||
|
---
|
||||||
|
|
||||||
|
Allocates a buffer of <code>n</code> 32-bit integer words, indexable as <code>w[i]</code>.
|
||||||
7
docs/language/net/_section.md
Normal file
7
docs/language/net/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: net
|
||||||
|
title: Net
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
The low-level networking seam — send and poll datagrams, serialize and apply entity state, and read ownership and role. Offline these collapse to single-player defaults.
|
||||||
14
docs/language/net/net-apply.md
Normal file
14
docs/language/net/net-apply.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-apply
|
||||||
|
name: Net.apply
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.apply
|
||||||
|
sig: Net.apply(entity, buf) -> int
|
||||||
|
tip: Apply serialized state to an entity.
|
||||||
|
order: 3
|
||||||
|
ns: Net
|
||||||
|
member: apply
|
||||||
|
---
|
||||||
|
|
||||||
|
Reads synced fields from <code>buf</code> into <code>entity</code> — the inverse of <code>Net.serialize</code>.
|
||||||
14
docs/language/net/net-is_owner.md
Normal file
14
docs/language/net/net-is_owner.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-is_owner
|
||||||
|
name: Net.is_owner
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.is_owner
|
||||||
|
sig: Net.is_owner(entity) -> bool
|
||||||
|
tip: Does this peer own the entity?
|
||||||
|
order: 7
|
||||||
|
ns: Net
|
||||||
|
member: is_owner
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns true when this peer owns <code>entity</code> — <code>owner(entity) == local_id()</code>.
|
||||||
14
docs/language/net/net-is_server.md
Normal file
14
docs/language/net/net-is_server.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-is_server
|
||||||
|
name: Net.is_server
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.is_server
|
||||||
|
sig: Net.is_server() -> bool
|
||||||
|
tip: Is this peer the server?
|
||||||
|
order: 6
|
||||||
|
ns: Net
|
||||||
|
member: is_server
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns true on the authoritative peer. Offline it is true, so server-only guards run in single-player.
|
||||||
14
docs/language/net/net-local_id.md
Normal file
14
docs/language/net/net-local_id.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-local_id
|
||||||
|
name: Net.local_id
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.local_id
|
||||||
|
sig: Net.local_id() -> int
|
||||||
|
tip: This peer's own id.
|
||||||
|
order: 8
|
||||||
|
ns: Net
|
||||||
|
member: local_id
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the network id of the local peer.
|
||||||
14
docs/language/net/net-owner.md
Normal file
14
docs/language/net/net-owner.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-owner
|
||||||
|
name: Net.owner
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.owner
|
||||||
|
sig: Net.owner(entity) -> int
|
||||||
|
tip: The peer that owns an entity.
|
||||||
|
order: 4
|
||||||
|
ns: Net
|
||||||
|
member: owner
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the peer id that owns <code>entity</code>'s <code>@Owned</code> state.
|
||||||
14
docs/language/net/net-poll.md
Normal file
14
docs/language/net/net-poll.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-poll
|
||||||
|
name: Net.poll
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.poll
|
||||||
|
sig: Net.poll(buf, cap) -> int
|
||||||
|
tip: Read an inbound datagram.
|
||||||
|
order: 1
|
||||||
|
ns: Net
|
||||||
|
member: poll
|
||||||
|
---
|
||||||
|
|
||||||
|
Reads up to <code>cap</code> bytes of the next inbound datagram into <code>buf</code> and returns the byte count, or zero when none is waiting.
|
||||||
14
docs/language/net/net-send.md
Normal file
14
docs/language/net/net-send.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-send
|
||||||
|
name: Net.send
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.send
|
||||||
|
sig: Net.send(peer, buf, len) -> void
|
||||||
|
tip: Put a datagram on the wire.
|
||||||
|
order: 0
|
||||||
|
ns: Net
|
||||||
|
member: send
|
||||||
|
---
|
||||||
|
|
||||||
|
Sends the first <code>len</code> bytes of <code>buf</code> to <code>peer</code>. Absent a real socket it uses the built-in loopback, so offline code still runs.
|
||||||
14
docs/language/net/net-serialize.md
Normal file
14
docs/language/net/net-serialize.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-serialize
|
||||||
|
name: Net.serialize
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.serialize
|
||||||
|
sig: Net.serialize(entity, buf) -> int
|
||||||
|
tip: Serialize an entity's synced state.
|
||||||
|
order: 2
|
||||||
|
ns: Net
|
||||||
|
member: serialize
|
||||||
|
---
|
||||||
|
|
||||||
|
Writes the <code>@Sync</code> fields of <code>entity</code> into <code>buf</code> and returns the byte count, for sending over the wire.
|
||||||
14
docs/language/net/net-set_owner.md
Normal file
14
docs/language/net/net-set_owner.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: net-set_owner
|
||||||
|
name: Net.set_owner
|
||||||
|
category: net
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Net.set_owner
|
||||||
|
sig: Net.set_owner(entity, peer) -> void
|
||||||
|
tip: Assign ownership of an entity.
|
||||||
|
order: 5
|
||||||
|
ns: Net
|
||||||
|
member: set_owner
|
||||||
|
---
|
||||||
|
|
||||||
|
Sets the owning peer of <code>entity</code> to <code>peer</code>.
|
||||||
|
|
@ -3,6 +3,7 @@ id: op-logical
|
||||||
name: Logical
|
name: Logical
|
||||||
category: operators
|
category: operators
|
||||||
kind: operator
|
kind: operator
|
||||||
|
tokens: and or not
|
||||||
sig: and or not
|
sig: and or not
|
||||||
tip: The boolean combinators, spelled as words — never && or || or a bare !.
|
tip: The boolean combinators, spelled as words — never && or || or a bare !.
|
||||||
order: 2
|
order: 2
|
||||||
|
|
|
||||||
25
docs/language/random/random-int.md
Normal file
25
docs/language/random/random-int.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: random-int
|
||||||
|
name: Random.int
|
||||||
|
category: random
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Random.int
|
||||||
|
sig: Random.int(max) -> int
|
||||||
|
tip: A random integer in [0, max).
|
||||||
|
order: 4
|
||||||
|
ns: Random
|
||||||
|
member: int
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns a deterministic integer from <code>0</code> up to but not including <code>max</code>. Use it to pick a random index into a list of length <code>max</code>. Returns 0 when <code>max</code> is not positive.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Seed phase Start { Random.seed(value: 7) }
|
||||||
|
handler Step phase Update {
|
||||||
|
let idx = Random.int(list_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
25
docs/language/random/random-sign.md
Normal file
25
docs/language/random/random-sign.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: random-sign
|
||||||
|
name: Random.sign
|
||||||
|
category: random
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Random.sign
|
||||||
|
sig: Random.sign() -> int
|
||||||
|
tip: A random +1 or -1.
|
||||||
|
order: 5
|
||||||
|
ns: Random
|
||||||
|
member: sign
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>1</code> or <code>-1</code> with equal probability — a deterministic coin flip for choosing a direction or jitter sign.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler Seed phase Start { Random.seed(value: 7) }
|
||||||
|
handler Step phase Update {
|
||||||
|
let dir = Random.sign()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue