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:
Orkun ÇAKILKAYA 2026-08-30 00:26:19 +03:00
parent ff15c4e01d
commit a38195128f
235 changed files with 24676 additions and 7762 deletions

View 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.

View 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
}
}
```

View 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)
}
}
}
```

View 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)
}
}
}
```

View 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)
}
}
}
```

View 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
}
}
```

View 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)
}
}
}
```

View 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)
}
}
```

View 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)
}
}
}
```

View 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)
}
}
```

View 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)
}
}
```

View 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)
}
}
```

View 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
}
}
```

View 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>&lt;</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)
}
}
```

View 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
}
}
```