refactor(lang): rename the fn keyword to function

Expand the function-declaration keyword to the full word across the whole
language and toolchain:
  fn name(...) -> T { ... }   ->   function name(...) -> T { ... }

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 01:43:22 +03:00
parent 2f19c8d8e2
commit 4c48077d68
86 changed files with 793 additions and 793 deletions

View file

@ -109,10 +109,10 @@ All verified. A compiler needs each of these, and each one works today.
| Deep recursion (recursive-descent parsing) | ✅ | 5,000 frames, no crash | | Deep recursion (recursive-descent parsing) | ✅ | 5,000 frames, no crash |
| Heap allocation | ✅ | `mem_alloc`, `mem_free`, `mem_copy`, `mem_set`; 1 MiB alloc verified | | Heap allocation | ✅ | `mem_alloc`, `mem_free`, `mem_copy`, `mem_set`; 1 MiB alloc verified |
| Byte-level memory | ✅ | `peek8`/`poke8`, `peek32`/`poke32`, `peekp`/`pokep`, `ptr_add` | | Byte-level memory | ✅ | `peek8`/`poke8`, `peek32`/`poke32`, `peekp`/`pokep`, `ptr_add` |
| `ptr` locals, params, returns | ✅ | `fn make(n: int) -> ptr` | | `ptr` locals, params, returns | ✅ | `function make(n: int) -> ptr` |
| `ptr` in a property field | ✅ | `property Nd { kind: int = 0, a: ptr = ptr_null() }` | | `ptr` in a property field | ✅ | `property Nd { kind: int = 0, a: ptr = ptr_null() }` |
| String literals as readable bytes | ✅ | `peek8("hello", 1)` → `101` | | String literals as readable bytes | ✅ | `peek8("hello", 1)` → `101` |
| `str` accepted where `ptr` expected | ✅ | `f("A")` into `fn f(p: ptr)` | | `str` accepted where `ptr` expected | ✅ | `f("A")` into `function f(p: ptr)` |
| String comparison, **hand-written in Ludic** | ✅ | `streq` over `peek8` | | String comparison, **hand-written in Ludic** | ✅ | `streq` over `peek8` |
| Integer → decimal, **hand-written in Ludic** | ✅ | `itoa(48291)` → `"48291"` | | Integer → decimal, **hand-written in Ludic** | ✅ | `itoa(48291)` → `"48291"` |
| File read: open/seek/tell/read/close | ✅ | full round-trip of a written file | | File read: open/seek/tell/read/close | ✅ | full round-trip of a written file |
@ -361,7 +361,7 @@ whole project.
| Gap | Why it does not block | | Gap | Why it does not block |
|---|---| |---|---|
| **64-bit integers** ❌ (`100000*100000` → `1410065408`, wraps at i32) | Line numbers, offsets, node indices and string lengths all fit in `i32`. Only matters for source files > 2 GiB. | | **64-bit integers** ❌ (`100000*100000` → `1410065408`, wraps at i32) | Line numbers, offsets, node indices and string lengths all fit in `i32`. Only matters for source files > 2 GiB. |
| **A non-ECS entry point** | A `Start`-phase system plus `os_exit(n)` gives correct exit codes — verified. You do pay for an unused 1024-entity world; that is a constant, not a blocker. A `tool Name { fn main() -> int }` form would be nicer, not necessary. | | **A non-ECS entry point** | A `Start`-phase system plus `os_exit(n)` gives correct exit codes — verified. You do pay for an unused 1024-entity world; that is a constant, not a blocker. A `tool Name { function main() -> int }` form would be nicer, not necessary. |
| Closures, generics, unions, sum types | A compiler in the style of `ludicc.c` uses none of them. | | Closures, generics, unions, sum types | A compiler in the style of `ludicc.c` uses none of them. |
| GC | A compiler should leak deliberately (§8). | | GC | A compiler should leak deliberately (§8). |
| Unsigned integer types | `shr` is already logical and `band`/`bor` are bit-level — sufficient. | | Unsigned integer types | `shr` is already logical and `band`/`bor` are bit-level — sufficient. |
@ -454,7 +454,7 @@ Each row verified by compiling a probe, not by reading docs.
| ~~**R3**~~ | ~~**`and`/`or` alias `&&`/`\|\|`.**~~ **RESOLVED** — `and`/`or`/`not` are the only boolean operators; `&&`, `\|\|` and `!` are each rejected with a diagnostic naming the fix, and all three words are reserved. `!=` is unaffected. | landed via S3 | — | | ~~**R3**~~ | ~~**`and`/`or` alias `&&`/`\|\|`.**~~ **RESOLVED** — `and`/`or`/`not` are the only boolean operators; `&&`, `\|\|` and `!` are each rejected with a diagnostic naming the fix, and all three words are reserved. `!=` is unaffected. | landed via S3 | — |
| **R4** | **`{ }` means seven different things** — statement block; property fields (`n: T = e`); model list (bare idents); spawn initialisers (`N = { … }`); ui props + children (`k=v` juxtaposed, no commas); match arms (`p, p => …`); machine states (`state N = v { … }`). | `block/comp/arche/spawn/parse_widget/match/machine` | The delimiter carries no information. You must already know the head keyword to know the inner grammar. | | **R4** | **`{ }` means seven different things** — statement block; property fields (`n: T = e`); model list (bare idents); spawn initialisers (`N = { … }`); ui props + children (`k=v` juxtaposed, no commas); match arms (`p, p => …`); machine states (`state N = v { … }`). | `block/comp/arche/spawn/parse_widget/match/machine` | The delimiter carries no information. You must already know the head keyword to know the inner grammar. |
| **R5** | **Contextual keywords, not reserved.** `phase`, `query`, `reads`, `writes`, `needs`, `uses`, `where`, `in`, `on`, `layer`, `state`, `start` are matched with `isid()` — ordinary identifiers. `let query = 5 let phase = 6` compiles and prints `11`. | `sys()`, `scene_decl()` | A local named `enter` or `match` produces a baffling error far from the cause. | | **R5** | **Contextual keywords, not reserved.** `phase`, `query`, `reads`, `writes`, `needs`, `uses`, `where`, `in`, `on`, `layer`, `state`, `start` are matched with `isid()` — ordinary identifiers. `let query = 5 let phase = 6` compiles and prints `11`. | `sys()`, `scene_decl()` | A local named `enter` or `match` produces a baffling error far from the cause. |
| **R6** | **Contracts are parsed and thrown away.** `requires`/`ensures`/`invariant` parse an expression and **discard it** (`pi++; expr();`). `reads`/`writes`/`needs`/`uses`/`effects` are `skip_brackets()`. `pure` is consumed and ignored. Verified: `fn half(n: int) -> int requires n > 100000 ensures false` compiles, and `half(8)` returns `4`. Verified: a system declaring `reads [Pos]` that **writes** `p.x = 99` compiles. | `fn()`, `sys()` | **The worst item in the audit.** The language accepts a contract and does nothing. A model writing `requires n > 0` is rewarded with a clean compile and zero enforcement — it learns a lie, and so does a human reader trusting the annotation. | | **R6** | **Contracts are parsed and thrown away.** `requires`/`ensures`/`invariant` parse an expression and **discard it** (`pi++; expr();`). `reads`/`writes`/`needs`/`uses`/`effects` are `skip_brackets()`. `pure` is consumed and ignored. Verified: `function half(n: int) -> int requires n > 100000 ensures false` compiles, and `half(8)` returns `4`. Verified: a system declaring `reads [Pos]` that **writes** `p.x = 99` compiles. | `fn()`, `sys()` | **The worst item in the audit.** The language accepts a contract and does nothing. A model writing `requires n > 0` is rewarded with a clean compile and zero enforcement — it learns a lie, and so does a human reader trusting the annotation. |
| **R7** | **`str + str` typechecks, then emits invalid IR.** | verified (§4 B2) | The front-end accepts what the backend cannot lower. | | **R7** | **`str + str` typechecks, then emits invalid IR.** | verified (§4 B2) | The front-end accepts what the backend cannot lower. |
| **R8** | **Two formatters, opposite philosophies, both called "format".** `ludicc --fmt` canonicalises hard (one statement per line, `and`→`&&`, full parenthesisation) but drops comments and inlines imports. `ludic-fmt` is token-based and preserves comments — but **normalises nothing**: handed the one-line `let a = 1 a = a + 1 if true and false { … }`, it returned it unchanged. | verified side-by-side | **Neither tool enforces a single spelling.** The canonicaliser is unusable on real source; the source formatter has no opinion. | | **R8** | **Two formatters, opposite philosophies, both called "format".** `ludicc --fmt` canonicalises hard (one statement per line, `and`→`&&`, full parenthesisation) but drops comments and inlines imports. `ludic-fmt` is token-based and preserves comments — but **normalises nothing**: handed the one-line `let a = 1 a = a + 1 if true and false { … }`, it returned it unchanged. | verified side-by-side | **Neither tool enforces a single spelling.** The canonicaliser is unusable on real source; the source formatter has no opinion. |
| **R9** | **Two ways to spell a tag** — `property Player { }` (empty property) or `model`. | LANGUAGE.md | | | **R9** | **Two ways to spell a tag** — `property Player { }` (empty property) or `model`. | LANGUAGE.md | |
@ -863,14 +863,14 @@ current tree (`bin/x test` = 64/64).
**Recursion** ✅ → `55` **Recursion** ✅ → `55`
```ludic ```ludic
program P { program P {
fn fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) } function fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) }
handler B phase Start { print_int(fib(10)); quit() } handler B phase Start { print_int(fib(10)); quit() }
} }
``` ```
**String comparison, hand-written** ✅ → `1` **String comparison, hand-written** ✅ → `1`
```ludic ```ludic
fn streq(a: ptr, b: ptr) -> bool { function streq(a: ptr, b: ptr) -> bool {
let i = 0 let i = 0
while true { while true {
let ca = peek8(a,i) let ca = peek8(a,i)
@ -885,7 +885,7 @@ fn streq(a: ptr, b: ptr) -> bool {
**Integer → string, hand-written** ✅ → `48291` **Integer → string, hand-written** ✅ → `48291`
```ludic ```ludic
fn itoa(v: int, buf: ptr) -> int { function itoa(v: int, buf: ptr) -> int {
let n = 0 let n = 0
let x = v let x = v
if x == 0 { poke8(buf,0,48); return 1 } if x == 0 { poke8(buf,0,48); return 1 }
@ -955,7 +955,7 @@ print_int(query + phase)
prints `4`: prints `4`:
```ludic ```ludic
# doc-check: skip — composite: declaration plus statements # doc-check: skip — composite: declaration plus statements
fn half(n: int) -> int requires n > 100000 ensures false { return n / 2 } function half(n: int) -> int requires n > 100000 ensures false { return n / 2 }
print_int(half(8)) print_int(half(8))
``` ```
And a system may declare read-only access, then write — also compiles: And a system may declare read-only access, then write — also compiles:

View file

@ -103,8 +103,8 @@ A source file opens with `game Name { … }` or `module Name { … }`.
```ludic ```ludic
# doc-check: skip — illustrative: elided body # doc-check: skip — illustrative: elided body
program Combat { program Combat {
@export fn damage(attack: int, armour: int, roll: int) -> int { … } @export function damage(attack: int, armour: int, roll: int) -> int { … }
fn curve(level: int) -> int { … } # private: not a symbol function curve(level: int) -> int { … } # private: not a symbol
} }
``` ```
@ -118,7 +118,7 @@ Those are ordinary C-ABI symbols, so anything that can call a shared library can
call Ludic. To call them from another Ludic program, declare them and link: call Ludic. To call them from another Ludic program, declare them and link:
```ludic ```ludic
extern fn damage(attack: int, armour: int, roll: int) -> int = "damage" extern function damage(attack: int, armour: int, roll: int) -> int = "damage"
``` ```
```bash ```bash

View file

@ -502,7 +502,7 @@ object, so assigning or passing one shares it rather than copying.
```ludic ```ludic
property Tok { kind: int = 0, line: int = 0, next: Tok } property Tok { kind: int = 0, line: int = 0, next: Tok }
fn bump(t: Tok) -> void { t.kind = t.kind + 1 } function bump(t: Tok) -> void { t.kind = t.kind + 1 }
handler Share phase Update { handler Share phase Update {
let a = new Tok let a = new Tok
@ -560,9 +560,9 @@ fields: `toks[i].kind = T_ID` is a single address computation.
## Functions & FFI ## Functions & FFI
```ludic ```ludic
fn heal(amount: int) -> int { return amount * 2 } function heal(amount: int) -> int { return amount * 2 }
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # bind a C symbol extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # bind a C symbol
``` ```
`extern fn … = "symbol"` declares a foreign function and binds it to a symbol `extern fn … = "symbol"` declares a foreign function and binds it to a symbol
resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how

View file

@ -429,7 +429,7 @@ nodes[i].param0 = C_STONE
print_int(nodes.len) print_int(nodes.len)
# slices: a (ptr, len) pair, no ownership — the workhorse for I/O # slices: a (ptr, len) pair, no ownership — the workhorse for I/O
fn checksum(bytes: []u8) -> int { function checksum(bytes: []u8) -> int {
var sum = 0 var sum = 0
for b in bytes { sum = sum + b } # `for x in slice` iteration for b in bytes { sum = sum + b } # `for x in slice` iteration
return sum return sum
@ -464,9 +464,9 @@ struct MapNode {
struct v3i { x: int = 0, y: int = 0, z: int = 0 } struct v3i { x: int = 0, y: int = 0, z: int = 0 }
fn index(p: v3i) -> int { return (p.z * 16 + p.y) * 16 + p.x } function index(p: v3i) -> int { return (p.z * 16 + p.y) * 16 + p.x }
fn get(blk: []MapNode, p: v3i) -> MapNode { return blk[index(p)] } function get(blk: []MapNode, p: v3i) -> MapNode { return blk[index(p)] }
# a component becomes "a struct that lives in the world" # a component becomes "a struct that lives in the world"
component Pos = v3i # aliasing form component Pos = v3i # aliasing form
@ -673,7 +673,7 @@ unknown node names, failed DB writes. It uses C++ exceptions
sum-typed results with sugar, which falls straight out of G-06: sum-typed results with sugar, which falls straight out of G-06:
```ludic ```ludic
fn read_block(path: str) -> MapBlock ! IoError { function read_block(path: str) -> MapBlock ! IoError {
let f = file_open(path, "rb") else return IoError.NotFound let f = file_open(path, "rb") else return IoError.NotFound
let n = try file_read(f, buf, 4096) # `try` propagates the error arm let n = try file_read(f, buf, 4096) # `try` propagates the error arm
return parse(buf[0..n]) return parse(buf[0..n])
@ -1085,7 +1085,7 @@ Ludic has a 320×240 software framebuffer presented through
| Write a GPU driver | ✗ | not serious | | Write a GPU driver | ✗ | not serious |
**Recommended:** treat the GPU exactly as the project already treats the OS — **Recommended:** treat the GPU exactly as the project already treats the OS —
an ABI to call, not a C program to compile. `extern fn glDrawElements(…) = an ABI to call, not a C program to compile. `extern function glDrawElements(…) =
"glDrawElements"`, with a thin `runtime/native/gfx3d.ludic` over it. This is "glDrawElements"`, with a thin `runtime/native/gfx3d.ludic` over it. This is
consistent with the existing rule ("C libraries are used via FFI") and needs consistent with the existing rule ("C libraries are used via FFI") and needs
**no compiler change beyond G-04's `f32`.** **no compiler change beyond G-04's `f32`.**
@ -1097,11 +1097,11 @@ struct v3f { x: f32 = 0, y: f32 = 0, z: f32 = 0 }
struct m4 { m: [16]f32 } struct m4 { m: [16]f32 }
struct aabb { min: v3f, max: v3f } struct aabb { min: v3f, max: v3f }
fn dot(a: v3f, b: v3f) -> f32 function dot(a: v3f, b: v3f) -> f32
fn cross(a: v3f, b: v3f) -> v3f function cross(a: v3f, b: v3f) -> v3f
fn normalize(v: v3f) -> v3f function normalize(v: v3f) -> v3f
fn look_at(eye: v3f, at: v3f, up: v3f) -> m4 function look_at(eye: v3f, at: v3f, up: v3f) -> m4
fn perspective(fovy: f32, aspect: f32, znear: f32, zfar: f32) -> m4 function perspective(fovy: f32, aspect: f32, znear: f32, zfar: f32) -> m4
``` ```
Worth considering: **operator overloading for vector types only** — Ludic Worth considering: **operator overloading for vector types only** — Ludic
@ -1211,11 +1211,11 @@ hand-rolled pointer arithmetic these features exist to delete.
```ludic ```ludic
# before (runtime/native/image.ludic today) # before (runtime/native/image.ludic today)
fn px(img: ptr, x: int, y: int, w: int) -> int { return peek32(img, (y*w+x)*4) } function px(img: ptr, x: int, y: int, w: int) -> int { return peek32(img, (y*w+x)*4) }
# after # after
struct Rgba { r: u8, g: u8, b: u8, a: u8 } struct Rgba { r: u8, g: u8, b: u8, a: u8 }
fn px(img: []Rgba, x: int, y: int, w: int) -> Rgba { return img[y*w + x] } function px(img: []Rgba, x: int, y: int, w: int) -> Rgba { return img[y*w + x] }
``` ```
### M1 — Language core, part 2: numbers, strings, errors *(≈6 weeks)* ### M1 — Language core, part 2: numbers, strings, errors *(≈6 weeks)*

View file

@ -31,7 +31,7 @@ them:
[:103](runtime/native/core.ludic:103), [:217](runtime/native/core.ludic:217)). [:103](runtime/native/core.ludic:103), [:217](runtime/native/core.ludic:217)).
`COMPILING.md` states the intent plainly: a new platform is "another `.ll` file `COMPILING.md` states the intent plainly: a new platform is "another `.ll` file
with the same five entry points and no compiler change." with the same five entry points and no compiler change."
- **`extern fn` FFI is real and live** — `extern fn c_hypot(a: fixed, b: fixed) -> - **`extern fn` FFI is real and live** — `extern function c_hypot(a: fixed, b: fixed) ->
fixed = "hypot_fx"` ([LANGUAGE.md:565](LANGUAGE.md:565)), with a full pipeline: fixed = "hypot_fx"` ([LANGUAGE.md:565](LANGUAGE.md:565)), with a full pipeline:
parse ([parse_game.ludic:236](selfhost/parse_game.ludic:236)) → call lowering to a parse ([parse_game.ludic:236](selfhost/parse_game.ludic:236)) → call lowering to a
direct `call @<sym>` ([emit_expr.ludic:168](selfhost/emit_expr.ludic:168)) → direct `call @<sym>` ([emit_expr.ludic:168](selfhost/emit_expr.ludic:168)) →
@ -211,9 +211,9 @@ no new language feature, no new intrinsic:
```ludic ```ludic
# doc-check: skip — runtime/native/gfx3d.ludic, illustrative # doc-check: skip — runtime/native/gfx3d.ludic, illustrative
extern fn gl_gen_textures(n: int, out: ptr) -> void = "glGenTextures" extern function gl_gen_textures(n: int, out: ptr) -> void = "glGenTextures"
extern fn gl_tex_image_2d(t: int, w: int, h: int, px: ptr) -> void = "gl_tex_image_2d" extern function gl_tex_image_2d(t: int, w: int, h: int, px: ptr) -> void = "gl_tex_image_2d"
extern fn gl_draw_elements(mode: int, count: int, ty: int, idx: ptr) -> void = "glDrawElements" extern function gl_draw_elements(mode: int, count: int, ty: int, idx: ptr) -> void = "glDrawElements"
``` ```
Two phases, additive: Two phases, additive:

View file

@ -151,7 +151,7 @@ directly from these; the high-level layer (§6) is sugar over them.
| Primitive | Signature (sketch) | Enables | | Primitive | Signature (sketch) | Enables |
|---|---|---| |---|---|---|
| **Transport seam** | `extern fn net_send(peer: int, buf: ptr, len: int)` · `extern fn net_poll(buf: ptr, cap: int) -> int` | any model; host binds UDP (native) or WebRTC/WebSocket (wasm), or a loopback for tests | | **Transport seam** | `extern function net_send(peer: int, buf: ptr, len: int)` · `extern function net_poll(buf: ptr, cap: int) -> int` | any model; host binds UDP (native) or WebRTC/WebSocket (wasm), or a loopback for tests |
| **World snapshot ↔ buffer** | `world_save(buf: ptr) -> int` · `world_load(buf: ptr, len: int)` | rollback, replication, join/resync — generalizes `save()`/`load()` off the filesystem | | **World snapshot ↔ buffer** | `world_save(buf: ptr) -> int` · `world_load(buf: ptr, len: int)` | rollback, replication, join/resync — generalizes `save()`/`load()` off the filesystem |
| **Generated serializers** | `serialize_<Model>(e: entity, buf: ptr) -> int` · `apply_<Model>(e: entity, buf: ptr, len: int)` | per-model, touch only the `@Sync` fields; emitted from the schema | | **Generated serializers** | `serialize_<Model>(e: entity, buf: ptr) -> int` · `apply_<Model>(e: entity, buf: ptr, len: int)` | per-model, touch only the `@Sync` fields; emitted from the schema |
| **Ownership** | `owner(e: entity) -> int` · `set_owner(e: entity, id: int)` | authority checks, per-entity owner metadata (an `@L_owner` array, like `@L_kind`) | | **Ownership** | `owner(e: entity) -> int` · `set_owner(e: entity, id: int)` | authority checks, per-entity owner metadata (an `@L_owner` array, like `@L_kind`) |

View file

@ -133,7 +133,7 @@ handler Move
### extern ### extern
```ludic ```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax) # doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # unchanged extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # unchanged
``` ```
The `= "symbol"` is a binding under Rule A — it stays. The `= "symbol"` is a binding under Rule A — it stays.

View file

@ -4,18 +4,18 @@ name: @export
category: annotations category: annotations
kind: annotation kind: annotation
tokens: @export tokens: @export
sig: @export fn name(…) -> R sig: @export function name(…) -> R
tip: Expose a function as a native symbol so a host can call it. tip: Expose a function as a native symbol so a host can call it.
order: 3 order: 3
--- ---
<code>@export</code> makes a <code>fn</code> visible outside the module as a plain native symbol, so a host program or another linked object can call it by name. Without it, functions are internal to the compiled unit; with it, the emitted symbol is externally linkable, which is how Ludic hands entry points to a runtime seam or a foreign caller. It is the outbound counterpart to <code>extern fn</code>, which pulls a foreign symbol in. Keep exported signatures to POD scalars and pointers, since they cross a native ABI boundary where Ludic's richer types do not apply. <code>@export</code> makes a <code>function</code> visible outside the module as a plain native symbol, so a host program or another linked object can call it by name. Without it, functions are internal to the compiled unit; with it, the emitted symbol is externally linkable, which is how Ludic hands entry points to a runtime seam or a foreign caller. It is the outbound counterpart to <code>extern function</code>, which pulls a foreign symbol in. Keep exported signatures to POD scalars and pointers, since they cross a native ABI boundary where Ludic's richer types do not apply.
```ludic ```ludic
program ScoreModule { program ScoreModule {
var running_total: int = 0 var running_total: int = 0
@export fn add_points(amount: int) -> int { # callable from a native host @export function add_points(amount: int) -> int { # callable from a native host
running_total = running_total + amount running_total = running_total + amount
return running_total return running_total
} }

View file

@ -4,19 +4,19 @@ name: extern
category: structure category: structure
kind: keyword kind: keyword
tokens: extern tokens: extern
sig: extern fn name(a: T) -> R = "symbol" sig: extern function name(a: T) -> R = "symbol"
tip: Bind a name to an external native symbol — the seam for platform and library calls. tip: Bind a name to an external native symbol — the seam for platform and library calls.
order: 12 order: 12
--- ---
An <code>extern fn</code> declares a function whose body lives outside Ludic and binds it to a native symbol resolved at link time. It is the seam through which Ludic reaches anything with a native interface — a system library, a math routine, or even another `.ludic` file compiled as a `module`. You write the Ludic signature you want to call and give the real symbol name after `=`; the linker connects them (pass `-L`/`-l` to `ludicc` to point at the library). Types must match the foreign ABI, so map each parameter and the return to the right Ludic type (`int`, `fixed`, `ptr`, …). An <code>extern function</code> declares a function whose body lives outside Ludic and binds it to a native symbol resolved at link time. It is the seam through which Ludic reaches anything with a native interface — a system library, a math routine, or even another `.ludic` file compiled as a `module`. You write the Ludic signature you want to call and give the real symbol name after `=`; the linker connects them (pass `-L`/`-l` to `ludicc` to point at the library). Types must match the foreign ABI, so map each parameter and the return to the right Ludic type (`int`, `fixed`, `ptr`, …).
Parameters: Parameters:
- `a` — a typed argument passed straight through to the foreign symbol - `a` — a typed argument passed straight through to the foreign symbol
```ludic ```ludic
program Physics { program Physics {
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx"
property Velocity { delta_x: int = 0, delta_y: int = 0 } property Velocity { delta_x: int = 0, delta_y: int = 0 }
model Projectile { Velocity } model Projectile { Velocity }

View file

@ -1,38 +0,0 @@
---
id: kw-fn
name: fn
category: structure
kind: keyword
tokens: fn
sig: fn name(a: T, b: T) -> R { … }
tip: A function — reusable logic called positionally or with named arguments.
order: 8
---
A <code>fn</code> declares a function: a reusable block of logic with typed parameters and a return type, written `-> R` (use `-> void` for one that returns nothing). Call it positionally, `heal(2, 10)`, or with named arguments, `heal(amount: 2, maximum: 10)`, which reads more clearly at the call site and is the house style. Functions live at program scope alongside handlers and may be called from any handler; they can `spawn`, run queries, and read program-scope state. Use them to factor out logic shared by several handlers so each handler stays short.
Parameters:
- `a`, `b` — the typed inputs; pass them positionally or by name at the call site
```ludic
program Healer {
property Health { current: int = 100, maximum: int = 100 }
model Player { Health }
fn heal(amount: int, maximum: int) -> int {
let restored = amount * 2
if restored > maximum { return maximum }
return restored
}
handler Boot phase Start {
spawn Hero { Health { current: 10, maximum: 100 } }
}
handler Recover phase Update {
for (health) in query [Health, {Player}] {
health.current = heal(amount: health.current, maximum: health.maximum)
}
}
}
```

View file

@ -0,0 +1,38 @@
---
id: kw-function
name: function
category: structure
kind: keyword
tokens: function
sig: function name(a: T, b: T) -> R { … }
tip: A function — reusable logic called positionally or with named arguments.
order: 8
---
A <code>function</code> declares a function: a reusable block of logic with typed parameters and a return type, written `-> R` (use `-> void` for one that returns nothing). Call it positionally, `heal(2, 10)`, or with named arguments, `heal(amount: 2, maximum: 10)`, which reads more clearly at the call site and is the house style. Functions live at program scope alongside handlers and may be called from any handler; they can `spawn`, run queries, and read program-scope state. Use them to factor out logic shared by several handlers so each handler stays short.
Parameters:
- `a`, `b` — the typed inputs; pass them positionally or by name at the call site
```ludic
program Healer {
property Health { current: int = 100, maximum: int = 100 }
model Player { Health }
function heal(amount: int, maximum: int) -> int {
let restored = amount * 2
if restored > maximum { return maximum }
return restored
}
handler Boot phase Start {
spawn Hero { Health { current: 10, maximum: 100 } }
}
handler Recover phase Update {
for (health) in query [Health, {Player}] {
health.current = heal(amount: health.current, maximum: health.maximum)
}
}
}
```

View file

@ -16,7 +16,7 @@ program Clamp {
property Health { current: int = 100, maximum: int = 100 } property Health { current: int = 100, maximum: int = 100 }
model Player { Health } model Player { Health }
fn clamp_current(value: int, maximum: int) -> int { function clamp_current(value: int, maximum: int) -> int {
if value < 0 { return 0 } if value < 0 { return 0 }
if value > maximum { return maximum } if value > maximum { return maximum }
return value return value

View file

@ -9,7 +9,7 @@ tip: A raw address into memory — a byte buffer from bytes(n), or an FFI handle
order: 5 order: 5
--- ---
`ptr` is a raw address into memory — the low-level type for runtime and foreign-function work, not something an everyday game reaches for. Allocate a raw byte buffer with `bytes(count)`, which returns a `ptr` you index as `buffer[index]` to read or write one byte; retype the binding as `words` / `fixeds` / `ptrs` to index in larger element sizes. A `ptr` is also how an `extern fn` passes an opaque C handle across the ABI. Test one for emptiness against the `null` literal. `ptr` is a raw address into memory — the low-level type for runtime and foreign-function work, not something an everyday game reaches for. Allocate a raw byte buffer with `bytes(count)`, which returns a `ptr` you index as `buffer[index]` to read or write one byte; retype the binding as `words` / `fixeds` / `ptrs` to index in larger element sizes. A `ptr` is also how an `extern function` passes an opaque C handle across the ABI. Test one for emptiness against the `null` literal.
```ludic ```ludic
let scratch: ptr = bytes(256) let scratch: ptr = bytes(256)

View file

@ -9,10 +9,10 @@ tip: The absence of a value — the return type of a function that returns nothi
order: 50 order: 50
--- ---
`void` is the absence of a value. It appears as the return type of a function that runs for its effect and hands nothing back — `fn reset_score() -> void { … }`. Such a function is called as a statement, not used in an expression, and a bare `return` (with no value) leaves it early. Use `void` whenever a helper mutates program state, draws, or spawns rather than computing a result to return. `void` is the absence of a value. It appears as the return type of a function that runs for its effect and hands nothing back — `function reset_score() -> void { … }`. Such a function is called as a statement, not used in an expression, and a bare `return` (with no value) leaves it early. Use `void` whenever a helper mutates program state, draws, or spawns rather than computing a result to return.
```ludic ```ludic
fn reset_score() -> void { function reset_score() -> void {
score = 0 score = 0
return return
} }

View file

@ -1,6 +1,6 @@
# rules.ludic — pure game rules & map data (functions only, no systems). # rules.ludic — pure game rules & map data (functions only, no systems).
fn tile_sprite(c: int) -> int { function tile_sprite(c: int) -> int {
match c { match c {
'T', '#' => return SPR_TREE 'T', '#' => return SPR_TREE
'H' => return SPR_HOUSE 'H' => return SPR_HOUSE
@ -11,18 +11,18 @@ fn tile_sprite(c: int) -> int {
} }
return SPR_GRASS return SPR_GRASS
} }
fn walkable(x: int, y: int) -> bool { function walkable(x: int, y: int) -> bool {
let c = Map.tile(x, y) let c = Map.tile(x, y)
return c == '.' or c == 'D' or c == 'r' or c == ',' or c == 'S' or c == 'B' or c == 'h' return c == '.' or c == 'D' or c == 'r' or c == ',' or c == 'S' or c == 'B' or c == 'h'
} }
fn is_confirm(k: int) -> bool { return k == 32 or k == 10 or k == 13 } function is_confirm(k: int) -> bool { return k == 32 or k == 10 or k == 13 }
fn enemy_name(et: int) -> str { function enemy_name(et: int) -> str {
if et == 0 { return "SLIME" } if et == 0 { return "SLIME" }
if et == 1 { return "SKELETON" } if et == 1 { return "SKELETON" }
return "RIFT WARDEN" return "RIFT WARDEN"
} }
fn build_field(x: int) -> int { function build_field(x: int) -> int {
Map.size(40, 18) Map.size(40, 18)
Map.row(0, "########################################") Map.row(0, "########################################")
Map.row(1, "#......................................#") Map.row(1, "#......................................#")
@ -44,7 +44,7 @@ fn build_field(x: int) -> int {
Map.row(17, "########################################") Map.row(17, "########################################")
return 0 return 0
} }
fn build_dungeon(x: int) -> int { function build_dungeon(x: int) -> int {
Map.size(40, 18) Map.size(40, 18)
Map.row(0, "========================================") Map.row(0, "========================================")
Map.row(1, "=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,=") Map.row(1, "=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,=")
@ -67,7 +67,7 @@ fn build_dungeon(x: int) -> int {
return 0 return 0
} }
fn grant_xp(amount: int) -> int { function grant_xp(amount: int) -> int {
for (t) in query [Stats, {Party}] { for (t) in query [Stats, {Party}] {
if t.hp > 0 { if t.hp > 0 {
t.xp = t.xp + amount t.xp = t.xp + amount

View file

@ -11,9 +11,9 @@
# ============================================================================ # ============================================================================
program Arena { program Arena {
extern fn damage(attack: int, armour: int, roll: int) -> int = "damage" extern function damage(attack: int, armour: int, roll: int) -> int = "damage"
extern fn hits_to_kill(hp: int, attack: int, armour: int) -> int = "hits_to_kill" extern function hits_to_kill(hp: int, attack: int, armour: int) -> int = "hits_to_kill"
extern fn xp_for(level: int, kills: int) -> int = "xp_for" extern function xp_for(level: int, kills: int) -> int = "xp_for"
property Fighter { hp: int = 30, attack: int = 9, armour: int = 2 } property Fighter { hp: int = 30, attack: int = 9, armour: int = 2 }

View file

@ -16,7 +16,7 @@ program Combat {
const CRIT_MULT: int = 2 const CRIT_MULT: int = 2
# damage after armour, with a crit multiplier applied on an exact roll # damage after armour, with a crit multiplier applied on an exact roll
@export fn damage(attack: int, armour: int, roll: int) -> int { @export function damage(attack: int, armour: int, roll: int) -> int {
var raw = attack - armour var raw = attack - armour
if raw < 1 { raw = 1 } if raw < 1 { raw = 1 }
if roll == 20 { raw = raw * CRIT_MULT } if roll == 20 { raw = raw * CRIT_MULT }
@ -25,7 +25,7 @@ program Combat {
# how many hits to drop a target — the loop is here so the caller cannot # how many hits to drop a target — the loop is here so the caller cannot
# accidentally disagree with `damage` about rounding # accidentally disagree with `damage` about rounding
@export fn hits_to_kill(hp: int, attack: int, armour: int) -> int { @export function hits_to_kill(hp: int, attack: int, armour: int) -> int {
var left = hp var left = hp
var n = 0 var n = 0
while left > 0 { while left > 0 {
@ -35,12 +35,12 @@ program Combat {
return n return n
} }
@export fn xp_for(level: int, kills: int) -> int { @export function xp_for(level: int, kills: int) -> int {
return curve(level) * kills return curve(level) * kills
} }
# private: not exported, so it is not a symbol in the library # private: not exported, so it is not a symbol in the library
fn curve(level: int) -> int { function curve(level: int) -> int {
return 10 + level * level * 3 return 10 + level * level * 3
} }
} }

View file

@ -14,7 +14,7 @@
# a real socket when a program binds `extern fn net_send/net_poll`). # a real socket when a program binds `extern fn net_send/net_poll`).
# The authority ships one entity's authoritative synced state to peers. # The authority ships one entity's authoritative synced state to peers.
fn rt_replicate(e: int) -> void { function rt_replicate(e: int) -> void {
let w = words(512) let w = words(512)
w[0] = e # entity id in the first word w[0] = e # entity id in the first word
let n = serialize(e, offset(w, 4)) # synced fields after it let n = serialize(e, offset(w, 4)) # synced fields after it
@ -23,7 +23,7 @@ fn rt_replicate(e: int) -> void {
# A peer drains every inbound snapshot and applies it to the named entity. One # A peer drains every inbound snapshot and applies it to the named entity. One
# datagram per poll (the transport is datagram-preserving), so loop until empty. # datagram per poll (the transport is datagram-preserving), so loop until empty.
fn rt_receive() -> void { function rt_receive() -> void {
let w = words(512) let w = words(512)
var n = net_poll(w, 2048) var n = net_poll(w, 2048)
while n > 0 { while n > 0 {

View file

@ -46,7 +46,7 @@ program Snake {
const SHADE_SCORE: int = 0xffe060 const SHADE_SCORE: int = 0xffe060
const SHADE_OVER: int = 0xff5555 const SHADE_OVER: int = 0xff5555
fn reset(unused: int) -> int { function reset(unused: int) -> int {
for (p, s) in query [Pos, Seg] { despawn self() } for (p, s) in query [Pos, Seg] { despawn self() }
spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } } spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } }
spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } } spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } }

View file

@ -29,11 +29,11 @@ var rt_alive: int = 1 # platform still running?
# 5x7 glyphs for ASCII 32..90, 7 rows per glyph, each row a 5-bit mask stored # 5x7 glyphs for ASCII 32..90, 7 rows per glyph, each row a 5-bit mask stored
# biased by '0' so the whole font is one printable string literal. # biased by '0' so the whole font is one printable string literal.
fn rt_font() -> str { function rt_font() -> str {
return "00000004444404000000000000000000000IJ4:FC000000000000000000000000000000E>O>E0044O4400000448000O000000004012448@@>ACEIA>4<4444>>A168@ON11>11N26:BO22O@N11A>>@@NAA>O124888>AA>AA>>AA?11>04004000000000024842000O0O000842480>A164040000000>AAOAAANAANAAN>A@@@A>LBAAABLO@@L@@OO@@L@@@>A@GAA>AAAOAAA>44444>7222BB<ABDHDBA@@@@@@OAKEEAAAAIEECAA>AAAAA>NAAN@@@>AAAEB=NAANDBA>A@>1A>O444444AAAAAA>AAAAA:4AAAEEKAAA:4:AAAA:4444O1248@O" return "00000004444404000000000000000000000IJ4:FC000000000000000000000000000000E>O>E0044O4400000448000O000000004012448@@>ACEIA>4<4444>>A168@ON11>11N26:BO22O@N11A>>@@NAA>O124888>AA>AA>>AA?11>04004000000000024842000O0O000842480>A164040000000>AAOAAANAANAAN>A@@@A>LBAAABLO@@L@@OO@@L@@@>A@GAA>AAAOAAA>44444>7222BB<ABDHDBA@@@@@@OAKEEAAAAIEECAA>AAAAA>NAAN@@@>AAAEB=NAANDBA>A@>1A>O444444AAAAAA>AAAAA:4AAAEEKAAA:4:AAAA:4444O1248@O"
} }
fn rt_init() -> void { function rt_init() -> void {
rt_fb = words(320 * 240) rt_fb = words(320 * 240)
rt_regs = words(64) rt_regs = words(64)
fill(rt_regs, 0, 64 * 4) fill(rt_regs, 0, 64 * 4)
@ -50,7 +50,7 @@ fn rt_init() -> void {
} }
} }
fn rt_shutdown() -> void { function rt_shutdown() -> void {
if is_windowed() { if is_windowed() {
win_close() win_close()
return return
@ -59,17 +59,17 @@ fn rt_shutdown() -> void {
} }
# ---- framebuffer ---------------------------------------------------------- # ---- framebuffer ----------------------------------------------------------
fn rt_screen_w() -> int { return rt_fbw } function rt_screen_w() -> int { return rt_fbw }
fn rt_screen_h() -> int { return rt_fbh } function rt_screen_h() -> int { return rt_fbh }
fn rt_clear(c: int) -> void { function rt_clear(c: int) -> void {
let n = rt_fbw * rt_fbh let n = rt_fbw * rt_fbh
for i in 0 .. n { for i in 0 .. n {
rt_fb[i] = c rt_fb[i] = c
} }
} }
fn rt_put_px(x: int, y: int, c: int) -> void { function rt_put_px(x: int, y: int, c: int) -> void {
if x < 0 { return } if x < 0 { return }
if y < 0 { return } if y < 0 { return }
if x >= rt_fbw { return } if x >= rt_fbw { return }
@ -77,7 +77,7 @@ fn rt_put_px(x: int, y: int, c: int) -> void {
rt_fb[y * rt_fbw + x] = c rt_fb[y * rt_fbw + x] = c
} }
fn rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void { function rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void {
let x0 = max(0, x) let x0 = max(0, x)
let y0 = max(0, y) let y0 = max(0, y)
let x1 = min(rt_fbw, x + w) let x1 = min(rt_fbw, x + w)
@ -94,7 +94,7 @@ fn rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void {
} }
} }
fn rt_frame_rect(x: int, y: int, w: int, h: int, c: int) -> void { function rt_frame_rect(x: int, y: int, w: int, h: int, c: int) -> void {
rt_fill_rect(x, y, w, 1, c) rt_fill_rect(x, y, w, 1, c)
rt_fill_rect(x, y + h - 1, w, 1, c) rt_fill_rect(x, y + h - 1, w, 1, c)
rt_fill_rect(x, y, 1, h, c) rt_fill_rect(x, y, 1, h, c)
@ -102,7 +102,7 @@ fn rt_frame_rect(x: int, y: int, w: int, h: int, c: int) -> void {
} }
# A straight line by Bresenham's algorithm — integer only, any direction. # A straight line by Bresenham's algorithm — integer only, any direction.
fn rt_line(x0: int, y0: int, x1: int, y1: int, c: int) -> void { function rt_line(x0: int, y0: int, x1: int, y1: int, c: int) -> void {
var x = x0; var y = y0 var x = x0; var y = y0
let dx = abs(x1 - x0); let dy = 0 - abs(y1 - y0) let dx = abs(x1 - x0); let dy = 0 - abs(y1 - y0)
var sx = 0 - 1; if x0 < x1 { sx = 1 } var sx = 0 - 1; if x0 < x1 { sx = 1 }
@ -118,7 +118,7 @@ fn rt_line(x0: int, y0: int, x1: int, y1: int, c: int) -> void {
} }
# A circle outline by the midpoint algorithm (eight-way symmetry). # A circle outline by the midpoint algorithm (eight-way symmetry).
fn rt_circle(cx: int, cy: int, r: int, c: int) -> void { function rt_circle(cx: int, cy: int, r: int, c: int) -> void {
if r < 0 { return } if r < 0 { return }
var x = r; var y = 0; var err = 1 - r var x = r; var y = 0; var err = 1 - r
while x >= y { while x >= y {
@ -133,7 +133,7 @@ fn rt_circle(cx: int, cy: int, r: int, c: int) -> void {
} }
# A filled disc — one horizontal span per row, width from the circle equation. # A filled disc — one horizontal span per row, width from the circle equation.
fn rt_fill_circle(cx: int, cy: int, r: int, c: int) -> void { function rt_fill_circle(cx: int, cy: int, r: int, c: int) -> void {
if r < 0 { return } if r < 0 { return }
let r2 = r * r let r2 = r * r
var dy = 0 - r var dy = 0 - r
@ -146,14 +146,14 @@ fn rt_fill_circle(cx: int, cy: int, r: int, c: int) -> void {
} }
# A triangle outline — three lines. # A triangle outline — three lines.
fn rt_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void { function rt_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void {
rt_line(x0, y0, x1, y1, c) rt_line(x0, y0, x1, y1, c)
rt_line(x1, y1, x2, y2, c) rt_line(x1, y1, x2, y2, c)
rt_line(x2, y2, x0, y0, c) rt_line(x2, y2, x0, y0, c)
} }
# A filled triangle — bounding-box scan with an edge-sign inside test. # A filled triangle — bounding-box scan with an edge-sign inside test.
fn rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void { function rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void {
let minx = min(x0, min(x1, x2)); let maxx = max(x0, max(x1, x2)) let minx = min(x0, min(x1, x2)); let maxx = max(x0, max(x1, x2))
let miny = min(y0, min(y1, y2)); let maxy = max(y0, max(y1, y2)) let miny = min(y0, min(y1, y2)); let maxy = max(y0, max(y1, y2))
var py = miny var py = miny
@ -174,14 +174,14 @@ fn rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int
# Windowed: hand the framebuffer to the platform layer, which blits it into # Windowed: hand the framebuffer to the platform layer, which blits it into
# the view. Headless: nothing to do until shutdown writes the last frame out. # the view. Headless: nothing to do until shutdown writes the last frame out.
fn rt_present() -> void { function rt_present() -> void {
if is_windowed() { if is_windowed() {
win_present(rt_fb, rt_fbw, rt_fbh) win_present(rt_fb, rt_fbw, rt_fbh)
} }
} }
# ---- text ----------------------------------------------------------------- # ---- text -----------------------------------------------------------------
fn rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void { function rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void {
var c = ch var c = ch
if c >= 97 { if c >= 97 {
if c <= 122 { c = c - 32 } if c <= 122 { c = c - 32 }
@ -203,7 +203,7 @@ fn rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void {
} }
} }
fn rt_text(x: int, y: int, s: str, colour: int, sc: int) -> void { function rt_text(x: int, y: int, s: str, colour: int, sc: int) -> void {
var i = 0 var i = 0
var cx = x var cx = x
var ch = s[0] var ch = s[0]
@ -215,7 +215,7 @@ fn rt_text(x: int, y: int, s: str, colour: int, sc: int) -> void {
} }
} }
fn rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void { function rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void {
if n == 0 { if n == 0 {
rt_glyph(x, y, 48, colour, sc) rt_glyph(x, y, 48, colour, sc)
return return
@ -246,20 +246,20 @@ fn rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void {
} }
# ---- registers ------------------------------------------------------------ # ---- registers ------------------------------------------------------------
fn rt_reg(i: int) -> int { function rt_reg(i: int) -> int {
if i < 0 { return 0 } if i < 0 { return 0 }
if i >= 64 { return 0 } if i >= 64 { return 0 }
return rt_regs[i] return rt_regs[i]
} }
fn rt_set_reg(i: int, v: int) -> void { function rt_set_reg(i: int, v: int) -> void {
if i < 0 { return } if i < 0 { return }
if i >= 64 { return } if i >= 64 { return }
rt_regs[i] = v rt_regs[i] = v
} }
# ---- rng (xorshift32) ----------------------------------------------------- # ---- rng (xorshift32) -----------------------------------------------------
fn rt_seed(s: int) -> void { function rt_seed(s: int) -> void {
if s == 0 { if s == 0 {
rt_rng = 305419896 rt_rng = 305419896
return return
@ -269,7 +269,7 @@ fn rt_seed(s: int) -> void {
# xorshift32 (Marsaglia). Runs on the raw 32-bit pattern, so the sign bit is # xorshift32 (Marsaglia). Runs on the raw 32-bit pattern, so the sign bit is
# masked off only when a caller asks for a number. # masked off only when a caller asks for a number.
fn rt_next_rand() -> int { function rt_next_rand() -> int {
var x = rt_rng var x = rt_rng
x = (x ^ (x << 13)) x = (x ^ (x << 13))
x = (x ^ (x >> 17)) x = (x ^ (x >> 17))
@ -278,35 +278,35 @@ fn rt_next_rand() -> int {
return (x & 2147483647) return (x & 2147483647)
} }
fn rt_rng_range(lo: int, hi: int) -> int { function rt_rng_range(lo: int, hi: int) -> int {
if hi <= lo { return lo } if hi <= lo { return lo }
return lo + rt_next_rand() % (hi - lo + 1) return lo + rt_next_rand() % (hi - lo + 1)
} }
fn rt_rng_chance(pct: int) -> bool { function rt_rng_chance(pct: int) -> bool {
return rt_next_rand() % 100 < pct return rt_next_rand() % 100 < pct
} }
# a deterministic fixed-point value in [0, 1) — the raw 0..65535 is exactly the # a deterministic fixed-point value in [0, 1) — the raw 0..65535 is exactly the
# Q16.16 fraction (fixed and int share the i32 representation). # Q16.16 fraction (fixed and int share the i32 representation).
fn rt_rng_value() -> fixed { function rt_rng_value() -> fixed {
return rt_rng_range(0, 65535) return rt_rng_range(0, 65535)
} }
# a deterministic integer in [0, max) — 0 when max <= 0 # a deterministic integer in [0, max) — 0 when max <= 0
fn rt_rng_int(max: int) -> int { function rt_rng_int(max: int) -> int {
if max <= 0 { return 0 } if max <= 0 { return 0 }
return rt_rng_range(0, max - 1) return rt_rng_range(0, max - 1)
} }
# a deterministic +1 or -1 # a deterministic +1 or -1
fn rt_rng_sign() -> int { function rt_rng_sign() -> int {
if rt_rng_chance(50) { return 1 } if rt_rng_chance(50) { return 1 }
return 0 - 1 return 0 - 1
} }
# ---- platform: input ------------------------------------------------------ # ---- platform: input ------------------------------------------------------
fn rt_poll() -> int { function rt_poll() -> int {
if is_windowed() { if is_windowed() {
return win_poll() return win_poll()
} }
@ -321,7 +321,7 @@ fn rt_poll() -> int {
return c return c
} }
fn rt_running() -> bool { function rt_running() -> bool {
if is_windowed() { if is_windowed() {
return win_running() return win_running()
} }
@ -329,7 +329,7 @@ fn rt_running() -> bool {
} }
# ---- writing the frame out ------------------------------------------------ # ---- writing the frame out ------------------------------------------------
fn rt_put_str(buf: ptr, at: int, s: str) -> int { function rt_put_str(buf: ptr, at: int, s: str) -> int {
var i = 0 var i = 0
var n = at var n = at
var ch = s[0] var ch = s[0]
@ -342,7 +342,7 @@ fn rt_put_str(buf: ptr, at: int, s: str) -> int {
return n return n
} }
fn rt_put_int(buf: ptr, at: int, v: int) -> int { function rt_put_int(buf: ptr, at: int, v: int) -> int {
if v == 0 { if v == 0 {
buf[at] = 48 buf[at] = 48
return at + 1 return at + 1
@ -367,7 +367,7 @@ fn rt_put_int(buf: ptr, at: int, v: int) -> int {
return n return n
} }
fn rt_dump_ppm(path: str) -> void { function rt_dump_ppm(path: str) -> void {
let f = file_open(path, "wb") let f = file_open(path, "wb")
if (f == null) { return } if (f == null) { return }
@ -406,13 +406,13 @@ var rt_map: ptr = null
var rt_mapw: int = 0 var rt_mapw: int = 0
var rt_maph: int = 0 var rt_maph: int = 0
fn rt_map_size(w: int, h: int) -> void { function rt_map_size(w: int, h: int) -> void {
rt_mapw = clamp(w, 0, 96) rt_mapw = clamp(w, 0, 96)
rt_maph = clamp(h, 0, 64) rt_maph = clamp(h, 0, 64)
fill(rt_map, 32, 96 * 64) fill(rt_map, 32, 96 * 64)
} }
fn rt_map_row(y: int, s: str) -> void { function rt_map_row(y: int, s: str) -> void {
if y < 0 { return } if y < 0 { return }
if y >= 64 { return } if y >= 64 { return }
var x = 0 var x = 0
@ -425,7 +425,7 @@ fn rt_map_row(y: int, s: str) -> void {
} }
} }
fn rt_tile(x: int, y: int) -> int { function rt_tile(x: int, y: int) -> int {
if x < 0 { return 35 } if x < 0 { return 35 }
if y < 0 { return 35 } if y < 0 { return 35 }
if x >= rt_mapw { return 35 } if x >= rt_mapw { return 35 }
@ -438,7 +438,7 @@ fn rt_tile(x: int, y: int) -> int {
# so it survives whatever the caller does with the original. # so it survives whatever the caller does with the original.
var rt_statusbuf: ptr = null var rt_statusbuf: ptr = null
fn rt_status(s: str) -> void { function rt_status(s: str) -> void {
var i = 0 var i = 0
var ch = s[0] var ch = s[0]
while ch != 0 { while ch != 0 {
@ -452,7 +452,7 @@ fn rt_status(s: str) -> void {
rt_statusbuf[i] = 0 rt_statusbuf[i] = 0
} }
fn rt_status_text() -> ptr { function rt_status_text() -> ptr {
return rt_statusbuf return rt_statusbuf
} }
@ -460,7 +460,7 @@ fn rt_status_text() -> ptr {
# The compiler writes the ECS (entities, components, archetype kinds) because # The compiler writes the ECS (entities, components, archetype kinds) because
# only it knows their shape. Everything below belongs to the runtime, so the # only it knows their shape. Everything below belongs to the runtime, so the
# runtime writes it — same order both ways. # runtime writes it — same order both ways.
fn rt_save_state(f: ptr) -> void { function rt_save_state(f: ptr) -> void {
let w: words = bytes(16) let w: words = bytes(16)
w[0] = rt_rng w[0] = rt_rng
w[1] = rt_mapw w[1] = rt_mapw
@ -473,7 +473,7 @@ fn rt_save_state(f: ptr) -> void {
free(w) free(w)
} }
fn rt_load_state(f: ptr) -> void { function rt_load_state(f: ptr) -> void {
let w: words = bytes(16) let w: words = bytes(16)
file_read(f, w, 16) file_read(f, w, 16)
rt_rng = w[0] rt_rng = w[0]

View file

@ -21,7 +21,7 @@ var img_n: int = 0
var spr_px: words = null # SPR_MAX * 16 * 16 RGBA pixels, one block var spr_px: words = null # SPR_MAX * 16 * 16 RGBA pixels, one block
var spr_n: int = 0 var spr_n: int = 0
fn rt_image_init() -> void { function rt_image_init() -> void {
img_px = bytes(IMG_MAX * 8) img_px = bytes(IMG_MAX * 8)
img_w = words(IMG_MAX) img_w = words(IMG_MAX)
img_h = words(IMG_MAX) img_h = words(IMG_MAX)
@ -30,11 +30,11 @@ fn rt_image_init() -> void {
} }
# ---- decoding ------------------------------------------------------------- # ---- decoding -------------------------------------------------------------
fn png_be32(b: ptr, at: int) -> int { function png_be32(b: ptr, at: int) -> int {
return (b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3] return (b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]
} }
fn png_tag(b: ptr, at: int, a: int, c: int, d: int, e: int) -> bool { function png_tag(b: ptr, at: int, a: int, c: int, d: int, e: int) -> bool {
if b[at] != a { return false } if b[at] != a { return false }
if b[at + 1] != c { return false } if b[at + 1] != c { return false }
if b[at + 2] != d { return false } if b[at + 2] != d { return false }
@ -42,7 +42,7 @@ fn png_tag(b: ptr, at: int, a: int, c: int, d: int, e: int) -> bool {
return true return true
} }
fn rt_read_file(path: str) -> ptr { function rt_read_file(path: str) -> ptr {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if (f == null) { return null } if (f == null) { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)
@ -62,7 +62,7 @@ var png_w: int = 0
var png_h: int = 0 var png_h: int = 0
var png_px: words = null var png_px: words = null
fn rt_decode_png(path: str) -> bool { function rt_decode_png(path: str) -> bool {
png_w = 0 png_w = 0
png_h = 0 png_h = 0
png_px = null png_px = null
@ -227,7 +227,7 @@ fn rt_decode_png(path: str) -> bool {
} }
# ---- images --------------------------------------------------------------- # ---- images ---------------------------------------------------------------
fn rt_image_load(path: str) -> int { function rt_image_load(path: str) -> int {
if img_n >= IMG_MAX { return 0 - 1 } if img_n >= IMG_MAX { return 0 - 1 }
if rt_decode_png(path) == false { return 0 - 1 } if rt_decode_png(path) == false { return 0 - 1 }
let id = img_n let id = img_n
@ -238,7 +238,7 @@ fn rt_image_load(path: str) -> int {
return id return id
} }
fn rt_blend_px(x: int, y: int, argb: int) -> void { function rt_blend_px(x: int, y: int, argb: int) -> void {
let a = ((argb >> 24) & 255) let a = ((argb >> 24) & 255)
if a == 0 { return } if a == 0 { return }
if x < 0 { return } if x < 0 { return }
@ -260,7 +260,7 @@ fn rt_blend_px(x: int, y: int, argb: int) -> void {
rt_fb[y * rt_fbw + x] = (((r << 16) | (g << 8)) | b) rt_fb[y * rt_fbw + x] = (((r << 16) | (g << 8)) | b)
} }
fn rt_draw_image(id: int, dx: int, dy: int) -> void { function rt_draw_image(id: int, dx: int, dy: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= img_n { return } if id >= img_n { return }
let w = img_w[id] let w = img_w[id]
@ -273,7 +273,7 @@ fn rt_draw_image(id: int, dx: int, dy: int) -> void {
} }
} }
fn rt_draw_image_scaled(id: int, dx: int, dy: int, dw: int, dh: int) -> void { function rt_draw_image_scaled(id: int, dx: int, dy: int, dw: int, dh: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= img_n { return } if id >= img_n { return }
if dw <= 0 { return } if dw <= 0 { return }
@ -290,7 +290,7 @@ fn rt_draw_image_scaled(id: int, dx: int, dy: int, dw: int, dh: int) -> void {
# map one destination axis onto the source for a 9-slice: the two insets are # map one destination axis onto the source for a 9-slice: the two insets are
# copied 1:1 and only the middle stretches # copied 1:1 and only the middle stretches
fn rt_9map(d: int, dsz: int, ssz: int, inset: int) -> int { function rt_9map(d: int, dsz: int, ssz: int, inset: int) -> int {
if inset * 2 >= dsz { return d * ssz / max(dsz, 1) } if inset * 2 >= dsz { return d * ssz / max(dsz, 1) }
if inset * 2 >= ssz { return d * ssz / max(dsz, 1) } if inset * 2 >= ssz { return d * ssz / max(dsz, 1) }
if d < inset { return d } if d < inset { return d }
@ -298,7 +298,7 @@ fn rt_9map(d: int, dsz: int, ssz: int, inset: int) -> int {
return inset + (d - inset) * (ssz - 2 * inset) / (dsz - 2 * inset) return inset + (d - inset) * (ssz - 2 * inset) / (dsz - 2 * inset)
} }
fn rt_draw_9slice(id: int, dx: int, dy: int, dw: int, dh: int, inset: int) -> void { function rt_draw_9slice(id: int, dx: int, dy: int, dw: int, dh: int, inset: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= img_n { return } if id >= img_n { return }
if dw <= 0 { return } if dw <= 0 { return }
@ -315,7 +315,7 @@ fn rt_draw_9slice(id: int, dx: int, dy: int, dw: int, dh: int, inset: int) -> vo
} }
# ---- sprites (16x16 art) -------------------------------------------------- # ---- sprites (16x16 art) --------------------------------------------------
fn rt_png_load(path: str) -> int { function rt_png_load(path: str) -> int {
if spr_n >= SPR_MAX { return 0 - 1 } if spr_n >= SPR_MAX { return 0 - 1 }
if rt_decode_png(path) == false { return 0 - 1 } if rt_decode_png(path) == false { return 0 - 1 }
let id = spr_n let id = spr_n
@ -338,7 +338,7 @@ fn rt_png_load(path: str) -> int {
return id return id
} }
fn rt_draw_sprite(id: int, px: int, py: int) -> void { function rt_draw_sprite(id: int, px: int, py: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= spr_n { return } if id >= spr_n { return }
let base = id * SPR_SZ * SPR_SZ let base = id * SPR_SZ * SPR_SZ
@ -352,7 +352,7 @@ fn rt_draw_sprite(id: int, px: int, py: int) -> void {
} }
} }
fn rt_draw_sprite_scaled(id: int, px: int, py: int, sc: int) -> void { function rt_draw_sprite_scaled(id: int, px: int, py: int, sc: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= spr_n { return } if id >= spr_n { return }
if sc < 1 { return } if sc < 1 { return }
@ -368,14 +368,14 @@ fn rt_draw_sprite_scaled(id: int, px: int, py: int, sc: int) -> void {
} }
# ---- .lspr sprite sheets (palette + ASCII rows) --------------------------- # ---- .lspr sprite sheets (palette + ASCII rows) ---------------------------
fn rt_hexval(c: int) -> int { function rt_hexval(c: int) -> int {
if c >= 48 { if c <= 57 { return c - 48 } } if c >= 48 { if c <= 57 { return c - 48 } }
if c >= 97 { if c <= 102 { return c - 87 } } if c >= 97 { if c <= 102 { return c - 87 } }
if c >= 65 { if c <= 70 { return c - 55 } } if c >= 65 { if c <= 70 { return c - 55 } }
return 0 - 1 return 0 - 1
} }
fn rt_sprites_load(path: str) -> void { function rt_sprites_load(path: str) -> void {
let d = rt_read_file(path) let d = rt_read_file(path)
if (d == null) { spr_n = 0; return } if (d == null) { spr_n = 0; return }
let size = rt_file_len let size = rt_file_len

View file

@ -20,7 +20,7 @@ var z_bitbuf: int = 0
var z_bitcnt: int = 0 var z_bitcnt: int = 0
var z_err: int = 0 var z_err: int = 0
fn z_start(src: ptr, len: int) -> void { function z_start(src: ptr, len: int) -> void {
z_src = src z_src = src
z_len = len z_len = len
z_pos = 0 z_pos = 0
@ -29,7 +29,7 @@ fn z_start(src: ptr, len: int) -> void {
z_err = 0 z_err = 0
} }
fn z_bits(need: int) -> int { function z_bits(need: int) -> int {
var val = z_bitbuf var val = z_bitbuf
while z_bitcnt < need { while z_bitcnt < need {
if z_pos >= z_len { if z_pos >= z_len {
@ -48,12 +48,12 @@ fn z_bits(need: int) -> int {
# ---- Huffman tables ------------------------------------------------------- # ---- Huffman tables -------------------------------------------------------
# A table is a single buffer: 16 length-counts followed by the symbols in # A table is a single buffer: 16 length-counts followed by the symbols in
# canonical order. One allocation, no structs. # canonical order. One allocation, no structs.
fn z_table_new(nsym: int) -> ptr { function z_table_new(nsym: int) -> ptr {
return words((16 + nsym)) return words((16 + nsym))
} }
# lengths[i] = code length of symbol i (0 = symbol unused) # lengths[i] = code length of symbol i (0 = symbol unused)
fn z_table_build(table: words, lengths: words, n: int) -> void { function z_table_build(table: words, lengths: words, n: int) -> void {
for i in 0 .. 16 { for i in 0 .. 16 {
table[i] = 0 table[i] = 0
} }
@ -78,7 +78,7 @@ fn z_table_build(table: words, lengths: words, n: int) -> void {
free(offs) free(offs)
} }
fn z_decode(table: words) -> int { function z_decode(table: words) -> int {
var code = 0 var code = 0
var first = 0 var first = 0
var index = 0 var index = 0
@ -97,7 +97,7 @@ fn z_decode(table: words) -> int {
} }
# ---- length / distance code tables (RFC 1951 section 3.2.5) --------------- # ---- length / distance code tables (RFC 1951 section 3.2.5) ---------------
fn z_len_base(sym: int) -> int { function z_len_base(sym: int) -> int {
if sym < 8 { return 3 + sym } if sym < 8 { return 3 + sym }
if sym == 28 { return 258 } if sym == 28 { return 258 }
let extra = (sym - 4) / 4 let extra = (sym - 4) / 4
@ -105,27 +105,27 @@ fn z_len_base(sym: int) -> int {
return 3 + ((group - 1) << 2) + 4 + (sym - 4 - extra * 4) * group return 3 + ((group - 1) << 2) + 4 + (sym - 4 - extra * 4) * group
} }
fn z_len_extra(sym: int) -> int { function z_len_extra(sym: int) -> int {
if sym < 8 { return 0 } if sym < 8 { return 0 }
if sym == 28 { return 0 } if sym == 28 { return 0 }
return (sym - 4) / 4 return (sym - 4) / 4
} }
fn z_dist_base(sym: int) -> int { function z_dist_base(sym: int) -> int {
if sym < 4 { return 1 + sym } if sym < 4 { return 1 + sym }
let extra = (sym - 2) / 2 let extra = (sym - 2) / 2
let group = (1 << extra) let group = (1 << extra)
return 1 + (group << 1) + (sym - 2 - extra * 2) * group return 1 + (group << 1) + (sym - 2 - extra * 2) * group
} }
fn z_dist_extra(sym: int) -> int { function z_dist_extra(sym: int) -> int {
if sym < 4 { return 0 } if sym < 4 { return 0 }
return (sym - 2) / 2 return (sym - 2) / 2
} }
# ---- block decoders ------------------------------------------------------- # ---- block decoders -------------------------------------------------------
# `out` is the destination window; returns the new write position, or -1. # `out` is the destination window; returns the new write position, or -1.
fn z_stored(out: ptr, at: int, cap: int) -> int { function z_stored(out: ptr, at: int, cap: int) -> int {
z_bitbuf = 0 z_bitbuf = 0
z_bitcnt = 0 # stored blocks are byte-aligned z_bitcnt = 0 # stored blocks are byte-aligned
if z_pos + 4 > z_len { return -1 } if z_pos + 4 > z_len { return -1 }
@ -142,7 +142,7 @@ fn z_stored(out: ptr, at: int, cap: int) -> int {
return w return w
} }
fn z_codes(out: ptr, at: int, cap: int, lit: ptr, dist: ptr) -> int { function z_codes(out: ptr, at: int, cap: int, lit: ptr, dist: ptr) -> int {
var w = at var w = at
var sym = z_decode(lit) var sym = z_decode(lit)
while sym != 256 { while sym != 256 {
@ -172,7 +172,7 @@ fn z_codes(out: ptr, at: int, cap: int, lit: ptr, dist: ptr) -> int {
return w return w
} }
fn z_fixed_tables(lit: ptr, dist: ptr) -> void { function z_fixed_tables(lit: ptr, dist: ptr) -> void {
let lengths: words = words(288) let lengths: words = words(288)
for i in 0 .. 144 { lengths[i] = 8 } for i in 0 .. 144 { lengths[i] = 8 }
for i in 144 .. 256 { lengths[i] = 9 } for i in 144 .. 256 { lengths[i] = 9 }
@ -184,7 +184,7 @@ fn z_fixed_tables(lit: ptr, dist: ptr) -> void {
free(lengths) free(lengths)
} }
fn z_dynamic_tables(lit: ptr, dist: ptr) -> int { function z_dynamic_tables(lit: ptr, dist: ptr) -> int {
let nlen = z_bits(5) + 257 let nlen = z_bits(5) + 257
let ndist = z_bits(5) + 1 let ndist = z_bits(5) + 1
let ncode = z_bits(4) + 4 let ncode = z_bits(4) + 4
@ -240,7 +240,7 @@ fn z_dynamic_tables(lit: ptr, dist: ptr) -> int {
} }
# Inflate a raw DEFLATE stream. Returns bytes written, or -1. # Inflate a raw DEFLATE stream. Returns bytes written, or -1.
fn z_inflate(src: ptr, len: int, out: ptr, cap: int) -> int { function z_inflate(src: ptr, len: int, out: ptr, cap: int) -> int {
z_start(src, len) z_start(src, len)
let lit = z_table_new(288) let lit = z_table_new(288)
let dist = z_table_new(30) let dist = z_table_new(30)
@ -268,7 +268,7 @@ fn z_inflate(src: ptr, len: int, out: ptr, cap: int) -> int {
} }
# zlib wrapper (RFC 1950): two header bytes, then DEFLATE, then Adler-32. # zlib wrapper (RFC 1950): two header bytes, then DEFLATE, then Adler-32.
fn z_uncompress(src: ptr, len: int, out: ptr, cap: int) -> int { function z_uncompress(src: ptr, len: int, out: ptr, cap: int) -> int {
if len < 2 { return -1 } if len < 2 { return -1 }
let cmf = src[0] let cmf = src[0]
if (cmf & 15) != 8 { return -1 } if (cmf & 15) != 8 { return -1 }

View file

@ -63,7 +63,7 @@ var gc_oy: words = null
var gc_adv: words = null var gc_adv: words = null
var gc_bmp: ptrs = null var gc_bmp: ptrs = null
fn rt_tt_init() -> void { function rt_tt_init() -> void {
tt_data = bytes(TT_MAX * 8) tt_data = bytes(TT_MAX * 8)
tt_size = words(TT_MAX) tt_size = words(TT_MAX)
tt_upem = words(TT_MAX) tt_upem = words(TT_MAX)
@ -105,24 +105,24 @@ fn rt_tt_init() -> void {
} }
# ---- big-endian readers --------------------------------------------------- # ---- big-endian readers ---------------------------------------------------
fn tt_u16(d: ptr, at: int) -> int { function tt_u16(d: ptr, at: int) -> int {
return ((d[at] << 8) | d[at + 1]) return ((d[at] << 8) | d[at + 1])
} }
fn tt_i16(d: ptr, at: int) -> int { function tt_i16(d: ptr, at: int) -> int {
let v = tt_u16(d, at) let v = tt_u16(d, at)
if v >= 32768 { return v - 65536 } if v >= 32768 { return v - 65536 }
return v return v
} }
fn tt_u32(d: ptr, at: int) -> int { function tt_u32(d: ptr, at: int) -> int {
return (d[at] << 24) | (d[at + 1] << 16) | (d[at + 2] << 8) | d[at + 3] return (d[at] << 24) | (d[at + 1] << 16) | (d[at + 2] << 8) | d[at + 3]
} }
fn tt_i8(d: ptr, at: int) -> int { function tt_i8(d: ptr, at: int) -> int {
let v = d[at] let v = d[at]
if v >= 128 { return v - 256 } if v >= 128 { return v - 256 }
return v return v
} }
fn tt_find_table(d: ptr, base: int, a: int, b: int, c: int, e: int) -> int { function tt_find_table(d: ptr, base: int, a: int, b: int, c: int, e: int) -> int {
let n = tt_u16(d, base + 4) let n = tt_u16(d, base + 4)
for i in 0 .. n { for i in 0 .. n {
let rec = base + 12 + i * 16 let rec = base + 12 + i * 16
@ -138,7 +138,7 @@ fn tt_find_table(d: ptr, base: int, a: int, b: int, c: int, e: int) -> int {
} }
# pick the most capable Unicode subtable, as the C loader did # pick the most capable Unicode subtable, as the C loader did
fn tt_pick_cmap(id: int, d: ptr, co: int) -> void { function tt_pick_cmap(id: int, d: ptr, co: int) -> void {
tt_cmap[id] = 0 - 1 tt_cmap[id] = 0 - 1
tt_cfmt[id] = 0 tt_cfmt[id] = 0
if co < 0 { return } if co < 0 { return }
@ -166,7 +166,7 @@ fn tt_pick_cmap(id: int, d: ptr, co: int) -> void {
tt_cfmt[id] = tt_u16(d, best) tt_cfmt[id] = tt_u16(d, best)
} }
fn rt_font_load(path: str) -> int { function rt_font_load(path: str) -> int {
if tt_n >= TT_MAX { return 0 - 1 } if tt_n >= TT_MAX { return 0 - 1 }
let d = rt_read_file(path) let d = rt_read_file(path)
if (d == null) { return 0 - 1 } if (d == null) { return 0 - 1 }
@ -212,7 +212,7 @@ fn rt_font_load(path: str) -> int {
} }
# ---- codepoint -> glyph id ------------------------------------------------ # ---- codepoint -> glyph id ------------------------------------------------
fn tt_glyph_index(id: int, cp: int) -> int { function tt_glyph_index(id: int, cp: int) -> int {
let d = tt_data[id] let d = tt_data[id]
let s = tt_cmap[id] let s = tt_cmap[id]
if s < 0 { return 0 } if s < 0 { return 0 }
@ -261,7 +261,7 @@ fn tt_glyph_index(id: int, cp: int) -> int {
return 0 return 0
} }
fn tt_advance(id: int, gid: int) -> int { function tt_advance(id: int, gid: int) -> int {
let d = tt_data[id] let d = tt_data[id]
let hmtx = tt_hmtx[id] let hmtx = tt_hmtx[id]
let n = tt_nhm[id] let n = tt_nhm[id]
@ -270,7 +270,7 @@ fn tt_advance(id: int, gid: int) -> int {
} }
# ---- outline extraction --------------------------------------------------- # ---- outline extraction ---------------------------------------------------
fn ol_pt(x: fixed, y: fixed, on: int) -> void { function ol_pt(x: fixed, y: fixed, on: int) -> void {
if ol_n >= TT_PTS { return } if ol_n >= TT_PTS { return }
ol_x[ol_n] = x ol_x[ol_n] = x
ol_y[ol_n] = y ol_y[ol_n] = y
@ -278,13 +278,13 @@ fn ol_pt(x: fixed, y: fixed, on: int) -> void {
ol_n = ol_n + 1 ol_n = ol_n + 1
} }
fn tt_glyph_start(id: int, gid: int) -> int { function tt_glyph_start(id: int, gid: int) -> int {
let d = tt_data[id] let d = tt_data[id]
let loca = tt_loca[id] let loca = tt_loca[id]
if tt_locfm[id] == 0 { return tt_u16(d, loca + gid * 2) * 2 } if tt_locfm[id] == 0 { return tt_u16(d, loca + gid * 2) * 2 }
return tt_u32(d, loca + gid * 4) return tt_u32(d, loca + gid * 4)
} }
fn tt_glyph_end(id: int, gid: int) -> int { function tt_glyph_end(id: int, gid: int) -> int {
let d = tt_data[id] let d = tt_data[id]
let loca = tt_loca[id] let loca = tt_loca[id]
if tt_locfm[id] == 0 { return tt_u16(d, loca + (gid + 1) * 2) * 2 } if tt_locfm[id] == 0 { return tt_u16(d, loca + (gid + 1) * 2) * 2 }
@ -292,7 +292,7 @@ fn tt_glyph_end(id: int, gid: int) -> int {
} }
# Append glyph `gid`, transformed by [a b c e] + (dx,dy), to the outline. # Append glyph `gid`, transformed by [a b c e] + (dx,dy), to the outline.
fn tt_load_outline(id: int, gid: int, a: fixed, b: fixed, c: fixed, e: fixed, dx: fixed, dy: fixed, depth: int) -> void { function tt_load_outline(id: int, gid: int, a: fixed, b: fixed, c: fixed, e: fixed, dx: fixed, dy: fixed, depth: int) -> void {
if gid < 0 { return } if gid < 0 { return }
if gid >= tt_nglyf[id] { return } if gid >= tt_nglyf[id] { return }
if depth > 5 { return } if depth > 5 { return }

View file

@ -2,7 +2,7 @@
# and text drawing. Split out of truetype.ludic (the sfnt/cmap/outline half). # and text drawing. Split out of truetype.ludic (the sfnt/cmap/outline half).
# ---- rasterization -------------------------------------------------------- # ---- rasterization --------------------------------------------------------
fn ed_add(x0: fixed, y0: fixed, x1: fixed, y1: fixed) -> void { function ed_add(x0: fixed, y0: fixed, x1: fixed, y1: fixed) -> void {
if ed_n >= TT_EDGES { return } if ed_n >= TT_EDGES { return }
ed_x0[ed_n] = x0 ed_x0[ed_n] = x0
ed_y0[ed_n] = y0 ed_y0[ed_n] = y0
@ -11,7 +11,7 @@ fn ed_add(x0: fixed, y0: fixed, x1: fixed, y1: fixed) -> void {
ed_n = ed_n + 1 ed_n = ed_n + 1
} }
fn tt_isqrt(v: int) -> int { function tt_isqrt(v: int) -> int {
if v <= 0 { return 0 } if v <= 0 { return 0 }
var r = 0 var r = 0
var b = 32768 var b = 32768
@ -24,7 +24,7 @@ fn tt_isqrt(v: int) -> int {
} }
# flatten one quadratic Bézier into line segments, subdivided by chord length # flatten one quadratic Bézier into line segments, subdivided by chord length
fn ed_quad(x0: fixed, y0: fixed, cx: fixed, cy: fixed, x1: fixed, y1: fixed) -> void { function ed_quad(x0: fixed, y0: fixed, cx: fixed, cy: fixed, x1: fixed, y1: fixed) -> void {
let dx = flr(x1) - flr(x0) let dx = flr(x1) - flr(x0)
let dy = flr(y1) - flr(y0) let dy = flr(y1) - flr(y0)
var n = tt_isqrt(dx * dx + dy * dy) / 3 var n = tt_isqrt(dx * dx + dy * dy) / 3
@ -51,7 +51,7 @@ var gr_ox: int = 0
var gr_oy: int = 0 var gr_oy: int = 0
var gr_adv: int = 0 var gr_adv: int = 0
fn tt_raster(id: int, gid: int, px: int) -> ptr { function tt_raster(id: int, gid: int, px: int) -> ptr {
let upem = tt_upem[id] let upem = tt_upem[id]
let scale = fx(px) / upem let scale = fx(px) / upem
gr_adv = flr(fx(tt_advance(id, gid)) * scale + 0.5) gr_adv = flr(fx(tt_advance(id, gid)) * scale + 0.5)
@ -231,11 +231,11 @@ fn tt_raster(id: int, gid: int, px: int) -> ptr {
# ---- glyph cache ---------------------------------------------------------- # ---- glyph cache ----------------------------------------------------------
# Rasterizing is far too slow to repeat per frame, so a coverage mask is kept # Rasterizing is far too slow to repeat per frame, so a coverage mask is kept
# per (font, codepoint, size). Open addressing, 8 probes, evict on miss. # per (font, codepoint, size). Open addressing, 8 probes, evict on miss.
fn gc_hash(font: int, cp: int, px: int) -> int { function gc_hash(font: int, cp: int, px: int) -> int {
return ((((cp * 2654435761) ^ (font << 20)) ^ (px << 8)) & (TT_GC - 1)) return ((((cp * 2654435761) ^ (font << 20)) ^ (px << 8)) & (TT_GC - 1))
} }
fn tt_glyph_get(font: int, cp: int, px: int) -> int { function tt_glyph_get(font: int, cp: int, px: int) -> int {
let h = gc_hash(font, cp, px) let h = gc_hash(font, cp, px)
for k in 0 .. 8 { for k in 0 .. 8 {
let s = ((h + k) & (TT_GC - 1)) let s = ((h + k) & (TT_GC - 1))
@ -269,7 +269,7 @@ fn tt_glyph_get(font: int, cp: int, px: int) -> int {
# ---- UTF-8 ---------------------------------------------------------------- # ---- UTF-8 ----------------------------------------------------------------
var u8_next: int = 0 # byte index just past the codepoint last decoded var u8_next: int = 0 # byte index just past the codepoint last decoded
fn tt_utf8(s: str, at: int) -> int { function tt_utf8(s: str, at: int) -> int {
let c = s[at] let c = s[at]
var extra = 0 - 1 var extra = 0 - 1
if c < 128 { extra = 0 } if c < 128 { extra = 0 }
@ -298,7 +298,7 @@ fn tt_utf8(s: str, at: int) -> int {
} }
# ---- drawing -------------------------------------------------------------- # ---- drawing --------------------------------------------------------------
fn tt_blit(bmp: ptr, w: int, h: int, dx: int, dy: int, colour: int) -> void { function tt_blit(bmp: ptr, w: int, h: int, dx: int, dy: int, colour: int) -> void {
if (bmp == null) { return } if (bmp == null) { return }
let cr = ((colour >> 16) & 255) let cr = ((colour >> 16) & 255)
let cg = ((colour >> 8) & 255) let cg = ((colour >> 8) & 255)
@ -327,7 +327,7 @@ fn tt_blit(bmp: ptr, w: int, h: int, dx: int, dy: int, colour: int) -> void {
} }
} }
fn rt_text_ttf(font: int, x: int, y: int, s: str, colour: int, px: int) -> void { function rt_text_ttf(font: int, x: int, y: int, s: str, colour: int, px: int) -> void {
if font < 0 { return } if font < 0 { return }
if font >= tt_n { return } if font >= tt_n { return }
if px <= 0 { return } if px <= 0 { return }
@ -351,7 +351,7 @@ fn rt_text_ttf(font: int, x: int, y: int, s: str, colour: int, px: int) -> void
} }
} }
fn rt_text_w(font: int, s: str, px: int) -> int { function rt_text_w(font: int, s: str, px: int) -> int {
if font < 0 { return 0 } if font < 0 { return 0 }
if font >= tt_n { return 0 } if font >= tt_n { return 0 }
if px <= 0 { return 0 } if px <= 0 { return 0 }
@ -372,7 +372,7 @@ fn rt_text_w(font: int, s: str, px: int) -> int {
return best return best
} }
fn rt_text_h(font: int, px: int) -> int { function rt_text_h(font: int, px: int) -> int {
if font < 0 { return 0 } if font < 0 { return 0 }
if font >= tt_n { return 0 } if font >= tt_n { return 0 }
let scale = fx(px) / tt_upem[font] let scale = fx(px) / tt_upem[font]

View file

@ -79,7 +79,7 @@ var ui_n: int = 0
var ui_active: int = -1 var ui_active: int = -1
var ui_focus: int = -1 var ui_focus: int = -1
fn rt_ui_init() -> void { function rt_ui_init() -> void {
ui_type = words(UI_MAX) ui_type = words(UI_MAX)
ui_parent = words(UI_MAX) ui_parent = words(UI_MAX)
ui_w = words(UI_MAX) ui_w = words(UI_MAX)
@ -112,7 +112,7 @@ fn rt_ui_init() -> void {
} }
# ---- build-time interface (called by compiler-emitted code) --------------- # ---- build-time interface (called by compiler-emitted code) ---------------
fn rt_ui_reset(n: int) -> void { function rt_ui_reset(n: int) -> void {
ui_n = n ui_n = n
for i in 0 .. n { for i in 0 .. n {
ui_type[i] = 0 ui_type[i] = 0
@ -142,7 +142,7 @@ fn rt_ui_reset(n: int) -> void {
} }
} }
fn rt_ui_set(i: int, k: int, v: int) -> void { function rt_ui_set(i: int, k: int, v: int) -> void {
if i < 0 { return } if i < 0 { return }
if i >= UI_MAX { return } if i >= UI_MAX { return }
if k == K_TYPE { ui_type[i] = v } if k == K_TYPE { ui_type[i] = v }
@ -168,21 +168,21 @@ fn rt_ui_set(i: int, k: int, v: int) -> void {
if k == K_SKIN { ui_skin[i] = v } if k == K_SKIN { ui_skin[i] = v }
} }
fn rt_ui_static_text(i: int, s: str) -> void { function rt_ui_static_text(i: int, s: str) -> void {
if i < 0 { return } if i < 0 { return }
if i >= UI_MAX { return } if i >= UI_MAX { return }
ui_text[i] = s ui_text[i] = s
} }
# ---- text helpers: TrueType when a font is loaded, bitmap otherwise ------- # ---- text helpers: TrueType when a font is loaded, bitmap otherwise -------
fn ui_str(i: int) -> ptr { function ui_str(i: int) -> ptr {
if ui_hasdyn[i] == 1 { return offset(ui_dyn, i * 96) } if ui_hasdyn[i] == 1 { return offset(ui_dyn, i * 96) }
let t = ui_text[i] let t = ui_text[i]
if (t == null) { return offset(ui_dyn, i * 96) } if (t == null) { return offset(ui_dyn, i * 96) }
return t return t
} }
fn ui_draw_text(font: int, x: int, y: int, s: ptr, colour: int, size: int) -> void { function ui_draw_text(font: int, x: int, y: int, s: ptr, colour: int, size: int) -> void {
if font >= 0 { if font >= 0 {
if font < tt_n { if font < tt_n {
rt_text_ttf(font, x, y, s, colour, size) rt_text_ttf(font, x, y, s, colour, size)
@ -192,7 +192,7 @@ fn ui_draw_text(font: int, x: int, y: int, s: ptr, colour: int, size: int) -> vo
rt_text(x, y, s, colour, max(size / 8, 1)) rt_text(x, y, s, colour, max(size / 8, 1))
} }
fn ui_tw(font: int, s: ptr, size: int) -> int { function ui_tw(font: int, s: ptr, size: int) -> int {
if font >= 0 { if font >= 0 {
if font < tt_n { return rt_text_w(font, s, size) } if font < tt_n { return rt_text_w(font, s, size) }
} }
@ -201,24 +201,24 @@ fn ui_tw(font: int, s: ptr, size: int) -> int {
return n * 6 * max(size / 8, 1) return n * 6 * max(size / 8, 1)
} }
fn ui_th(font: int, size: int) -> int { function ui_th(font: int, size: int) -> int {
if font >= 0 { if font >= 0 {
if font < tt_n { return rt_text_h(font, size) } if font < tt_n { return rt_text_h(font, size) }
} }
return 7 * max(size / 8, 1) return 7 * max(size / 8, 1)
} }
fn ui_is_container(t: int) -> bool { function ui_is_container(t: int) -> bool {
if t == WT_PANEL { return true } if t == WT_PANEL { return true }
if t == WT_COL { return true } if t == WT_COL { return true }
if t == WT_ROW { return true } if t == WT_ROW { return true }
return false return false
} }
fn ui_dir(t: int) -> int { function ui_dir(t: int) -> int {
if t == WT_ROW { return 0 } if t == WT_ROW { return 0 }
return 1 return 1
} }
fn ui_under(i: int, root: int) -> bool { function ui_under(i: int, root: int) -> bool {
var k = i var k = i
while k >= 0 { while k >= 0 {
if k == root { return true } if k == root { return true }
@ -228,7 +228,7 @@ fn ui_under(i: int, root: int) -> bool {
} }
# ---- measure: content sizes, children before parents ---------------------- # ---- measure: content sizes, children before parents ----------------------
fn ui_measure() -> void { function ui_measure() -> void {
var i = ui_n - 1 var i = ui_n - 1
while i >= 0 { while i >= 0 {
if ui_visible[i] == 0 { if ui_visible[i] == 0 {
@ -303,7 +303,7 @@ fn ui_measure() -> void {
} }
} }
fn ui_eff_align(child: int, parent: int) -> int { function ui_eff_align(child: int, parent: int) -> int {
if ui_align[child] >= 0 { return ui_align[child] } if ui_align[child] >= 0 { return ui_align[child] }
if ui_align[parent] >= 0 { return ui_align[parent] } if ui_align[parent] >= 0 { return ui_align[parent] }
return 0 return 0

View file

@ -2,7 +2,7 @@
# accessors. Split out of ui.ludic (the storage/build/measure half). # accessors. Split out of ui.ludic (the storage/build/measure half).
# ---- arrange: rects, parents before children ------------------------------ # ---- arrange: rects, parents before children ------------------------------
fn ui_arrange(i: int, x: int, y: int) -> void { function ui_arrange(i: int, x: int, y: int) -> void {
ui_rx[i] = x ui_rx[i] = x
ui_ry[i] = y ui_ry[i] = y
let t = ui_type[i] let t = ui_type[i]
@ -61,7 +61,7 @@ fn ui_arrange(i: int, x: int, y: int) -> void {
} }
} }
fn ui_layout() -> void { function ui_layout() -> void {
if ui_active < 0 { return } if ui_active < 0 { return }
ui_measure() ui_measure()
var px = (rt_fbw - ui_rw[ui_active]) / 2 var px = (rt_fbw - ui_rw[ui_active]) / 2
@ -74,7 +74,7 @@ fn ui_layout() -> void {
} }
# ---- drawing -------------------------------------------------------------- # ---- drawing --------------------------------------------------------------
fn ui_lighten(c: int) -> int { function ui_lighten(c: int) -> int {
var r = ((c >> 16) & 255) var r = ((c >> 16) & 255)
var g = ((c >> 8) & 255) var g = ((c >> 8) & 255)
var b = (c & 255) var b = (c & 255)
@ -84,7 +84,7 @@ fn ui_lighten(c: int) -> int {
return (((r << 16) | (g << 8)) | b) return (((r << 16) | (g << 8)) | b)
} }
fn ui_draw_node(i: int) -> void { function ui_draw_node(i: int) -> void {
if ui_visible[i] == 0 { return } if ui_visible[i] == 0 { return }
let t = ui_type[i] let t = ui_type[i]
let rx = ui_rx[i] let rx = ui_rx[i]
@ -148,7 +148,7 @@ fn ui_draw_node(i: int) -> void {
# ---- focus and activation ------------------------------------------------- # ---- focus and activation -------------------------------------------------
var ui_fl: words = null var ui_fl: words = null
fn ui_focusables() -> int { function ui_focusables() -> int {
if (ui_fl == null) { ui_fl = words(UI_MAX) } if (ui_fl == null) { ui_fl = words(UI_MAX) }
var n = 0 var n = 0
for i in 0 .. ui_n { for i in 0 .. ui_n {
@ -164,14 +164,14 @@ fn ui_focusables() -> int {
return n return n
} }
fn rt_ui_open(root: int) -> void { function rt_ui_open(root: int) -> void {
ui_active = root ui_active = root
let n = ui_focusables() let n = ui_focusables()
ui_focus = 0 - 1 ui_focus = 0 - 1
if n > 0 { ui_focus = ui_fl[0] } if n > 0 { ui_focus = ui_fl[0] }
} }
fn rt_ui_tick(k: int) -> void { function rt_ui_tick(k: int) -> void {
if ui_active < 0 { return } if ui_active < 0 { return }
for i in 0 .. ui_n { ui_fired[i] = 0 } for i in 0 .. ui_n { ui_fired[i] = 0 }
ui_layout() ui_layout()
@ -201,20 +201,20 @@ fn rt_ui_tick(k: int) -> void {
if k == 13 { ui_fired[ui_focus] = 1 } if k == 13 { ui_fired[ui_focus] = 1 }
} }
fn rt_ui_render() -> void { function rt_ui_render() -> void {
if ui_active < 0 { return } if ui_active < 0 { return }
ui_layout() ui_layout()
ui_draw_node(ui_active) ui_draw_node(ui_active)
} }
# ---- game-facing accessors ------------------------------------------------ # ---- game-facing accessors ------------------------------------------------
fn rt_ui_clicked(id: int) -> bool { function rt_ui_clicked(id: int) -> bool {
if id < 0 { return false } if id < 0 { return false }
if id >= ui_n { return false } if id >= ui_n { return false }
return ui_fired[id] == 1 return ui_fired[id] == 1
} }
fn rt_ui_set_text(id: int, s: str) -> void { function rt_ui_set_text(id: int, s: str) -> void {
if id < 0 { return } if id < 0 { return }
if id >= ui_n { return } if id >= ui_n { return }
let base = id * 96 let base = id * 96
@ -232,7 +232,7 @@ fn rt_ui_set_text(id: int, s: str) -> void {
ui_hasdyn[id] = 1 ui_hasdyn[id] = 1
} }
fn rt_ui_set_int(id: int, n: int) -> void { function rt_ui_set_int(id: int, n: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= ui_n { return } if id >= ui_n { return }
let base = id * 96 let base = id * 96
@ -266,17 +266,17 @@ fn rt_ui_set_int(id: int, n: int) -> void {
ui_hasdyn[id] = 1 ui_hasdyn[id] = 1
} }
fn rt_ui_focus(id: int) -> void { function rt_ui_focus(id: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= ui_n { return } if id >= ui_n { return }
ui_focus = id ui_focus = id
} }
fn rt_ui_focused() -> int { function rt_ui_focused() -> int {
return ui_focus return ui_focus
} }
fn rt_ui_visible(id: int, v: int) -> void { function rt_ui_visible(id: int, v: int) -> void {
if id < 0 { return } if id < 0 { return }
if id >= ui_n { return } if id >= ui_n { return }
ui_visible[id] = v ui_visible[id] = v

View file

@ -76,7 +76,7 @@ property Node {
line: int = 0 line: int = 0
} }
fn node(kind: int) -> Node { function node(kind: int) -> Node {
let n = new Node let n = new Node
n.kind = kind n.kind = kind
n.kids = new []Node n.kids = new []Node

View file

@ -4,26 +4,26 @@
property Buf { data: ptr = null, len: int = 0, cap: int = 0 } property Buf { data: ptr = null, len: int = 0, cap: int = 0 }
fn buf_new() -> Buf { function buf_new() -> Buf {
let b = new Buf let b = new Buf
b.cap = 256 b.cap = 256
b.data = bytes(b.cap) b.data = bytes(b.cap)
b.len = 0 b.len = 0
return b return b
} }
fn buf_ensure(b: Buf, extra: int) -> void { function buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return } if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 } while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 }
b.data = resize(b.data, b.cap) b.data = resize(b.data, b.cap)
} }
fn buf_putc(b: Buf, c: int) -> void { function buf_putc(b: Buf, c: int) -> void {
buf_ensure(b, 1) buf_ensure(b, 1)
b.data[b.len] = c b.data[b.len] = c
b.len = b.len + 1 b.len = b.len + 1
} }
fn buf_puts(b: Buf, s: ptr) -> void { function buf_puts(b: Buf, s: ptr) -> void {
var i = 0 var i = 0
while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 }
} }
fn buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) } function buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) }
fn buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data } function buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data }

View file

@ -5,7 +5,7 @@
var g_addr_ty: ptr # out-param: the type at the computed address var g_addr_ty: ptr # out-param: the type at the computed address
# address of `base.field` # address of `base.field`
fn emit_member_addr(e: Node) -> ptr { function emit_member_addr(e: Node) -> ptr {
let base = emit_expr(e.a) let base = emit_expr(e.a)
let s = layout_node(base.ty) let s = layout_node(base.ty)
if (s == null) { perr(`member access on non-aggregate {base.ty}`) } if (s == null) { perr(`member access on non-aggregate {base.ty}`) }
@ -19,7 +19,7 @@ fn emit_member_addr(e: Node) -> ptr {
} }
# address of `base[index]` (slices only in this subset) # address of `base[index]` (slices only in this subset)
fn emit_index_addr(e: Node) -> ptr { function emit_index_addr(e: Node) -> ptr {
let base = emit_expr(e.a) let base = emit_expr(e.a)
if not is_slice_ty(base.ty) { # a raw pointer: address of element i if not is_slice_ty(base.ty) { # a raw pointer: address of element i
let bi = emit_expr(e.b) # (evaluate index first — it may set g_addr_ty) let bi = emit_expr(e.b) # (evaluate index first — it may set g_addr_ty)

View file

@ -3,14 +3,14 @@
# origin at the top-left; circles are (x, y, r). Squared distances use i64 so a # origin at the top-left; circles are (x, y, r). Squared distances use i64 so a
# large coordinate can't overflow. Each returns a bool. # large coordinate can't overflow. Each returns a bool.
fn is_collide_ns(meth: ptr) -> bool { function is_collide_ns(meth: ptr) -> bool {
if (meth == "rects") or (meth == "point_rect") { return true } if (meth == "rects") or (meth == "point_rect") { return true }
if (meth == "circles") or (meth == "rect_circle") { return true } if (meth == "circles") or (meth == "rect_circle") { return true }
return false return false
} }
# dx*dx + dy*dy widened to i64 (no overflow for 32-bit deltas) # dx*dx + dy*dy widened to i64 (no overflow for 32-bit deltas)
fn coll_sq_sum(dx: ptr, dy: ptr) -> ptr { function coll_sq_sum(dx: ptr, dy: ptr) -> ptr {
let dx64 = emit_bind(`sext i32 {dx} to i64`) let dx64 = emit_bind(`sext i32 {dx} to i64`)
let dy64 = emit_bind(`sext i32 {dy} to i64`) let dy64 = emit_bind(`sext i32 {dy} to i64`)
let xx = emit_bind(`mul i64 {dx64}, {dx64}`) let xx = emit_bind(`mul i64 {dx64}, {dx64}`)
@ -19,14 +19,14 @@ fn coll_sq_sum(dx: ptr, dy: ptr) -> ptr {
} }
# max(lo, min(v, hi)) — clamp v into [lo, hi] # max(lo, min(v, hi)) — clamp v into [lo, hi]
fn coll_clamp(v: ptr, lo: ptr, hi: ptr) -> ptr { function coll_clamp(v: ptr, lo: ptr, hi: ptr) -> ptr {
let c1 = emit_bind(`icmp slt i32 {v}, {hi}`) let c1 = emit_bind(`icmp slt i32 {v}, {hi}`)
let t = emit_bind(`select i1 {c1}, i32 {v}, i32 {hi}`) let t = emit_bind(`select i1 {c1}, i32 {v}, i32 {hi}`)
let c2 = emit_bind(`icmp sgt i32 {lo}, {t}`) let c2 = emit_bind(`icmp sgt i32 {lo}, {t}`)
return emit_bind(`select i1 {c2}, i32 {lo}, i32 {t}`) return emit_bind(`select i1 {c2}, i32 {lo}, i32 {t}`)
} }
fn emit_collide_ns(meth: ptr, e: Node) -> Val { function emit_collide_ns(meth: ptr, e: Node) -> Val {
if (meth == "rects") { # AABB overlap of two rects if (meth == "rects") { # AABB overlap of two rects
let ax = emit_expr(e.kids[0]); let ay = emit_expr(e.kids[1]); let aw = emit_expr(e.kids[2]); let ah = emit_expr(e.kids[3]) let ax = emit_expr(e.kids[0]); let ay = emit_expr(e.kids[1]); let aw = emit_expr(e.kids[2]); let ah = emit_expr(e.kids[3])
let bx = emit_expr(e.kids[4]); let by = emit_expr(e.kids[5]); let bw = emit_expr(e.kids[6]); let bh = emit_expr(e.kids[7]) let bx = emit_expr(e.kids[4]); let by = emit_expr(e.kids[5]); let bw = emit_expr(e.kids[6]); let bh = emit_expr(e.kids[7])

View file

@ -9,7 +9,7 @@
# Edit the palette there and regenerate; do not hand-edit this file. # Edit the palette there and regenerate; do not hand-edit this file.
# ============================================================================ # ============================================================================
fn color_lookup(name: ptr) -> int { function color_lookup(name: ptr) -> int {
if (name == "White") { return 0xFFFFFF } if (name == "White") { return 0xFFFFFF }
if (name == "Snow") { return 0xFFFAFA } if (name == "Snow") { return 0xFFFAFA }
if (name == "Ivory") { return 0xFFFFF0 } if (name == "Ivory") { return 0xFFFFF0 }

View file

@ -3,20 +3,20 @@
# and blend them with plain integer/fixed math. rgb/rgba pack channels; lerp/ # and blend them with plain integer/fixed math. rgb/rgba pack channels; lerp/
# darken/lighten/with_alpha transform an existing color. # darken/lighten/with_alpha transform an existing color.
fn is_colorfn_ns(meth: ptr) -> bool { function is_colorfn_ns(meth: ptr) -> bool {
if (meth == "rgb") or (meth == "rgba") or (meth == "lerp") { return true } if (meth == "rgb") or (meth == "rgba") or (meth == "lerp") { return true }
if (meth == "darken") or (meth == "lighten") or (meth == "with_alpha") { return true } if (meth == "darken") or (meth == "lighten") or (meth == "with_alpha") { return true }
return false return false
} }
# (c >> shift) & 255 -> code of a channel value # (c >> shift) & 255 -> code of a channel value
fn color_ch(c: ptr, shift: ptr) -> ptr { function color_ch(c: ptr, shift: ptr) -> ptr {
let sh = emit_bind(`lshr i32 {c}, {shift}`) let sh = emit_bind(`lshr i32 {c}, {shift}`)
return emit_bind(`and i32 {sh}, 255`) return emit_bind(`and i32 {sh}, 255`)
} }
# (r << 16) | (g << 8) | b -> code of a packed color # (r << 16) | (g << 8) | b -> code of a packed color
fn color_pack(r: ptr, g: ptr, b: ptr) -> ptr { function color_pack(r: ptr, g: ptr, b: ptr) -> ptr {
let r16 = emit_bind(`shl i32 {r}, 16`) let r16 = emit_bind(`shl i32 {r}, 16`)
let g8 = emit_bind(`shl i32 {g}, 8`) let g8 = emit_bind(`shl i32 {g}, 8`)
let rg = emit_bind(`or i32 {r16}, {g8}`) let rg = emit_bind(`or i32 {r16}, {g8}`)
@ -24,14 +24,14 @@ fn color_pack(r: ptr, g: ptr, b: ptr) -> ptr {
} }
# ch0 + ((ch1 - ch0) * t >> 16), t a fixed 0..1 -> code of a blended channel # ch0 + ((ch1 - ch0) * t >> 16), t a fixed 0..1 -> code of a blended channel
fn color_lerp_ch(ch0: ptr, ch1: ptr, t: ptr) -> ptr { function color_lerp_ch(ch0: ptr, ch1: ptr, t: ptr) -> ptr {
let d = emit_bind(`sub i32 {ch1}, {ch0}`) let d = emit_bind(`sub i32 {ch1}, {ch0}`)
let dt = emit_bind(`mul i32 {d}, {t}`) let dt = emit_bind(`mul i32 {d}, {t}`)
let dsh = emit_bind(`ashr i32 {dt}, 16`) let dsh = emit_bind(`ashr i32 {dt}, 16`)
return emit_bind(`add i32 {ch0}, {dsh}`) return emit_bind(`add i32 {ch0}, {dsh}`)
} }
fn emit_colorfn_ns(meth: ptr, e: Node) -> Val { function emit_colorfn_ns(meth: ptr, e: Node) -> Val {
if (meth == "rgb") { # rgb(r, g, b) -> 0xRRGGBB if (meth == "rgb") { # rgb(r, g, b) -> 0xRRGGBB
let r = emit_expr(e.kids[0]); let g = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2]) let r = emit_expr(e.kids[0]); let g = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2])
return val(color_pack(r.code, g.code, b.code), "int") return val(color_pack(r.code, g.code, b.code), "int")

View file

@ -3,7 +3,7 @@
# structs and slices are references, so every non-scalar type lowers to `ptr`. # structs and slices are references, so every non-scalar type lowers to `ptr`.
property Val { code: ptr = null, ty: ptr = null } property Val { code: ptr = null, ty: ptr = null }
fn val(code: ptr, ty: ptr) -> Val { let v = new Val; v.code = code; v.ty = ty; return v } function val(code: ptr, ty: ptr) -> Val { let v = new Val; v.code = code; v.ty = ty; return v }
var head: Buf # module-level: types, globals, string constants var head: Buf # module-level: types, globals, string constants
var code: Buf # function bodies var code: Buf # function bodies
@ -47,30 +47,30 @@ var g_scene_count: int = 0 # parse-time id counter
var g_start_scene: int = 0 # id of the scene marked `start` (else the first) var g_start_scene: int = 0 # id of the scene marked `start` (else the first)
var g_cur_scene: Node = null # scene owning the handler being emitted, for `become` var g_cur_scene: Node = null # scene owning the handler being emitted, for `become`
fn find_scene(name: ptr) -> Node { function find_scene(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_scenes) { if (g_scenes[i].s == name) { return g_scenes[i] }; i = i + 1 } while i < len(g_scenes) { if (g_scenes[i].s == name) { return g_scenes[i] }; i = i + 1 }
return null return null
} }
fn emit(s: ptr) -> void { buf_puts(code, s) } function emit(s: ptr) -> void { buf_puts(code, s) }
fn emith(s: ptr) -> void { buf_puts(head, s) } function emith(s: ptr) -> void { buf_puts(head, s) }
# stack slots MUST live in the entry block (an alloca in a loop walks the stack # stack slots MUST live in the entry block (an alloca in a loop walks the stack
# off its end), so they go into a per-function buffer spliced in at entry. # off its end), so they go into a per-function buffer spliced in at entry.
fn emit_alloca(llt: ptr) -> ptr { function emit_alloca(llt: ptr) -> ptr {
let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1 let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1
buf_puts(falloc, " "); buf_puts(falloc, r); buf_puts(falloc, " = alloca "); buf_puts(falloc, llt); buf_puts(falloc, "\n") buf_puts(falloc, " "); buf_puts(falloc, r); buf_puts(falloc, " = alloca "); buf_puts(falloc, llt); buf_puts(falloc, "\n")
return r return r
} }
# "%t<n>" fresh register # "%t<n>" fresh register
fn nreg() -> ptr { let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1; return r } function nreg() -> ptr { let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1; return r }
fn lbl(pfx: ptr) -> ptr { let r = (pfx + itoa(ll_lbl)); ll_lbl = ll_lbl + 1; return r } function lbl(pfx: ptr) -> ptr { let r = (pfx + itoa(ll_lbl)); ll_lbl = ll_lbl + 1; return r }
# Ludic type -> LLVM type. int/bool are i32; everything else (ptr/str/struct/ # Ludic type -> LLVM type. int/bool are i32; everything else (ptr/str/struct/
# slice) is a pointer; void is void. # slice) is a pointer; void is void.
fn llty(t: ptr) -> ptr { function llty(t: ptr) -> ptr {
if (t == "int") or (t == "bool") or (t == "fixed") or (t == "entity") { return "i32" } # entity = an i32 handle (self()) if (t == "int") or (t == "bool") or (t == "fixed") or (t == "entity") { return "i32" } # entity = an i32 handle (self())
if (t == "long") { return "i64" } # a 64-bit signed integer if (t == "long") { return "i64" } # a 64-bit signed integer
if (t == "byte") { return "i8" } # a single byte (p[i] on a raw ptr) if (t == "byte") { return "i8" } # a single byte (p[i] on a raw ptr)
@ -79,37 +79,37 @@ fn llty(t: ptr) -> ptr {
return "ptr" return "ptr"
} }
fn is_slice_ty(t: ptr) -> bool { return t[0] == 91 and t[1] == 93 } # "[]" function is_slice_ty(t: ptr) -> bool { return t[0] == 91 and t[1] == 93 } # "[]"
fn slice_elem(t: ptr) -> ptr { return t[2..len(t)] } function slice_elem(t: ptr) -> ptr { return t[2..len(t)] }
fn find_arch(name: ptr) -> Node { function find_arch(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i = i + 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i = i + 1 }
return null return null
} }
fn find_comp(name: ptr) -> Node { function find_comp(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i = i + 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i = i + 1 }
return null return null
} }
# every record is a `property` with a %Cmp_ layout of named fields — whether it # every record is a `property` with a %Cmp_ layout of named fields — whether it
# is stored per-entity by the ECS or heap-allocated by `new` is a matter of use. # is stored per-entity by the ECS or heap-allocated by `new` is a matter of use.
fn layout_node(name: ptr) -> Node { return find_comp(name) } function layout_node(name: ptr) -> Node { return find_comp(name) }
fn layout_ty(name: ptr) -> ptr { return (("%Cmp_") + name) } function layout_ty(name: ptr) -> ptr { return (("%Cmp_") + name) }
fn field_index(s: Node, fname: ptr) -> int { function field_index(s: Node, fname: ptr) -> int {
var i = 0 var i = 0
while i < len(s.kids) { if (s.kids[i].s == fname) { return i }; i = i + 1 } while i < len(s.kids) { if (s.kids[i].s == fname) { return i }; i = i + 1 }
return 0 - 1 return 0 - 1
} }
fn field_type(s: Node, fname: ptr) -> ptr { function field_type(s: Node, fname: ptr) -> ptr {
var i = 0 var i = 0
while i < len(s.kids) { if (s.kids[i].s == fname) { return s.kids[i].ty }; i = i + 1 } while i < len(s.kids) { if (s.kids[i].s == fname) { return s.kids[i].ty }; i = i + 1 }
return "int" return "int"
} }
# find a global var/const by name # find a global var/const by name
fn find_global(name: ptr) -> Node { function find_global(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]
@ -122,7 +122,7 @@ fn find_global(name: ptr) -> Node {
# `Enum.Variant` -> the variant's ordinal (its index), or -1 if `ename` names no # `Enum.Variant` -> the variant's ordinal (its index), or -1 if `ename` names no
# enum with that variant. Enum names live in `prog` like any other declaration. # enum with that variant. Enum names live in `prog` like any other declaration.
fn enum_ordinal(ename: ptr, vname: ptr) -> int { function enum_ordinal(ename: ptr, vname: ptr) -> int {
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]
@ -141,18 +141,18 @@ fn enum_ordinal(ename: ptr, vname: ptr) -> int {
} }
return 0 - 1 return 0 - 1
} }
fn find_fn(name: ptr) -> Node { function find_fn(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i = i + 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i = i + 1 }
return null return null
} }
# `extern fn name(params) -> T = "sym"` binds a Ludic name to a link symbol. A # `extern function name(params) -> T = "sym"` binds a Ludic name to a link symbol. A
# call to `name` lowers to a direct `@<sym>` call (no @fn_ prefix — the string is # call to `name` lowers to a direct `@<sym>` call (no @fn_ prefix — the string is
# the exact linked symbol), and emit_extern_decls emits a matching `declare`. This # the exact linked symbol), and emit_extern_decls emits a matching `declare`. This
# is the transport seam (net_send/net_poll), the windowing/socket FFI, and any # is the transport seam (net_send/net_poll), the windowing/socket FFI, and any
# C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names. # C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names.
fn find_extern(name: ptr) -> Node { function find_extern(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i = i + 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i = i + 1 }
return null return null
@ -163,11 +163,11 @@ fn find_extern(name: ptr) -> Node {
# expression with its bare names read as fields of `x`. Populated at parse time. # expression with its bare names read as fields of `x`. Populated at parse time.
var g_computed: []Node # each: s = "Prop.field", ty = result type, a = expr var g_computed: []Node # each: s = "Prop.field", ty = result type, a = expr
fn register_computed(prop: ptr, field: ptr, ty: ptr, e: Node) -> void { function register_computed(prop: ptr, field: ptr, ty: ptr, e: Node) -> void {
let cf = node(N_FIELD); cf.s = `{prop}.{field}`; cf.ty = ty; cf.a = e let cf = node(N_FIELD); cf.s = `{prop}.{field}`; cf.ty = ty; cf.a = e
push(g_computed, cf) push(g_computed, cf)
} }
fn computed_expr(prop: ptr, field: ptr) -> Node { function computed_expr(prop: ptr, field: ptr) -> Node {
if (prop == null) { return null } if (prop == null) { return null }
let key = `{prop}.{field}` let key = `{prop}.{field}`
var i = 0 var i = 0
@ -175,7 +175,7 @@ fn computed_expr(prop: ptr, field: ptr) -> Node {
return null return null
} }
# best-effort static type of an expression (for computed-field lookup; emits nothing) # best-effort static type of an expression (for computed-field lookup; emits nothing)
fn static_type(e: Node) -> ptr { function static_type(e: Node) -> ptr {
if e.kind == E_ID { let li = loc_find(e.s); if li >= 0 { return loc_ty[li] } } if e.kind == E_ID { let li = loc_find(e.s); if li >= 0 { return loc_ty[li] } }
return null return null
} }
@ -185,10 +185,10 @@ fn static_type(e: Node) -> ptr {
# constructor). Spawn statically knows the model, so no runtime dispatch is needed. # constructor). Spawn statically knows the model, so no runtime dispatch is needed.
var g_onspawn: []Node # each: s = Model name, a = hook body block var g_onspawn: []Node # each: s = Model name, a = hook body block
fn register_onspawn(model: ptr, body: Node) -> void { function register_onspawn(model: ptr, body: Node) -> void {
let n = node(N_BLOCK); n.s = model; n.a = body; push(g_onspawn, n) let n = node(N_BLOCK); n.s = model; n.a = body; push(g_onspawn, n)
} }
fn onspawn_body(model: ptr) -> Node { function onspawn_body(model: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_onspawn) { if (g_onspawn[i].s == model) { return g_onspawn[i].a }; i = i + 1 } while i < len(g_onspawn) { if (g_onspawn[i].s == model) { return g_onspawn[i].a }; i = i + 1 }
return null return null
@ -203,18 +203,18 @@ var g_onattach: []Node # each: s = Property name, a = hook body block
# LC1: `.ty` carries the optional `reason:` binding name (null if the hook took # LC1: `.ty` carries the optional `reason:` binding name (null if the hook took
# no reason). The despawn hook function gains an `i32 %reason` parameter and each # no reason). The despawn hook function gains an `i32 %reason` parameter and each
# teardown site passes a constant EndReason (see emit_despawn_hooks / emit_despawn). # teardown site passes a constant EndReason (see emit_despawn_hooks / emit_despawn).
fn register_ondespawn(model: ptr, body: Node, reason: ptr) -> void { function register_ondespawn(model: ptr, body: Node, reason: ptr) -> void {
let n = node(N_BLOCK); n.s = model; n.a = body; n.ty = reason; push(g_ondespawn, n) let n = node(N_BLOCK); n.s = model; n.a = body; n.ty = reason; push(g_ondespawn, n)
} }
fn ondespawn_body(model: ptr) -> Node { function ondespawn_body(model: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_ondespawn) { if (g_ondespawn[i].s == model) { return g_ondespawn[i].a }; i = i + 1 } while i < len(g_ondespawn) { if (g_ondespawn[i].s == model) { return g_ondespawn[i].a }; i = i + 1 }
return null return null
} }
fn register_onattach(prop: ptr, body: Node) -> void { function register_onattach(prop: ptr, body: Node) -> void {
let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onattach, n) let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onattach, n)
} }
fn onattach_body(prop: ptr) -> Node { function onattach_body(prop: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_onattach) { if (g_onattach[i].s == prop) { return g_onattach[i].a }; i = i + 1 } while i < len(g_onattach) { if (g_onattach[i].s == prop) { return g_onattach[i].a }; i = i + 1 }
return null return null
@ -224,10 +224,10 @@ fn onattach_body(prop: ptr) -> Node {
# is removed from a live entity (`detach P on e`), with the property bound by name # is removed from a live entity (`detach P on e`), with the property bound by name
# so the body can read its outgoing value before it is cleared. # so the body can read its outgoing value before it is cleared.
var g_ondetach: []Node # each: s = Property name, a = hook body block var g_ondetach: []Node # each: s = Property name, a = hook body block
fn register_ondetach(prop: ptr, body: Node) -> void { function register_ondetach(prop: ptr, body: Node) -> void {
let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondetach, n) let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondetach, n)
} }
fn ondetach_body(prop: ptr) -> Node { function ondetach_body(prop: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_ondetach) { if (g_ondetach[i].s == prop) { return g_ondetach[i].a }; i = i + 1 } while i < len(g_ondetach) { if (g_ondetach[i].s == prop) { return g_ondetach[i].a }; i = i + 1 }
return null return null
@ -237,14 +237,14 @@ fn ondetach_body(prop: ptr) -> Node {
# entity, with the property bound by name. # entity, with the property bound by name.
var g_onenable: []Node var g_onenable: []Node
var g_ondisable: []Node var g_ondisable: []Node
fn register_onenable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onenable, n) } function register_onenable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onenable, n) }
fn register_ondisable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondisable, n) } function register_ondisable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondisable, n) }
fn onenable_body(prop: ptr) -> Node { function onenable_body(prop: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_onenable) { if (g_onenable[i].s == prop) { return g_onenable[i].a }; i = i + 1 } while i < len(g_onenable) { if (g_onenable[i].s == prop) { return g_onenable[i].a }; i = i + 1 }
return null return null
} }
fn ondisable_body(prop: ptr) -> Node { function ondisable_body(prop: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_ondisable) { if (g_ondisable[i].s == prop) { return g_ondisable[i].a }; i = i + 1 } while i < len(g_ondisable) { if (g_ondisable[i].s == prop) { return g_ondisable[i].a }; i = i + 1 }
return null return null
@ -259,13 +259,13 @@ var g_events: []Node # each: an N_EVENT node (s = name, kids = payloa
var g_onlisten: []Node # each: N_BLOCK, s = event name, a = listener body block var g_onlisten: []Node # each: N_BLOCK, s = event name, a = listener body block
var g_cancel_addr: ptr = null # EV3: address of the current cancellable dispatch's flag (null outside one) var g_cancel_addr: ptr = null # EV3: address of the current cancellable dispatch's flag (null outside one)
fn register_event(n: Node) -> void { push(g_events, n) } function register_event(n: Node) -> void { push(g_events, n) }
fn find_event(name: ptr) -> Node { function find_event(name: ptr) -> Node {
var i = 0 var i = 0
while i < len(g_events) { if (g_events[i].s == name) { return g_events[i] }; i = i + 1 } while i < len(g_events) { if (g_events[i].s == name) { return g_events[i] }; i = i + 1 }
return null return null
} }
fn register_onlisten(evt: ptr, body: Node) -> void { function register_onlisten(evt: ptr, body: Node) -> void {
let n = node(N_BLOCK); n.s = evt; n.a = body; push(g_onlisten, n) let n = node(N_BLOCK); n.s = evt; n.a = body; push(g_onlisten, n)
} }
@ -274,7 +274,7 @@ fn register_onlisten(evt: ptr, body: Node) -> void {
# presence in g_events is what makes each lifecycle fire site also `emit` it, so # presence in g_events is what makes each lifecycle fire site also `emit` it, so
# `find_event(name) != null` doubles as the "is this hook public?" test. Names are # `find_event(name) != null` doubles as the "is this hook public?" test. Names are
# the stable ABI contract: `model_<M>_spawn`, `model_<M>_despawn`, etc. # the stable ABI contract: `model_<M>_spawn`, `model_<M>_despawn`, etc.
fn ensure_event(name: ptr, with_reason: bool) -> void { function ensure_event(name: ptr, with_reason: bool) -> void {
if (find_event(name) != null) { return } if (find_event(name) != null) { return }
let n = node(N_EVENT); n.s = name let n = node(N_EVENT); n.s = name
let ent = node(N_FIELD); ent.s = "entity"; ent.ty = "int"; push(n.kids, ent) let ent = node(N_FIELD); ent.s = "entity"; ent.ty = "int"; push(n.kids, ent)
@ -282,7 +282,7 @@ fn ensure_event(name: ptr, with_reason: bool) -> void {
register_event(n) register_event(n)
} }
# a promoted scene/program event has no per-entity payload # a promoted scene/program event has no per-entity payload
fn ensure_event_empty(name: ptr) -> void { function ensure_event_empty(name: ptr) -> void {
if (find_event(name) != null) { return } if (find_event(name) != null) { return }
let n = node(N_EVENT); n.s = name; register_event(n) let n = node(N_EVENT); n.s = name; register_event(n)
} }
@ -292,32 +292,32 @@ fn ensure_event_empty(name: ptr) -> void {
# handlers gate on it. Only managed layers pay for this, so a scene program that # handlers gate on it. Only managed layers pay for this, so a scene program that
# never toggles a layer is byte-identical. # never toggles a layer is byte-identical.
var g_toggled_layers: []ptr var g_toggled_layers: []ptr
fn note_toggled_layer(name: ptr) -> void { function note_toggled_layer(name: ptr) -> void {
var i = 0 var i = 0
while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return }; i = i + 1 } while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return }; i = i + 1 }
push(g_toggled_layers, name) push(g_toggled_layers, name)
} }
fn is_toggled_layer(name: ptr) -> bool { function is_toggled_layer(name: ptr) -> bool {
var i = 0 var i = 0
while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return true }; i = i + 1 } while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return true }; i = i + 1 }
return false return false
} }
# is `name` a model (archetype)? — chooses model-vs-handler for a bare enable/disable # is `name` a model (archetype)? — chooses model-vs-handler for a bare enable/disable
fn is_model(name: ptr) -> bool { return find_arch_id(name) > 0 } function is_model(name: ptr) -> bool { return find_arch_id(name) > 0 }
# local variable environment # local variable environment
fn loc_reset() -> void { nloc = 0 } function loc_reset() -> void { nloc = 0 }
# push a local. Defaults to mutable (params, loop and query bindings are all # push a local. Defaults to mutable (params, loop and query bindings are all
# reassignable/rebindable); a `let` binding marks its slot immutable afterward # reassignable/rebindable); a `let` binding marks its slot immutable afterward
# via loc_set_mut, so a later `name = …` can be rejected. # via loc_set_mut, so a later `name = …` can be rejected.
fn loc_push(name: ptr, r: ptr, ty: ptr) -> void { function loc_push(name: ptr, r: ptr, ty: ptr) -> void {
if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty; loc_mut[nloc] = 1 } if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty; loc_mut[nloc] = 1 }
else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty); push(loc_mut, 1) } else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty); push(loc_mut, 1) }
nloc = nloc + 1 nloc = nloc + 1
} }
fn loc_set_mut(m: int) -> void { if nloc > 0 { loc_mut[nloc - 1] = m } } function loc_set_mut(m: int) -> void { if nloc > 0 { loc_mut[nloc - 1] = m } }
fn loc_find(name: ptr) -> int { function loc_find(name: ptr) -> int {
var i = nloc - 1 var i = nloc - 1
while i >= 0 { if (loc_name[i] == name) { return i }; i = i - 1 } while i >= 0 { if (loc_name[i] == name) { return i }; i = i - 1 }
return 0 - 1 return 0 - 1

View file

@ -2,7 +2,7 @@
# body is built into a scratch buffer so entry-block allocas can be spliced in # body is built into a scratch buffer so entry-block allocas can be spliced in
# ahead of it (see emit_alloca). Returns go through a %retval slot + `ret` label. # ahead of it (see emit_alloca). Returns go through a %retval slot + `ret` label.
fn emit_params_sig(d: Node) -> void { function emit_params_sig(d: Node) -> void {
var i = 0 var i = 0
while i < len(d.kids) { while i < len(d.kids) {
if i > 0 { emit(", ") } if i > 0 { emit(", ") }
@ -11,7 +11,7 @@ fn emit_params_sig(d: Node) -> void {
} }
} }
fn emit_fn(d: Node) -> void { function emit_fn(d: Node) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0 ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
ret_ty = d.ty ret_ty = d.ty
let fbody = buf_new() let fbody = buf_new()
@ -41,7 +41,7 @@ fn emit_fn(d: Node) -> void {
emit("}\n\n") emit("}\n\n")
} }
fn emit_main(d: Node) -> void { function emit_main(d: Node) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0 ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
ret_ty = "int" ret_ty = "int"
let fbody = buf_new() let fbody = buf_new()
@ -64,7 +64,7 @@ fn emit_main(d: Node) -> void {
emit("}\n") emit("}\n")
} }
fn emit_program() -> void { function emit_program() -> void {
head = buf_new() head = buf_new()
code = buf_new() code = buf_new()
g_uses_str = false g_uses_str = false
@ -109,7 +109,7 @@ fn emit_program() -> void {
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell # Flush the emitted IR. With a null path it goes to stdout (the pipe the shell
# drivers read); with a path it is written to that file so ludicc can hand it to # drivers read); with a path it is written to that file so ludicc can hand it to
# clang itself. # clang itself.
fn ir_flush(path: ptr) -> bool { function ir_flush(path: ptr) -> bool {
let h = buf_str(head) let h = buf_str(head)
let c = buf_str(code) let c = buf_str(code)
if (path == null) { # raw IR to stdout (no trailing newline) if (path == null) { # raw IR to stdout (no trailing newline)

View file

@ -2,19 +2,19 @@
# t in 0.0..1.0, returning an eased fixed. All pure Q16.16, deterministic. The # t in 0.0..1.0, returning an eased fixed. All pure Q16.16, deterministic. The
# "juice" layer that makes motion feel good (Robert Penner's easings). # "juice" layer that makes motion feel good (Robert Penner's easings).
fn is_ease_ns(meth: ptr) -> bool { function is_ease_ns(meth: ptr) -> bool {
if (meth == "in") or (meth == "out") or (meth == "in_out") { return true } if (meth == "in") or (meth == "out") or (meth == "in_out") { return true }
if (meth == "back") or (meth == "bounce") or (meth == "elastic") { return true } if (meth == "back") or (meth == "bounce") or (meth == "elastic") { return true }
return false return false
} }
# n1 * u * u (u a fixed code) -> code of a fixed i32 # n1 * u * u (u a fixed code) -> code of a fixed i32
fn ease_bounce_seg(u: ptr) -> ptr { function ease_bounce_seg(u: ptr) -> ptr {
let uu = fx_mul_code(u, u) let uu = fx_mul_code(u, u)
return fx_mul_code(uu, "495616") # 7.5625 * u*u return fx_mul_code(uu, "495616") # 7.5625 * u*u
} }
fn emit_ease_ns(meth: ptr, e: Node) -> Val { function emit_ease_ns(meth: ptr, e: Node) -> Val {
let t = emit_expr(e.kids[0]) let t = emit_expr(e.kids[0])
if (meth == "in") { # ease-in quad: t*t if (meth == "in") { # ease-in quad: t*t
return val(fx_mul_code(t.code, t.code), "fixed") return val(fx_mul_code(t.code, t.code), "fixed")

View file

@ -5,12 +5,12 @@
const MAX_ENT: int = 1024 const MAX_ENT: int = 1024
fn has_systems() -> bool { function has_systems() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_SYS { return true }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_SYS { return true }; i = i + 1 }
return false return false
} }
fn has_models() -> bool { function has_models() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_ARCH { return true }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_ARCH { return true }; i = i + 1 }
return false return false
@ -18,7 +18,7 @@ fn has_models() -> bool {
# N5: does the program have an `entry` block? A game with both handlers and an # N5: does the program have an `entry` block? A game with both handlers and an
# `entry` drives its own loop (calling tick_fixed/tick_render), instead of the # `entry` drives its own loop (calling tick_fixed/tick_render), instead of the
# compiler's auto frame loop. A game with handlers and no entry uses the auto-loop. # compiler's auto frame loop. A game with handlers and no entry uses the auto-loop.
fn has_entry() -> bool { function has_entry() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_MAIN { return true }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_MAIN { return true }; i = i + 1 }
return false return false
@ -28,9 +28,9 @@ fn has_entry() -> bool {
# `struct`). A program uses the ECS when it has a handler or a model; a tool that # `struct`). A program uses the ECS when it has a handler or a model; a tool that
# only declares record types and functions does not, and gets no entity storage, # only declares record types and functions does not, and gets no entity storage,
# allocator, snapshot or runtime splice. # allocator, snapshot or runtime splice.
fn has_ecs() -> bool { return has_systems() or has_models() } function has_ecs() -> bool { return has_systems() or has_models() }
fn emit_ecs_storage() -> void { function emit_ecs_storage() -> void {
emith("@L_running = internal global i32 1\n") emith("@L_running = internal global i32 1\n")
emith("@L_key = internal global i32 0\n") emith("@L_key = internal global i32 0\n")
emith("@L_frame = internal global i32 0\n") emith("@L_frame = internal global i32 0\n")
@ -71,7 +71,7 @@ fn emit_ecs_storage() -> void {
} }
# L_reset(e): clear every has-flag and the archetype kind for entity e # L_reset(e): clear every has-flag and the archetype kind for entity e
fn emit_ecs_allocator() -> void { function emit_ecs_allocator() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define void @L_reset(i32 %e) {\nentry:\n") emit("define void @L_reset(i32 %e) {\nentry:\n")
var i = 0 var i = 0

View file

@ -1,13 +1,13 @@
# emit_expr.ludic — lower an expression to IR, returning its register and type. # emit_expr.ludic — lower an expression to IR, returning its register and type.
fn emit_load_at(addr: ptr, ty: ptr) -> Val { function emit_load_at(addr: ptr, ty: ptr) -> Val {
let r = emit_bind(`load {llty(ty)}, ptr {addr}`) let r = emit_bind(`load {llty(ty)}, ptr {addr}`)
return val(r, ty) return val(r, ty)
} }
# short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether # short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether
# to overwrite with (right!=0). # to overwrite with (right!=0).
fn emit_logic(e: Node) -> Val { function emit_logic(e: Node) -> Val {
let slot = emit_alloca("i32") let slot = emit_alloca("i32")
let la = emit_expr(e.a) let la = emit_expr(e.a)
let lc = emit_bind(`icmp ne i32 {la.code}, 0`) let lc = emit_bind(`icmp ne i32 {la.code}, 0`)
@ -26,7 +26,7 @@ fn emit_logic(e: Node) -> Val {
return val(emit_bind(`load i32, ptr {slot}`), "bool") return val(emit_bind(`load i32, ptr {slot}`), "bool")
} }
fn cmp_code(op: ptr) -> ptr { function cmp_code(op: ptr) -> ptr {
if (op == ("<")) { return "slt" } if (op == ("<")) { return "slt" }
if (op == ("<=")) { return "sle" } if (op == ("<=")) { return "sle" }
if (op == (">")) { return "sgt" } if (op == (">")) { return "sgt" }
@ -34,10 +34,10 @@ fn cmp_code(op: ptr) -> ptr {
if (op == ("==")) { return "eq" } if (op == ("==")) { return "eq" }
return "ne" return "ne"
} }
fn is_cmp(op: ptr) -> bool { function is_cmp(op: ptr) -> bool {
return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!=")) return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!="))
} }
fn arith_code(op: ptr) -> ptr { function arith_code(op: ptr) -> ptr {
if (op == ("+")) { return "add" } if (op == ("+")) { return "add" }
if (op == ("-")) { return "sub" } if (op == ("-")) { return "sub" }
if (op == ("*")) { return "mul" } if (op == ("*")) { return "mul" }
@ -51,7 +51,7 @@ fn arith_code(op: ptr) -> ptr {
} }
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through) # widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
fn to_fixed(v: Val) -> ptr { function to_fixed(v: Val) -> ptr {
if (v.ty == "fixed") { return v.code } if (v.ty == "fixed") { return v.code }
return emit_bind(`shl i32 {v.code}, 16`) return emit_bind(`shl i32 {v.code}, 16`)
} }
@ -59,7 +59,7 @@ fn to_fixed(v: Val) -> ptr {
# coerce a value's code to the LLVM type of `target`, for the only cross-width # coerce a value's code to the LLVM type of `target`, for the only cross-width
# pair the language has: int (i32) <-> long (i64). int widens with sext, long # pair the language has: int (i32) <-> long (i64). int widens with sext, long
# narrows with trunc; everything else (same width, or ptr) passes through. # narrows with trunc; everything else (same width, or ptr) passes through.
fn coerce_code(v: Val, target: ptr) -> ptr { function coerce_code(v: Val, target: ptr) -> ptr {
let lt = llty(target) let lt = llty(target)
let vt = llty(v.ty) let vt = llty(v.ty)
if (lt == vt) { return v.code } if (lt == vt) { return v.code }
@ -69,14 +69,14 @@ fn coerce_code(v: Val, target: ptr) -> ptr {
} }
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed # widen an int value to i64 (a long passes through) — the long analogue of to_fixed
fn to_long(v: Val) -> ptr { function to_long(v: Val) -> ptr {
if (llty(v.ty) == "i64") { return v.code } if (llty(v.ty) == "i64") { return v.code }
return emit_bind(`sext i32 {v.code} to i64`) return emit_bind(`sext i32 {v.code} to i64`)
} }
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content. # string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
# Both call the @fn_str_* prelude (emitted once per program that uses them). # Both call the @fn_str_* prelude (emitted once per program that uses them).
fn emit_str_op(op: ptr, a: Val, b: Val) -> Val { function emit_str_op(op: ptr, a: Val, b: Val) -> Val {
g_uses_str = true g_uses_str = true
if (op == ("+")) { if (op == ("+")) {
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "str") return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "str")
@ -89,7 +89,7 @@ fn emit_str_op(op: ptr, a: Val, b: Val) -> Val {
return val(r, "bool") return val(r, "bool")
} }
fn emit_bin(e: Node) -> Val { function emit_bin(e: Node) -> Val {
if (e.s == "and") or (e.s == "or") { return emit_logic(e) } if (e.s == "and") or (e.s == "or") { return emit_logic(e) }
let a = emit_expr(e.a) let a = emit_expr(e.a)
let b = emit_expr(e.b) let b = emit_expr(e.b)
@ -147,12 +147,12 @@ fn emit_bin(e: Node) -> Val {
# is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in # is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in
# the order the callee declares its parameters, so the rest of emit_call is # the order the callee declares its parameters, so the rest of emit_call is
# oblivious to whether the caller used names. # oblivious to whether the caller used names.
fn args_are_named(e: Node) -> bool { function args_are_named(e: Node) -> bool {
var i = 0 var i = 0
while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i = i + 1 } while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i = i + 1 }
return false return false
} }
fn reorder_named(e: Node, labels: []ptr) -> void { function reorder_named(e: Node, labels: []ptr) -> void {
if not args_are_named(e) { return } if not args_are_named(e) { return }
var i = 0 var i = 0
while i < len(e.kids) { while i < len(e.kids) {
@ -173,14 +173,14 @@ fn reorder_named(e: Node, labels: []ptr) -> void {
e.kids = out e.kids = out
} }
# The parameter labels of a resolved fn/extern, in declaration order. # The parameter labels of a resolved fn/extern, in declaration order.
fn param_labels(fn: Node) -> []ptr { function param_labels(fn: Node) -> []ptr {
let out = new []ptr let out = new []ptr
var i = 0 var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i = i + 1 } while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i = i + 1 }
return out return out
} }
fn param_types(fn: Node) -> []ptr { function param_types(fn: Node) -> []ptr {
let out = new []ptr let out = new []ptr
var i = 0 var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 } while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 }
@ -192,7 +192,7 @@ fn param_types(fn: Node) -> []ptr {
# runtime builtin plus the parameter labels callers may use as named arguments; # runtime builtin plus the parameter labels callers may use as named arguments;
# after reordering we rewrite the callee to that bare name and fall back into the # after reordering we rewrite the callee to that bare name and fall back into the
# ordinary builtin path (which resolves it to its rt_ function). # ordinary builtin path (which resolves it to its rt_ function).
fn emit_ns_call(ns: ptr, meth: ptr, e: Node) -> Val { function emit_ns_call(ns: ptr, meth: ptr, e: Node) -> Val {
# Math.* is computed inline (deterministic fixed-point), not routed through a # Math.* is computed inline (deterministic fixed-point), not routed through a
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace. # bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
if (ns == "Math") { if (ns == "Math") {
@ -325,7 +325,7 @@ fn emit_ns_call(ns: ptr, meth: ptr, e: Node) -> Val {
return emit_call(e) return emit_call(e)
} }
fn emit_call(e: Node) -> Val { function emit_call(e: Node) -> Val {
# `Subject.action(...)` — a namespaced builtin (Screen/Random/Input). # `Subject.action(...)` — a namespaced builtin (Screen/Random/Input).
if e.a.kind == E_MEMBER { if e.a.kind == E_MEMBER {
if e.a.a.kind == E_ID { return emit_ns_call(e.a.a.s, e.a.s, e) } if e.a.a.kind == E_ID { return emit_ns_call(e.a.a.s, e.a.s, e) }
@ -525,7 +525,7 @@ fn emit_call(e: Node) -> Val {
return val(rreg, fn2.ty) return val(rreg, fn2.ty)
} }
fn emit_expr(e: Node) -> Val { function emit_expr(e: Node) -> Val {
if (e == null) { return val("0", "int") } if (e == null) { return val("0", "int") }
if e.kind == E_INT { return val(itoa(e.ival), "int") } if e.kind == E_INT { return val(itoa(e.ival), "int") }
if e.kind == E_FLOAT { return val(itoa(e.ival), "fixed") } if e.kind == E_FLOAT { return val(itoa(e.ival), "fixed") }

View file

@ -3,7 +3,7 @@
# order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are # order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are
# called only when the runtime defines them. # called only when the runtime defines them.
fn emit_system_fn(sys: Node) -> void { function emit_system_fn(sys: Node) -> void {
g_cur_scene = sys.c # scene owning this handler (null if global) — for `become` g_cur_scene = sys.c # scene owning this handler (null if global) — for `become`
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0 ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void" ret_ty = "void"
@ -22,7 +22,7 @@ fn emit_system_fn(sys: Node) -> void {
} }
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled). # one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).
fn emit_call_one(d: Node) -> void { function emit_call_one(d: Node) -> void {
let he = emit_bind(`load i32, ptr @HE_{d.s}`) let he = emit_bind(`load i32, ptr @HE_{d.s}`)
var hc = emit_bind(`icmp ne i32 {he}, 0`) var hc = emit_bind(`icmp ne i32 {he}, 0`)
# a handler in a toggled layer also gates on its layer's @LE_ flag (byte-identical # a handler in a toggled layer also gates on its layer's @LE_ flag (byte-identical
@ -50,7 +50,7 @@ fn emit_call_one(d: Node) -> void {
# declaration (draw) order. The active scene is snapshotted once per phase, so a # declaration (draw) order. The active scene is snapshotted once per phase, so a
# `become` mid-phase takes effect at the next phase boundary — exactly one scene # `become` mid-phase takes effect at the next phase boundary — exactly one scene
# is active within any single phase. # is active within any single phase.
fn emit_calls_for_phase(phase: ptr) -> void { function emit_calls_for_phase(phase: ptr) -> void {
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]
@ -81,7 +81,7 @@ fn emit_calls_for_phase(phase: ptr) -> void {
# on enter / on exit compile to void functions @scene_enter_<Name> / # on enter / on exit compile to void functions @scene_enter_<Name> /
# @scene_exit_<Name>, called at the transition point (and enter at boot for the # @scene_exit_<Name>, called at the transition point (and enter at boot for the
# start scene). Emitted for every scene, empty body when the hook is absent. # start scene). Emitted for every scene, empty body when the hook is absent.
fn emit_scene_fn(name: ptr, kind: ptr, body: Node) -> void { function emit_scene_fn(name: ptr, kind: ptr, body: Node) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0 ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void" ret_ty = "void"
let fbody = buf_new() let fbody = buf_new()
@ -101,7 +101,7 @@ fn emit_scene_fn(name: ptr, kind: ptr, body: Node) -> void {
emit("}\n\n") emit("}\n\n")
} }
fn emit_scene_hooks() -> void { function emit_scene_hooks() -> void {
var i = 0 var i = 0
while i < len(g_scenes) { while i < len(g_scenes) {
let sc = g_scenes[i] let sc = g_scenes[i]
@ -116,7 +116,7 @@ fn emit_scene_hooks() -> void {
# functions that bind the model's properties and run the body — dispatched by # functions that bind the model's properties and run the body — dispatched by
# kind at `despawn`. LC1: the `i32 %reason` parameter carries an EndReason; when # kind at `despawn`. LC1: the `i32 %reason` parameter carries an EndReason; when
# the hook declared `reason: r`, `r` is bound as an int local reading it. # the hook declared `reason: r`, `r` is bound as an int local reading it.
fn emit_despawn_hooks() -> void { function emit_despawn_hooks() -> void {
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
let hk = g_ondespawn[i] let hk = g_ondespawn[i]
@ -152,7 +152,7 @@ fn emit_despawn_hooks() -> void {
# the same per-model dispatch as `despawn`, but without freeing (the process is # the same per-model dispatch as `despawn`, but without freeing (the process is
# ending). Emitted only when the program has @OnDespawn hooks, so despawn-free # ending). Emitted only when the program has @OnDespawn hooks, so despawn-free
# programs are byte-for-byte unchanged. # programs are byte-for-byte unchanged.
fn emit_despawn_all_fn() -> void { function emit_despawn_all_fn() -> void {
if len(g_ondespawn) == 0 { return } if len(g_ondespawn) == 0 { return }
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define void @L_despawn_all(i32 %reason) {\nentry:\n br label %loop\n") emit("define void @L_despawn_all(i32 %reason) {\nentry:\n br label %loop\n")
@ -204,7 +204,7 @@ const EV_CAP: int = 16
# returns immediately (a cancellable event returns "not cancelled"). # returns immediately (a cancellable event returns "not cancelled").
const EV_DEPTH_CAP: int = 32 const EV_DEPTH_CAP: int = 32
fn emit_event_fns() -> void { function emit_event_fns() -> void {
emith("@ev_depth = global i32 0\n") # EV6 re-entrancy counter emith("@ev_depth = global i32 0\n") # EV6 re-entrancy counter
var e = 0 var e = 0
while e < len(g_events) { while e < len(g_events) {
@ -400,7 +400,7 @@ fn emit_event_fns() -> void {
# #
# First cut: integer component fields (the common case — hp, x, amount). Property # First cut: integer component fields (the common case — hp, x, amount). Property
# ids are assignment order in the source; field ids are declaration order. # ids are assignment order in the source; field ids are declaration order.
fn emit_world_table() -> void { function emit_world_table() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emith("declare i32 @strcmp(ptr, ptr)\n") emith("declare i32 @strcmp(ptr, ptr)\n")
@ -687,7 +687,7 @@ fn emit_world_table() -> void {
# runs are also exposed as callables, so a game that owns its `entry` loop can # runs are also exposed as callables, so a game that owns its `entry` loop can
# drive the simulation itself (for prediction/rollback, replay, headless tests, or # drive the simulation itself (for prediction/rollback, replay, headless tests, or
# AI). tick_fixed() runs the sim phases; tick_render() runs Render. # AI). tick_fixed() runs the sim phases; tick_render() runs Render.
fn emit_tick_helpers() -> void { function emit_tick_helpers() -> void {
emit("define void @L_tick_fixed() {\nentry:\n") emit("define void @L_tick_fixed() {\nentry:\n")
ll_t = 0; ll_lbl = 0 ll_t = 0; ll_lbl = 0
emit_calls_for_phase("Input") emit_calls_for_phase("Input")
@ -703,7 +703,7 @@ fn emit_tick_helpers() -> void {
# system functions + lifecycle hooks + the drivable tick helpers — shared by the # system functions + lifecycle hooks + the drivable tick helpers — shared by the
# auto-loop game (emit_game_main) and an entry-driven game that owns its own loop. # auto-loop game (emit_game_main) and an entry-driven game that owns its own loop.
fn emit_game_defs() -> void { function emit_game_defs() -> void {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i = i + 1 }
emit_despawn_hooks() emit_despawn_hooks()
@ -711,7 +711,7 @@ fn emit_game_defs() -> void {
emit_tick_helpers() emit_tick_helpers()
} }
fn emit_game_main() -> void { function emit_game_main() -> void {
emit_game_defs() emit_game_defs()
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n") emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")

View file

@ -11,7 +11,7 @@
# Hash.mix(x) fmix32 avalanche of a single int (turn a counter into a seed) # Hash.mix(x) fmix32 avalanche of a single int (turn a counter into a seed)
# Hash.combine(...) fold several ints into one (e.g. world_seed, cx, cy) # Hash.combine(...) fold several ints into one (e.g. world_seed, cx, cy)
fn is_hash_ns(meth: ptr) -> bool { function is_hash_ns(meth: ptr) -> bool {
if (meth == "of") or (meth == "fnv1a") or (meth == "crc32") { return true } if (meth == "of") or (meth == "fnv1a") or (meth == "crc32") { return true }
if (meth == "mix") or (meth == "combine") { return true } if (meth == "mix") or (meth == "combine") { return true }
if (meth == "of64") or (meth == "fnv1a_64") or (meth == "mix64") { return true } if (meth == "of64") or (meth == "fnv1a_64") or (meth == "mix64") { return true }
@ -21,7 +21,7 @@ fn is_hash_ns(meth: ptr) -> bool {
# fmix32 (MurmurHash3 finalizer) of a single i32 -> code of an i32. A strong # fmix32 (MurmurHash3 finalizer) of a single i32 -> code of an i32. A strong
# avalanche: flips ~half the output bits for any one input bit. Used on its own # avalanche: flips ~half the output bits for any one input bit. Used on its own
# (Hash.mix) and nowhere else — combine has its own mixing step. # (Hash.mix) and nowhere else — combine has its own mixing step.
fn hash_mix_code(x: ptr) -> ptr { function hash_mix_code(x: ptr) -> ptr {
let a = emit_bind(`lshr i32 {x}, 16`) let a = emit_bind(`lshr i32 {x}, 16`)
let b = emit_bind(`xor i32 {x}, {a}`) let b = emit_bind(`xor i32 {x}, {a}`)
let c = emit_bind(`mul i32 {b}, -2048144789`) # * 0x85ebca6b let c = emit_bind(`mul i32 {b}, -2048144789`) # * 0x85ebca6b
@ -35,7 +35,7 @@ fn hash_mix_code(x: ptr) -> ptr {
# fmix64 (MurmurHash3 64-bit finalizer) of a single i64 -> code of an i64. The # fmix64 (MurmurHash3 64-bit finalizer) of a single i64 -> code of an i64. The
# 64-bit twin of hash_mix_code: shift by 33 and multiply by the two 64-bit # 64-bit twin of hash_mix_code: shift by 33 and multiply by the two 64-bit
# constants. Backs Hash.mix64. # constants. Backs Hash.mix64.
fn hash_mix64_code(x: ptr) -> ptr { function hash_mix64_code(x: ptr) -> ptr {
let a = emit_bind(`lshr i64 {x}, 33`) let a = emit_bind(`lshr i64 {x}, 33`)
let b = emit_bind(`xor i64 {x}, {a}`) let b = emit_bind(`xor i64 {x}, {a}`)
let c = emit_bind(`mul i64 {b}, -49064778989728563`) # * 0xff51afd7ed558ccd let c = emit_bind(`mul i64 {b}, -49064778989728563`) # * 0xff51afd7ed558ccd
@ -46,7 +46,7 @@ fn hash_mix64_code(x: ptr) -> ptr {
return emit_bind(`xor i64 {f}, {g}`) return emit_bind(`xor i64 {f}, {g}`)
} }
fn emit_hash_ns(meth: ptr, e: Node) -> Val { function emit_hash_ns(meth: ptr, e: Node) -> Val {
if (meth == "of") or (meth == "fnv1a") { # FNV-1a 32-bit over the bytes if (meth == "of") or (meth == "fnv1a") { # FNV-1a 32-bit over the bytes
g_uses_hashrt = true g_uses_hashrt = true
let s = emit_expr(e.kids[0]) let s = emit_expr(e.kids[0])
@ -94,7 +94,7 @@ fn emit_hash_ns(meth: ptr, e: Node) -> Val {
# at a time with pure integer IR: FNV-1a with the standard 32-bit offset basis / # at a time with pure integer IR: FNV-1a with the standard 32-bit offset basis /
# prime, and a bitwise CRC-32 with the reflected poly 0xEDB88320. No libc, no # prime, and a bitwise CRC-32 with the reflected poly 0xEDB88320. No libc, no
# allocation, bit-identical on every target. # allocation, bit-identical on every target.
fn emit_hash_prelude() -> void { function emit_hash_prelude() -> void {
emith("define i32 @fn_hash_fnv1a(ptr %s) {\n") emith("define i32 @fn_hash_fnv1a(ptr %s) {\n")
emith("entry:\n") emith("entry:\n")
emith(" %hp = alloca i32\n") emith(" %hp = alloca i32\n")

View file

@ -1,10 +1,10 @@
# emit_head.ludic — string constants and the module header (libc declarations, # emit_head.ludic — string constants and the module header (libc declarations,
# the slice header type, struct layouts, globals, argv, format strings). # the slice header type, struct layouts, globals, argv, format strings).
fn hexdig(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } # 0-9 A-F function hexdig(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } # 0-9 A-F
# emit `@.strN = ... c"escaped\00"` and return its name; % and non-print -> \XX # emit `@.strN = ... c"escaped\00"` and return its name; % and non-print -> \XX
fn emit_str_const(s: ptr) -> ptr { function emit_str_const(s: ptr) -> ptr {
let name = `@.str{itoa(ll_str)}` let name = `@.str{itoa(ll_str)}`
ll_str = ll_str + 1 ll_str = ll_str + 1
let n = len(s) let n = len(s)
@ -25,7 +25,7 @@ fn emit_str_const(s: ptr) -> ptr {
} }
# the constant initializer for a global var: a literal, or 0/null # the constant initializer for a global var: a literal, or 0/null
fn global_init(d: Node) -> ptr { function global_init(d: Node) -> ptr {
if (d.a == null) { if (llty(d.ty) == "ptr") { return "null" }; return "0" } if (d.a == null) { if (llty(d.ty) == "ptr") { return "null" }; return "0" }
let e = d.a let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) } if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
@ -34,7 +34,7 @@ fn global_init(d: Node) -> ptr {
return "0" return "0"
} }
fn emit_header() -> void { function emit_header() -> void {
emith("; Ludic (self-hosted) -> LLVM IR\n") emith("; Ludic (self-hosted) -> LLVM IR\n")
emith("declare i32 @printf(ptr, ...)\n") emith("declare i32 @printf(ptr, ...)\n")
emith("declare ptr @malloc(i64)\n") emith("declare ptr @malloc(i64)\n")
@ -109,7 +109,7 @@ fn emit_header() -> void {
# One `declare <ret> @<sym>(<argtys>)` per `extern fn`, so the linker resolves the # One `declare <ret> @<sym>(<argtys>)` per `extern fn`, so the linker resolves the
# call to the bound symbol. Emitted after the header; a program with no `extern fn` # call to the bound symbol. Emitted after the header; a program with no `extern fn`
# emits nothing here, so un-networked builds stay byte-identical. # emits nothing here, so un-networked builds stay byte-identical.
fn emit_extern_decls() -> void { function emit_extern_decls() -> void {
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]
@ -131,7 +131,7 @@ fn emit_extern_decls() -> void {
# on strings. Hand-written IR over NUL-terminated byte buffers: str_eq walks both # on strings. Hand-written IR over NUL-terminated byte buffers: str_eq walks both
# until a mismatch or a shared terminator; str_concat measures both, mallocs # until a mismatch or a shared terminator; str_concat measures both, mallocs
# len+len+1, copies each half, and NUL-terminates. @malloc is always declared. # len+len+1, copies each half, and NUL-terminates. @malloc is always declared.
fn emit_str_prelude() -> void { function emit_str_prelude() -> void {
emith("define i32 @fn_str_eq(ptr %a, ptr %b) {\n") emith("define i32 @fn_str_eq(ptr %a, ptr %b) {\n")
emith("entry:\n br label %loop\n") emith("entry:\n br label %loop\n")
emith("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n") emith("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
@ -173,7 +173,7 @@ fn emit_str_prelude() -> void {
# int -> decimal string, emitted (once) into any program that uses str(int) # int -> decimal string, emitted (once) into any program that uses str(int)
# (string interpolation of a number). Writes digits from the end of a 24-byte # (string interpolation of a number). Writes digits from the end of a 24-byte
# buffer, prepends '-' for negatives, and returns a pointer into the buffer. # buffer, prepends '-' for negatives, and returns a pointer into the buffer.
fn emit_int_str() -> void { function emit_int_str() -> void {
emith("define ptr @fn_int_str(i32 %n0) {\n") emith("define ptr @fn_int_str(i32 %n0) {\n")
emith("entry:\n %buf = call ptr @malloc(i64 24)\n") emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
emith(" %isneg = icmp slt i32 %n0, 0\n %neg = sub i32 0, %n0\n") emith(" %isneg = icmp slt i32 %n0, 0\n %neg = sub i32 0, %n0\n")
@ -196,7 +196,7 @@ fn emit_int_str() -> void {
# the i64 twin of fn_int_str: a signed 64-bit integer -> decimal text. Emitted # the i64 twin of fn_int_str: a signed 64-bit integer -> decimal text. Emitted
# once per program that stringifies a `long` (g_uses_longstr). A 64-bit value is # once per program that stringifies a `long` (g_uses_longstr). A 64-bit value is
# at most 20 digits plus sign and NUL, so the 24-byte scratch buffer still fits. # at most 20 digits plus sign and NUL, so the 24-byte scratch buffer still fits.
fn emit_long_str() -> void { function emit_long_str() -> void {
emith("define ptr @fn_long_str(i64 %n0) {\n") emith("define ptr @fn_long_str(i64 %n0) {\n")
emith("entry:\n %buf = call ptr @malloc(i64 24)\n") emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
emith(" %isneg = icmp slt i64 %n0, 0\n %neg = sub i64 0, %n0\n") emith(" %isneg = icmp slt i64 %n0, 0\n %neg = sub i64 0, %n0\n")
@ -218,7 +218,7 @@ fn emit_long_str() -> void {
# s[a..b] -> a fresh NUL-terminated copy of the bytes [a, b), emitted (once) into # s[a..b] -> a fresh NUL-terminated copy of the bytes [a, b), emitted (once) into
# any program that slices a string. Mallocs (b-a)+1, copies, terminates. # any program that slices a string. Mallocs (b-a)+1, copies, terminates.
fn emit_str_slice() -> void { function emit_str_slice() -> void {
emith("define ptr @fn_str_slice(ptr %s, i32 %start, i32 %end) {\n") emith("define ptr @fn_str_slice(ptr %s, i32 %start, i32 %end) {\n")
emith("entry:\n %len = sub i32 %end, %start\n %sz = add i32 %len, 1\n") emith("entry:\n %len = sub i32 %end, %start\n %sz = add i32 %len, 1\n")
emith(" %sz64 = sext i32 %sz to i64\n %out = call ptr @malloc(i64 %sz64)\n br label %loop\n") emith(" %sz64 = sext i32 %sz to i64\n %out = call ptr @malloc(i64 %sz64)\n br label %loop\n")

View file

@ -6,7 +6,7 @@ var g_intrin_ok: bool = false
# is `name` a low-level intrinsic? A pure name check, so it can gate dispatch # is `name` a low-level intrinsic? A pure name check, so it can gate dispatch
# without evaluating arguments (which could clobber shared state). # without evaluating arguments (which could clobber shared state).
fn is_intrinsic(name: ptr) -> bool { function is_intrinsic(name: ptr) -> bool {
if (name == "resize") { return true } if (name == "resize") { return true }
if (name == "file_open") or (name == "file_read") or (name == "file_write") { return true } if (name == "file_open") or (name == "file_read") or (name == "file_write") { return true }
if (name == "file_seek") or (name == "file_tell") or (name == "file_close") { return true } if (name == "file_seek") or (name == "file_tell") or (name == "file_close") { return true }
@ -16,11 +16,11 @@ fn is_intrinsic(name: ptr) -> bool {
return false return false
} }
# emit " <r> = <rest>\n" and return r # emit " <r> = <rest>\n" and return r
fn emit_bind(rest: ptr) -> ptr { let r = nreg(); emit(" "); emit(r); emit(" = "); emit(rest); emit("\n"); return r } function emit_bind(rest: ptr) -> ptr { let r = nreg(); emit(" "); emit(r); emit(" = "); emit(rest); emit("\n"); return r }
fn arg_code(e: Node, i: int) -> ptr { let v = emit_expr(e.kids[i]); return v.code } function arg_code(e: Node, i: int) -> ptr { let v = emit_expr(e.kids[i]); return v.code }
fn emit_intrinsic(name: ptr, e: Node) -> Val { function emit_intrinsic(name: ptr, e: Node) -> Val {
g_intrin_ok = true g_intrin_ok = true
# ptr_null / ptr_is_null are the `null` literal and `x == null` now. # ptr_null / ptr_is_null are the `null` literal and `x == null` now.
# mem_alloc is bytes(n) / words(n) now (see emit_call). # mem_alloc is bytes(n) / words(n) now (see emit_call).

View file

@ -4,7 +4,7 @@
var g_windowed: bool = false # headless by default (games read stdin / dump PPM) var g_windowed: bool = false # headless by default (games read stdin / dump PPM)
fn is_intrinsic2(name: ptr) -> bool { function is_intrinsic2(name: ptr) -> bool {
if (name == "free") or (name == "fill") { return true } if (name == "free") or (name == "fill") { return true }
if (name == "offset") or (name == "read_char") { return true } if (name == "offset") or (name == "read_char") { return true }
if (name == "as_fixed") or (name == "as_int") { return true } if (name == "as_fixed") or (name == "as_int") { return true }
@ -14,7 +14,7 @@ fn is_intrinsic2(name: ptr) -> bool {
return false return false
} }
fn emit_intrinsic2(name: ptr, e: Node) -> Val { function emit_intrinsic2(name: ptr, e: Node) -> Val {
if (name == "free") { if (name == "free") {
let p = arg_code(e, 0); emit(" call void @free(ptr "); emit(p); emit(")\n"); return val("0", "void") let p = arg_code(e, 0); emit(" call void @free(ptr "); emit(p); emit(")\n"); return val("0", "void")
} }

View file

@ -5,13 +5,13 @@
# for reference elements (structs/strings), matching how `==` behaves elsewhere. # for reference elements (structs/strings), matching how `==` behaves elsewhere.
# address of element `idx` (an i32 code) in slice header `h`, element LLVM type `elt` # address of element `idx` (an i32 code) in slice header `h`, element LLVM type `elt`
fn list_elem_addr(h: ptr, elt: ptr, idx: ptr) -> ptr { function list_elem_addr(h: ptr, elt: ptr, idx: ptr) -> ptr {
let dp = slice_field(h, 0) let dp = slice_field(h, 0)
let data = emit_bind(`load ptr, ptr {dp}`) let data = emit_bind(`load ptr, ptr {dp}`)
return emit_bind(`getelementptr inbounds {elt}, ptr {data}, i32 {idx}`) return emit_bind(`getelementptr inbounds {elt}, ptr {data}, i32 {idx}`)
} }
fn is_list_ns(meth: ptr) -> bool { function is_list_ns(meth: ptr) -> bool {
if (meth == "len") or (meth == "push") or (meth == "clear") { return true } if (meth == "len") or (meth == "push") or (meth == "clear") { return true }
if (meth == "first") or (meth == "last") or (meth == "pop") or (meth == "swap") { return true } if (meth == "first") or (meth == "last") or (meth == "pop") or (meth == "swap") { return true }
if (meth == "contains") or (meth == "index_of") or (meth == "reverse") { return true } if (meth == "contains") or (meth == "index_of") or (meth == "reverse") { return true }
@ -21,7 +21,7 @@ fn is_list_ns(meth: ptr) -> bool {
# grow the slice's backing buffer if it is full, exactly as push does (double, # grow the slice's backing buffer if it is full, exactly as push does (double,
# or 8 from empty). Leaves length untouched; only capacity/data may change. # or 8 from empty). Leaves length untouched; only capacity/data may change.
fn list_grow_if_full(h: ptr, elt: ptr) -> void { function list_grow_if_full(h: ptr, elt: ptr) -> void {
let lp = slice_field(h, 1); let cp = slice_field(h, 2); let dp = slice_field(h, 0) let lp = slice_field(h, 1); let cp = slice_field(h, 2); let dp = slice_field(h, 0)
let l = emit_bind(`load i32, ptr {lp}`) let l = emit_bind(`load i32, ptr {lp}`)
let c = emit_bind(`load i32, ptr {cp}`) let c = emit_bind(`load i32, ptr {cp}`)
@ -43,7 +43,7 @@ fn list_grow_if_full(h: ptr, elt: ptr) -> void {
emit(done); emit(":\n") emit(done); emit(":\n")
} }
fn emit_list_ns(meth: ptr, e: Node) -> Val { function emit_list_ns(meth: ptr, e: Node) -> Val {
if (meth == "len") { return emit_len(e) } # same header length as len(s) if (meth == "len") { return emit_len(e) } # same header length as len(s)
if (meth == "push") { return emit_push(e) } # same as push(s, v) if (meth == "push") { return emit_push(e) } # same as push(s, v)
let s = emit_expr(e.kids[0]) let s = emit_expr(e.kids[0])

View file

@ -4,7 +4,7 @@
# older code, a register index (reg()/set_reg()). # older code, a register index (reg()/set_reg()).
# If `a` names a program-scope `var`, return it; else null (a register index). # If `a` names a program-scope `var`, return it; else null (a register index).
fn machine_var(a: Node) -> Node { function machine_var(a: Node) -> Node {
if a.kind == E_ID { if a.kind == E_ID {
let g = find_global(a.s) let g = find_global(a.s)
if (g != null) { if g.kind == N_VAR { return g } } if (g != null) { if g.kind == N_VAR { return g } }
@ -12,7 +12,7 @@ fn machine_var(a: Node) -> Node {
return null return null
} }
fn emit_machine(st: Node) -> void { function emit_machine(st: Node) -> void {
let gv = machine_var(st.a) let gv = machine_var(st.a)
var s = "" var s = ""
if (gv != null) { s = emit_bind(`load i32, ptr @g_{st.a.s}`) } if (gv != null) { s = emit_bind(`load i32, ptr @g_{st.a.s}`) }
@ -45,7 +45,7 @@ fn emit_machine(st: Node) -> void {
# enclosing `machine`, otherwise a scene transition. A scene transition runs the # enclosing `machine`, otherwise a scene transition. A scene transition runs the
# source scene's on-exit, stores the target scene id into @L_scene, and runs the # source scene's on-exit, stores the target scene id into @L_scene, and runs the
# target's on-enter (two direct calls and a store — no dispatch table). # target's on-enter (two direct calls and a store — no dispatch table).
fn emit_become(st: Node) -> void { function emit_become(st: Node) -> void {
if nmach > 0 { # inside a machine: try a state first if nmach > 0 { # inside a machine: try a state first
let m = mach_stk[nmach - 1] let m = mach_stk[nmach - 1]
var target: Node = null var target: Node = null

View file

@ -5,11 +5,11 @@
# integer root, @fn_fx_sin a 256-entry interpolated sine table). Everything is # integer root, @fn_fx_sin a 256-entry interpolated sine table). Everything is
# plain integer IR, so it is bit-identical on every platform. # plain integer IR, so it is bit-identical on every platform.
fn is_math_builtin(name: ptr) -> bool { function is_math_builtin(name: ptr) -> bool {
return (name == "min") or (name == "max") or (name == "abs") or (name == "clamp") return (name == "min") or (name == "max") or (name == "abs") or (name == "clamp")
} }
fn emit_math_builtin(name: ptr, e: Node) -> Val { function emit_math_builtin(name: ptr, e: Node) -> Val {
if (name == "abs") { if (name == "abs") {
let a = emit_expr(e.kids[0]) let a = emit_expr(e.kids[0])
let c = emit_bind(`icmp slt i32 {a.code}, 0`) let c = emit_bind(`icmp slt i32 {a.code}, 0`)
@ -32,7 +32,7 @@ fn emit_math_builtin(name: ptr, e: Node) -> Val {
} }
# a * b in Q16.16 (64-bit intermediate, arithmetic shift back) -> code of an i32 # a * b in Q16.16 (64-bit intermediate, arithmetic shift back) -> code of an i32
fn fx_mul_code(a: ptr, b: ptr) -> ptr { function fx_mul_code(a: ptr, b: ptr) -> ptr {
let a64 = emit_bind(`sext i32 {a} to i64`) let a64 = emit_bind(`sext i32 {a} to i64`)
let b64 = emit_bind(`sext i32 {b} to i64`) let b64 = emit_bind(`sext i32 {b} to i64`)
let m = emit_bind(`mul i64 {a64}, {b64}`) let m = emit_bind(`mul i64 {a64}, {b64}`)
@ -41,7 +41,7 @@ fn fx_mul_code(a: ptr, b: ptr) -> ptr {
} }
# a / b in Q16.16 (shift the numerator up before the divide) -> code of an i32 # a / b in Q16.16 (shift the numerator up before the divide) -> code of an i32
fn fx_div_code(a: ptr, b: ptr) -> ptr { function fx_div_code(a: ptr, b: ptr) -> ptr {
let a64 = emit_bind(`sext i32 {a} to i64`) let a64 = emit_bind(`sext i32 {a} to i64`)
let ash = emit_bind(`shl i64 {a64}, 16`) let ash = emit_bind(`shl i64 {a64}, 16`)
let b64 = emit_bind(`sext i32 {b} to i64`) let b64 = emit_bind(`sext i32 {b} to i64`)
@ -50,14 +50,14 @@ fn fx_div_code(a: ptr, b: ptr) -> ptr {
} }
# lerp(a, b, t) = a + (b - a) * t, all Q16.16 -> code of a fixed i32 # lerp(a, b, t) = a + (b - a) * t, all Q16.16 -> code of a fixed i32
fn fx_lerp_code(a: ptr, b: ptr, t: ptr) -> ptr { function fx_lerp_code(a: ptr, b: ptr, t: ptr) -> ptr {
let d = emit_bind(`sub i32 {b}, {a}`) let d = emit_bind(`sub i32 {b}, {a}`)
let dt = fx_mul_code(d, t) let dt = fx_mul_code(d, t)
return emit_bind(`add i32 {a}, {dt}`) return emit_bind(`add i32 {a}, {dt}`)
} }
# inverse_lerp(a, b, v) = (v - a) / (b - a), all Q16.16 -> code of a fixed i32 # inverse_lerp(a, b, v) = (v - a) / (b - a), all Q16.16 -> code of a fixed i32
fn fx_inv_lerp_code(a: ptr, b: ptr, v: ptr) -> ptr { function fx_inv_lerp_code(a: ptr, b: ptr, v: ptr) -> ptr {
let num = emit_bind(`sub i32 {v}, {a}`) let num = emit_bind(`sub i32 {v}, {a}`)
let den = emit_bind(`sub i32 {b}, {a}`) let den = emit_bind(`sub i32 {b}, {a}`)
return fx_div_code(num, den) return fx_div_code(num, den)
@ -66,7 +66,7 @@ fn fx_inv_lerp_code(a: ptr, b: ptr, v: ptr) -> ptr {
# Math.* — the namespaced surface. min/max/abs/clamp reuse the bare lowering; # Math.* — the namespaced surface. min/max/abs/clamp reuse the bare lowering;
# the rest are new deterministic fixed-point helpers. Returns g_intrin-style via # the rest are new deterministic fixed-point helpers. Returns g_intrin-style via
# a direct Val; callers guard with is_math_ns first. # a direct Val; callers guard with is_math_ns first.
fn is_math_ns(meth: ptr) -> bool { function is_math_ns(meth: ptr) -> bool {
if (meth == "min") or (meth == "max") or (meth == "abs") or (meth == "clamp") { return true } if (meth == "min") or (meth == "max") or (meth == "abs") or (meth == "clamp") { return true }
if (meth == "sign") or (meth == "floor") or (meth == "ceil") or (meth == "round") { return true } if (meth == "sign") or (meth == "floor") or (meth == "ceil") or (meth == "round") { return true }
if (meth == "lerp") or (meth == "inverse_lerp") or (meth == "remap") { return true } if (meth == "lerp") or (meth == "inverse_lerp") or (meth == "remap") { return true }
@ -79,7 +79,7 @@ fn is_math_ns(meth: ptr) -> bool {
return false return false
} }
fn emit_math_ns(meth: ptr, e: Node) -> Val { function emit_math_ns(meth: ptr, e: Node) -> Val {
if (meth == "min") or (meth == "max") or (meth == "abs") or (meth == "clamp") { if (meth == "min") or (meth == "max") or (meth == "abs") or (meth == "clamp") {
return emit_math_builtin(meth, e) return emit_math_builtin(meth, e)
} }
@ -284,7 +284,7 @@ fn emit_math_ns(meth: ptr, e: Node) -> Val {
# sine table with linear interpolation; @fn_fx_exp2/@fn_fx_log2 are range-reduced # sine table with linear interpolation; @fn_fx_exp2/@fn_fx_log2 are range-reduced
# Q16.16 polynomials (base-2 exp and log) that back exp/log/pow. All are pure # Q16.16 polynomials (base-2 exp and log) that back exp/log/pow. All are pure
# integer IR, so bit-identical on every platform. # integer IR, so bit-identical on every platform.
fn emit_math_prelude() -> void { function emit_math_prelude() -> void {
emith("@L_sin_tab = private unnamed_addr constant [256 x i32] [i32 0, i32 1608, i32 3216, i32 4821, i32 6424, i32 8022, i32 9616, i32 11204, i32 12785, i32 14359, i32 15924, i32 17479, i32 19024, i32 20557, i32 22078, i32 23586, i32 25080, i32 26558, i32 28020, i32 29466, i32 30893, i32 32303, i32 33692, i32 35062, i32 36410, i32 37736, i32 39040, i32 40320, i32 41576, i32 42806, i32 44011, i32 45190, i32 46341, i32 47464, i32 48559, i32 49624, i32 50660, i32 51665, i32 52639, i32 53581, i32 54491, i32 55368, i32 56212, i32 57022, i32 57798, i32 58538, i32 59244, i32 59914, i32 60547, i32 61145, i32 61705, i32 62228, i32 62714, i32 63162, i32 63572, i32 63944, i32 64277, i32 64571, i32 64827, i32 65043, i32 65220, i32 65358, i32 65457, i32 65516, i32 65536, i32 65516, i32 65457, i32 65358, i32 65220, i32 65043, i32 64827, i32 64571, i32 64277, i32 63944, i32 63572, i32 63162, i32 62714, i32 62228, i32 61705, i32 61145, i32 60547, i32 59914, i32 59244, i32 58538, i32 57798, i32 57022, i32 56212, i32 55368, i32 54491, i32 53581, i32 52639, i32 51665, i32 50660, i32 49624, i32 48559, i32 47464, i32 46341, i32 45190, i32 44011, i32 42806, i32 41576, i32 40320, i32 39040, i32 37736, i32 36410, i32 35062, i32 33692, i32 32303, i32 30893, i32 29466, i32 28020, i32 26558, i32 25080, i32 23586, i32 22078, i32 20557, i32 19024, i32 17479, i32 15924, i32 14359, i32 12785, i32 11204, i32 9616, i32 8022, i32 6424, i32 4821, i32 3216, i32 1608, i32 0, i32 -1608, i32 -3216, i32 -4821, i32 -6424, i32 -8022, i32 -9616, i32 -11204, i32 -12785, i32 -14359, i32 -15924, i32 -17479, i32 -19024, i32 -20557, i32 -22078, i32 -23586, i32 -25080, i32 -26558, i32 -28020, i32 -29466, i32 -30893, i32 -32303, i32 -33692, i32 -35062, i32 -36410, i32 -37736, i32 -39040, i32 -40320, i32 -41576, i32 -42806, i32 -44011, i32 -45190, i32 -46341, i32 -47464, i32 -48559, i32 -49624, i32 -50660, i32 -51665, i32 -52639, i32 -53581, i32 -54491, i32 -55368, i32 -56212, i32 -57022, i32 -57798, i32 -58538, i32 -59244, i32 -59914, i32 -60547, i32 -61145, i32 -61705, i32 -62228, i32 -62714, i32 -63162, i32 -63572, i32 -63944, i32 -64277, i32 -64571, i32 -64827, i32 -65043, i32 -65220, i32 -65358, i32 -65457, i32 -65516, i32 -65536, i32 -65516, i32 -65457, i32 -65358, i32 -65220, i32 -65043, i32 -64827, i32 -64571, i32 -64277, i32 -63944, i32 -63572, i32 -63162, i32 -62714, i32 -62228, i32 -61705, i32 -61145, i32 -60547, i32 -59914, i32 -59244, i32 -58538, i32 -57798, i32 -57022, i32 -56212, i32 -55368, i32 -54491, i32 -53581, i32 -52639, i32 -51665, i32 -50660, i32 -49624, i32 -48559, i32 -47464, i32 -46341, i32 -45190, i32 -44011, i32 -42806, i32 -41576, i32 -40320, i32 -39040, i32 -37736, i32 -36410, i32 -35062, i32 -33692, i32 -32303, i32 -30893, i32 -29466, i32 -28020, i32 -26558, i32 -25080, i32 -23586, i32 -22078, i32 -20557, i32 -19024, i32 -17479, i32 -15924, i32 -14359, i32 -12785, i32 -11204, i32 -9616, i32 -8022, i32 -6424, i32 -4821, i32 -3216, i32 -1608]\n") emith("@L_sin_tab = private unnamed_addr constant [256 x i32] [i32 0, i32 1608, i32 3216, i32 4821, i32 6424, i32 8022, i32 9616, i32 11204, i32 12785, i32 14359, i32 15924, i32 17479, i32 19024, i32 20557, i32 22078, i32 23586, i32 25080, i32 26558, i32 28020, i32 29466, i32 30893, i32 32303, i32 33692, i32 35062, i32 36410, i32 37736, i32 39040, i32 40320, i32 41576, i32 42806, i32 44011, i32 45190, i32 46341, i32 47464, i32 48559, i32 49624, i32 50660, i32 51665, i32 52639, i32 53581, i32 54491, i32 55368, i32 56212, i32 57022, i32 57798, i32 58538, i32 59244, i32 59914, i32 60547, i32 61145, i32 61705, i32 62228, i32 62714, i32 63162, i32 63572, i32 63944, i32 64277, i32 64571, i32 64827, i32 65043, i32 65220, i32 65358, i32 65457, i32 65516, i32 65536, i32 65516, i32 65457, i32 65358, i32 65220, i32 65043, i32 64827, i32 64571, i32 64277, i32 63944, i32 63572, i32 63162, i32 62714, i32 62228, i32 61705, i32 61145, i32 60547, i32 59914, i32 59244, i32 58538, i32 57798, i32 57022, i32 56212, i32 55368, i32 54491, i32 53581, i32 52639, i32 51665, i32 50660, i32 49624, i32 48559, i32 47464, i32 46341, i32 45190, i32 44011, i32 42806, i32 41576, i32 40320, i32 39040, i32 37736, i32 36410, i32 35062, i32 33692, i32 32303, i32 30893, i32 29466, i32 28020, i32 26558, i32 25080, i32 23586, i32 22078, i32 20557, i32 19024, i32 17479, i32 15924, i32 14359, i32 12785, i32 11204, i32 9616, i32 8022, i32 6424, i32 4821, i32 3216, i32 1608, i32 0, i32 -1608, i32 -3216, i32 -4821, i32 -6424, i32 -8022, i32 -9616, i32 -11204, i32 -12785, i32 -14359, i32 -15924, i32 -17479, i32 -19024, i32 -20557, i32 -22078, i32 -23586, i32 -25080, i32 -26558, i32 -28020, i32 -29466, i32 -30893, i32 -32303, i32 -33692, i32 -35062, i32 -36410, i32 -37736, i32 -39040, i32 -40320, i32 -41576, i32 -42806, i32 -44011, i32 -45190, i32 -46341, i32 -47464, i32 -48559, i32 -49624, i32 -50660, i32 -51665, i32 -52639, i32 -53581, i32 -54491, i32 -55368, i32 -56212, i32 -57022, i32 -57798, i32 -58538, i32 -59244, i32 -59914, i32 -60547, i32 -61145, i32 -61705, i32 -62228, i32 -62714, i32 -63162, i32 -63572, i32 -63944, i32 -64277, i32 -64571, i32 -64827, i32 -65043, i32 -65220, i32 -65358, i32 -65457, i32 -65516, i32 -65536, i32 -65516, i32 -65457, i32 -65358, i32 -65220, i32 -65043, i32 -64827, i32 -64571, i32 -64277, i32 -63944, i32 -63572, i32 -63162, i32 -62714, i32 -62228, i32 -61705, i32 -61145, i32 -60547, i32 -59914, i32 -59244, i32 -58538, i32 -57798, i32 -57022, i32 -56212, i32 -55368, i32 -54491, i32 -53581, i32 -52639, i32 -51665, i32 -50660, i32 -49624, i32 -48559, i32 -47464, i32 -46341, i32 -45190, i32 -44011, i32 -42806, i32 -41576, i32 -40320, i32 -39040, i32 -37736, i32 -36410, i32 -35062, i32 -33692, i32 -32303, i32 -30893, i32 -29466, i32 -28020, i32 -26558, i32 -25080, i32 -23586, i32 -22078, i32 -20557, i32 -19024, i32 -17479, i32 -15924, i32 -14359, i32 -12785, i32 -11204, i32 -9616, i32 -8022, i32 -6424, i32 -4821, i32 -3216, i32 -1608]\n")
emith("define i32 @fn_fx_sqrt(i32 %x) {\n") emith("define i32 @fn_fx_sqrt(i32 %x) {\n")
emith("entry:\n %neg = icmp slt i32 %x, 0\n br i1 %neg, label %ret0, label %go\n") emith("entry:\n %neg = icmp slt i32 %x, 0\n br i1 %neg, label %ret0, label %go\n")

View file

@ -3,13 +3,13 @@
# read and write one byte. The low-level escape hatch, PICO-8's memcpy/memset/ # read and write one byte. The low-level escape hatch, PICO-8's memcpy/memset/
# peek/poke by another name. # peek/poke by another name.
fn is_mem_ns(meth: ptr) -> bool { function is_mem_ns(meth: ptr) -> bool {
if (meth == "bytes") or (meth == "words") { return true } if (meth == "bytes") or (meth == "words") { return true }
if (meth == "copy") or (meth == "fill") or (meth == "peek") or (meth == "poke") { return true } if (meth == "copy") or (meth == "fill") or (meth == "peek") or (meth == "poke") { return true }
return false return false
} }
fn emit_mem_ns(meth: ptr, e: Node) -> Val { function emit_mem_ns(meth: ptr, e: Node) -> Val {
if (meth == "bytes") { # allocate n bytes -> a byte buffer if (meth == "bytes") { # allocate n bytes -> a byte buffer
let n = emit_expr(e.kids[0]) let n = emit_expr(e.kids[0])
let w = emit_bind(`zext i32 {n.code} to i64`) let w = emit_bind(`zext i32 {n.code} to i64`)

View file

@ -19,11 +19,11 @@ var g_uses_loopback: bool = false
# component participates in the entity's model (the model member is @Sync, # component participates in the entity's model (the model member is @Sync,
# member.ival==1). Participation is decided per model use-site. # member.ival==1). Participation is decided per model use-site.
fn net_field_ibytes(ty: ptr) -> int { if (llty(ty) == "i8") { return 1 }; return 4 } function net_field_ibytes(ty: ptr) -> int { if (llty(ty) == "i8") { return 1 }; return 4 }
fn net_field_bytes(ty: ptr) -> ptr { if (llty(ty) == "i8") { return "1" }; return "4" } function net_field_bytes(ty: ptr) -> ptr { if (llty(ty) == "i8") { return "1" }; return "4" }
# total replicated bytes for model m (compile-time constant) # total replicated bytes for model m (compile-time constant)
fn net_model_bytes(m: Node) -> int { function net_model_bytes(m: Node) -> int {
var total = 0 var total = 0
var ci = 0 var ci = 0
while ci < len(m.kids) { while ci < len(m.kids) {
@ -39,16 +39,16 @@ fn net_model_bytes(m: Node) -> int {
return total return total
} }
fn net_model_syncs(m: Node) -> bool { return net_model_bytes(m) > 0 } function net_model_syncs(m: Node) -> bool { return net_model_bytes(m) > 0 }
fn net_has_sync() -> bool { function net_has_sync() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_ARCH { if net_model_syncs(prog[i]) { return true } }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_ARCH { if net_model_syncs(prog[i]) { return true } }; i = i + 1 }
return false return false
} }
# ---- N3: ownership ----------------------------------------------------------- # ---- N3: ownership -----------------------------------------------------------
fn net_has_owned() -> bool { function net_has_owned() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_ARCH and (prog[i].ival == 1) { return true }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_ARCH and (prog[i].ival == 1) { return true }; i = i + 1 }
return false return false
@ -56,7 +56,7 @@ fn net_has_owned() -> bool {
# ---- N5: role-tagged handlers ------------------------------------------------ # ---- N5: role-tagged handlers ------------------------------------------------
# A handler tagged @Server (ival==1) or @Predicted (ival==2) has a network role. # A handler tagged @Server (ival==1) or @Predicted (ival==2) has a network role.
fn net_has_role() -> bool { function net_has_role() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_SYS and (prog[i].ival != 0) { return true }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_SYS and (prog[i].ival != 0) { return true }; i = i + 1 }
return false return false
@ -64,10 +64,10 @@ fn net_has_role() -> bool {
# Any networking feature in use → emit the shared role registers (@L_role / # Any networking feature in use → emit the shared role registers (@L_role /
# @L_localid). A runtime sets them; offline they keep their single-player default. # @L_localid). A runtime sets them; offline they keep their single-player default.
fn net_any() -> bool { return net_has_sync() or net_has_owned() or net_has_role() } function net_any() -> bool { return net_has_sync() or net_has_owned() or net_has_role() }
# ---- diagnostics ------------------------------------------------------------- # ---- diagnostics -------------------------------------------------------------
fn net_warn(msg: ptr) -> void { function net_warn(msg: ptr) -> void {
let e = file_stderr() let e = file_stderr()
file_write(e, "ludicc(self): warning: ", 23) file_write(e, "ludicc(self): warning: ", 23)
file_write(e, msg, len(msg)) file_write(e, msg, len(msg))
@ -77,7 +77,7 @@ fn net_warn(msg: ptr) -> void {
# Validate @Sync usage: a participating member whose component replicates nothing # Validate @Sync usage: a participating member whose component replicates nothing
# is a warning (participation that replicates nothing); a @Sync ptr field is a # is a warning (participation that replicates nothing); a @Sync ptr field is a
# hard error (footgun 3 — networked fields must be POD scalars). # hard error (footgun 3 — networked fields must be POD scalars).
fn net_check() -> void { function net_check() -> void {
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_ARCH { if prog[i].kind == N_ARCH {
@ -110,7 +110,7 @@ fn net_check() -> void {
# ---- N2: per-model serializer / applier -------------------------------------- # ---- N2: per-model serializer / applier --------------------------------------
# serialize_<M>(e, buf) -> bytes written. Copies each replicated field, tightly # serialize_<M>(e, buf) -> bytes written. Copies each replicated field, tightly
# packed in member-then-field order, so apply reads the identical layout. # packed in member-then-field order, so apply reads the identical layout.
fn emit_net_serialize(m: Node) -> void { function emit_net_serialize(m: Node) -> void {
ll_t = 0 ll_t = 0
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @L_serialize_"); emit(m.s); emit("(i32 %e, ptr %buf) {\nentry:\n") emit("define i32 @L_serialize_"); emit(m.s); emit("(i32 %e, ptr %buf) {\nentry:\n")
@ -143,7 +143,7 @@ fn emit_net_serialize(m: Node) -> void {
# apply_<M>(e, buf, len): the inverse — copy each replicated field back from the # apply_<M>(e, buf, len): the inverse — copy each replicated field back from the
# buffer into component storage. `len` is accepted for symmetry (the runtime's # buffer into component storage. `len` is accepted for symmetry (the runtime's
# framing) but the layout is fixed, so it is not consulted. # framing) but the layout is fixed, so it is not consulted.
fn emit_net_apply(m: Node) -> void { function emit_net_apply(m: Node) -> void {
ll_t = 0 ll_t = 0
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define void @L_apply_"); emit(m.s); emit("(i32 %e, ptr %buf, i32 %len) {\nentry:\n") emit("define void @L_apply_"); emit(m.s); emit("(i32 %e, ptr %buf, i32 %len) {\nentry:\n")
@ -177,7 +177,7 @@ fn emit_net_apply(m: Node) -> void {
# ludic_serialize(e, buf) -> bytes / ludic_apply(e, buf, len) / ludic_sync_size(e) # ludic_serialize(e, buf) -> bytes / ludic_apply(e, buf, len) / ludic_sync_size(e)
# route on the entity's model kind to the per-model function above, so a # route on the entity's model kind to the per-model function above, so a
# replication runtime replicates any entity without knowing its type. # replication runtime replicates any entity without knowing its type.
fn emit_net_dispatch() -> void { function emit_net_dispatch() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n") emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n")
@ -236,7 +236,7 @@ fn emit_net_dispatch() -> void {
# @L_owner: one i32 owner id per entity, -1 = unowned. Only emitted when a model # @L_owner: one i32 owner id per entity, -1 = unowned. Only emitted when a model
# is @Owned, and L_reset clears it to -1 on alloc/free (see emit_ecs). owner()/ # is @Owned, and L_reset clears it to -1 on alloc/free (see emit_ecs). owner()/
# set_owner()/is_owner() read and write it; the authority assigns. # set_owner()/is_owner() read and write it; the authority assigns.
fn emit_net_owner() -> void { function emit_net_owner() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @L_owner(i32 %e) {\nentry:\n") emit("define i32 @L_owner(i32 %e) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n") emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n")
@ -256,7 +256,7 @@ fn emit_net_owner() -> void {
# with NO foreign host at all (the Ludic-native default). Datagram-preserving: # with NO foreign host at all (the Ludic-native default). Datagram-preserving:
# one message per poll, matching how replication/RPC frame. Emitted only when a # one message per poll, matching how replication/RPC frame. Emitted only when a
# program actually calls net_send/net_poll without an extern override. # program actually calls net_send/net_poll without an extern override.
fn emit_loopback() -> void { function emit_loopback() -> void {
emith("@L_netq = internal global [64 x [2048 x i8]] zeroinitializer\n") emith("@L_netq = internal global [64 x [2048 x i8]] zeroinitializer\n")
emith("@L_netlen = internal global [64 x i32] zeroinitializer\n") emith("@L_netlen = internal global [64 x i32] zeroinitializer\n")
emith("@L_nethead = internal global i32 0\n") emith("@L_nethead = internal global i32 0\n")
@ -294,24 +294,24 @@ fn emit_loopback() -> void {
# re-emits each into the ordinary @ev_<E> dispatch on the far side. Reuses the # re-emits each into the ordinary @ev_<E> dispatch on the far side. Reuses the
# EV0 payload (already flat) and the transport seam — no new concept. # EV0 payload (already flat) and the transport seam — no new concept.
fn net_has_remote() -> bool { function net_has_remote() -> bool {
var i = 0 var i = 0
while i < len(g_events) { if (g_events[i].ty != null) { return true }; i = i + 1 } while i < len(g_events) { if (g_events[i].ty != null) { return true }; i = i + 1 }
return false return false
} }
# stable wire id for an event = its index in g_events (same program both peers) # stable wire id for an event = its index in g_events (same program both peers)
fn net_event_id(name: ptr) -> int { function net_event_id(name: ptr) -> int {
var i = 0 var i = 0
while i < len(g_events) { if (g_events[i].s == name) { return i }; i = i + 1 } while i < len(g_events) { if (g_events[i].s == name) { return i }; i = i + 1 }
return 0 - 1 return 0 - 1
} }
# the transport symbols: an `extern fn` override, else the built-in loopback. # the transport symbols: an `extern fn` override, else the built-in loopback.
fn net_send_sym() -> ptr { let x = find_extern("net_send"); if (x != null) { return x.a.s }; return "L_net_send" } function net_send_sym() -> ptr { let x = find_extern("net_send"); if (x != null) { return x.a.s }; return "L_net_send" }
fn net_poll_sym() -> ptr { let x = find_extern("net_poll"); if (x != null) { return x.a.s }; return "L_net_poll" } function net_poll_sym() -> ptr { let x = find_extern("net_poll"); if (x != null) { return x.a.s }; return "L_net_poll" }
# net_pump(): poll every pending frame and re-emit it locally. The receive path # net_pump(): poll every pending frame and re-emit it locally. The receive path
# of a remote event — the runtime/game calls this each tick. # of a remote event — the runtime/game calls this each tick.
fn emit_net_pump() -> void { function emit_net_pump() -> void {
emith("@L_recvbuf = internal global [2048 x i8] zeroinitializer\n") emith("@L_recvbuf = internal global [2048 x i8] zeroinitializer\n")
if (find_extern("net_poll") == null) { g_uses_loopback = true } if (find_extern("net_poll") == null) { g_uses_loopback = true }
let psym = net_poll_sym() let psym = net_poll_sym()
@ -352,7 +352,7 @@ fn emit_net_pump() -> void {
} }
# ---- driver ------------------------------------------------------------------ # ---- driver ------------------------------------------------------------------
fn emit_net() -> void { function emit_net() -> void {
if net_has_sync() { if net_has_sync() {
net_check() net_check()
var i = 0 var i = 0

View file

@ -2,12 +2,12 @@
# push(slice, v) and len(slice). Slices are a { data, len, cap } header the # push(slice, v) and len(slice). Slices are a { data, len, cap } header the
# holder points at, so growth is visible to every holder. # holder points at, so growth is visible to every holder.
fn emit_sizeof(llt: ptr) -> ptr { function emit_sizeof(llt: ptr) -> ptr {
let p = emit_bind(`getelementptr {llt}, ptr null, i32 1`) let p = emit_bind(`getelementptr {llt}, ptr null, i32 1`)
return emit_bind(`ptrtoint ptr {p} to i64`) return emit_bind(`ptrtoint ptr {p} to i64`)
} }
fn emit_new_struct(name: ptr) -> Val { function emit_new_struct(name: ptr) -> Val {
let s = layout_node(name) # a struct or a property — same shape let s = layout_node(name) # a struct or a property — same shape
if (s == null) { perr("unknown record type in new") } if (s == null) { perr("unknown record type in new") }
let lty = layout_ty(name) let lty = layout_ty(name)
@ -29,7 +29,7 @@ fn emit_new_struct(name: ptr) -> Val {
return val(obj, name) return val(obj, name)
} }
fn emit_new_slice(ty: ptr) -> Val { function emit_new_slice(ty: ptr) -> Val {
let sz = emit_sizeof("%LSlice") let sz = emit_sizeof("%LSlice")
let h = emit_bind(`call ptr @malloc(i64 {sz})`) let h = emit_bind(`call ptr @malloc(i64 {sz})`)
let d0 = nreg(); emit(" "); emit(d0); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h); emit(", i32 0, i32 0\n") let d0 = nreg(); emit(" "); emit(d0); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h); emit(", i32 0, i32 0\n")
@ -41,14 +41,14 @@ fn emit_new_slice(ty: ptr) -> Val {
return val(h, ty) return val(h, ty)
} }
fn slice_field(h: ptr, i: int) -> ptr { function slice_field(h: ptr, i: int) -> ptr {
let r = nreg() let r = nreg()
emit(" "); emit(r); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h) emit(" "); emit(r); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h)
emit(", i32 0, i32 "); emit(itoa(i)); emit("\n") emit(", i32 0, i32 "); emit(itoa(i)); emit("\n")
return r return r
} }
fn emit_len(e: Node) -> Val { function emit_len(e: Node) -> Val {
let s = emit_expr(e.kids[0]) let s = emit_expr(e.kids[0])
if is_slice_ty(s.ty) { # a slice: read its header length if is_slice_ty(s.ty) { # a slice: read its header length
let lp = slice_field(s.code, 1) let lp = slice_field(s.code, 1)
@ -58,7 +58,7 @@ fn emit_len(e: Node) -> Val {
return val(emit_bind(`trunc i64 {r} to i32`), "int") return val(emit_bind(`trunc i64 {r} to i32`), "int")
} }
fn emit_push(e: Node) -> Val { function emit_push(e: Node) -> Val {
let s = emit_expr(e.kids[0]) let s = emit_expr(e.kids[0])
let el = slice_elem(s.ty) let el = slice_elem(s.ty)
let elt = llty(el) let elt = llty(el)

View file

@ -2,13 +2,13 @@
# components/archetypes each carries, binds the requested components, and runs # components/archetypes each carries, binds the requested components, and runs
# the body once per match. Mirrors ll_query in compiler/back/ir_ecs.c. # the body once per match. Mirrors ll_query in compiler/back/ir_ecs.c.
fn find_arch_id(name: ptr) -> int { function find_arch_id(name: ptr) -> int {
var i = 0; var n = 1 var i = 0; var n = 1
while i < len(prog) { if prog[i].kind == N_ARCH { if (prog[i].s == name) { return n }; n = n + 1 }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_ARCH { if (prog[i].s == name) { return n }; n = n + 1 }; i = i + 1 }
return 0 return 0
} }
fn emit_query(st: Node) -> void { function emit_query(st: Node) -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let ip = emit_alloca("i32") let ip = emit_alloca("i32")
store_at("i32", "0", ip) store_at("i32", "0", ip)

View file

@ -16,7 +16,7 @@ var g_iok: int = 0
var g_snap_mode: ptr = null # "file" | "save" | "load" | "size" var g_snap_mode: ptr = null # "file" | "save" | "load" | "size"
var g_off: ptr = null # current byte-offset register, buffer modes var g_off: ptr = null # current byte-offset register, buffer modes
fn emit_io(fn2: ptr, p: ptr, bytes: ptr) -> void { function emit_io(fn2: ptr, p: ptr, bytes: ptr) -> void {
if (g_snap_mode == "file") { if (g_snap_mode == "file") {
let r = `%io{itoa(g_iok)}`; g_iok = g_iok + 1 let r = `%io{itoa(g_iok)}`; g_iok = g_iok + 1
emit(" "); emit(r); emit(" = call i64 @"); emit(fn2); emit("(ptr "); emit(p); emit(", i64 1, i64 "); emit(bytes); emit(", ptr %f)\n") emit(" "); emit(r); emit(" = call i64 @"); emit(fn2); emit("(ptr "); emit(p); emit(", i64 1, i64 "); emit(bytes); emit(", ptr %f)\n")
@ -43,7 +43,7 @@ fn emit_io(fn2: ptr, p: ptr, bytes: ptr) -> void {
g_iok = g_iok + 1 g_iok = g_iok + 1
} }
fn emit_snapshot_blocks(fn2: ptr) -> void { function emit_snapshot_blocks(fn2: ptr) -> void {
g_iok = 0 g_iok = 0
g_off = "0" g_off = "0"
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
@ -73,7 +73,7 @@ fn emit_snapshot_blocks(fn2: ptr) -> void {
} }
} }
fn emit_snapshot() -> void { function emit_snapshot() -> void {
g_snap_mode = "file" # seed (module ptr inits are null) g_snap_mode = "file" # seed (module ptr inits are null)
emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n") emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n")
emith("@.sav_wb = private unnamed_addr constant [3 x i8] c\"wb\\00\"\n") emith("@.sav_wb = private unnamed_addr constant [3 x i8] c\"wb\\00\"\n")

View file

@ -1,7 +1,7 @@
# emit_spawn.ludic — spawn / despawn / self(), and seeding a component's fields # emit_spawn.ludic — spawn / despawn / self(), and seeding a component's fields
# from its declared defaults plus any per-spawn overrides. # from its declared defaults plus any per-spawn overrides.
fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void { function emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let c = find_comp(comp) let c = find_comp(comp)
if (c == null) { perr(`spawn: unknown property {comp}`) } if (c == null) { perr(`spawn: unknown property {comp}`) }
@ -54,7 +54,7 @@ fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
# bind each of a model's properties to entity `e`'s component storage, so an # bind each of a model's properties to entity `e`'s component storage, so an
# @OnSpawn hook body can address them by name (like a query binding for one entity). # @OnSpawn hook body can address them by name (like a query binding for one entity).
fn emit_bind_props(model: Node, e: ptr) -> void { function emit_bind_props(model: Node, e: ptr) -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
var c = 0 var c = 0
while c < len(model.kids) { while c < len(model.kids) {
@ -68,7 +68,7 @@ fn emit_bind_props(model: Node, e: ptr) -> void {
} }
} }
fn emit_spawn(st: Node) -> ptr { function emit_spawn(st: Node) -> ptr {
let e = emit_bind("call i32 @L_alloc()") let e = emit_bind("call i32 @L_alloc()")
let ak = find_arch_id(st.s) let ak = find_arch_id(st.s)
if ak > 0 { if ak > 0 {
@ -103,7 +103,7 @@ fn emit_spawn(st: Node) -> ptr {
return e # the new entity id (for ludic_spawn_<M>) return e # the new entity id (for ludic_spawn_<M>)
} }
fn emit_despawn(st: Node) -> void { function emit_despawn(st: Node) -> void {
let v = emit_expr(st.a) let v = emit_expr(st.a)
# @OnDespawn: dispatch on the entity's kind and run the matching model's hook # @OnDespawn: dispatch on the entity's kind and run the matching model's hook
if len(g_ondespawn) > 0 { if len(g_ondespawn) > 0 {
@ -131,7 +131,7 @@ fn emit_despawn(st: Node) -> void {
# (not a toggle): seeds the property's fields and fires @OnAttach, but only on a # (not a toggle): seeds the property's fields and fires @OnAttach, but only on a
# real transition — if the entity already has the property it is a no-op, so the # real transition — if the entity already has the property it is a no-op, so the
# hook fires once per genuine attach (flecs/Bevy "real add" semantics). # hook fires once per genuine attach (flecs/Bevy "real add" semantics).
fn emit_attach(st: Node) -> void { function emit_attach(st: Node) -> void {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
@ -149,7 +149,7 @@ fn emit_attach(st: Node) -> void {
# property bound by name (its data still lives in @S_ storage, so the teardown # property bound by name (its data still lives in @S_ storage, so the teardown
# body reads the outgoing value), then clears the has-flag so queries skip it. # body reads the outgoing value), then clears the has-flag so queries skip it.
# Only fires on a real transition; detaching an absent property is a no-op. # Only fires on a real transition; detaching an absent property is a no-op.
fn emit_detach(st: Node) -> void { function emit_detach(st: Node) -> void {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
@ -178,7 +178,7 @@ fn emit_detach(st: Node) -> void {
# enable/disable. `<P> on <e>` toggles a property's has-flag on an entity (its # enable/disable. `<P> on <e>` toggles a property's has-flag on an entity (its
# data persists, so re-enabling restores it, and queries already skip a cleared # data persists, so re-enabling restores it, and queries already skip a cleared
# flag). A bare `<Model>` / `<Handler>` flips a global enabled flag. # flag). A bare `<Model>` / `<Handler>` flips a global enabled flag.
fn emit_toggle(st: Node) -> void { function emit_toggle(st: Node) -> void {
var val = "0"; if st.ival == 1 { val = "1" } var val = "0"; if st.ival == 1 { val = "1" }
if (st.ty != null) and (st.ty == "layer") { # enable/disable layer L if (st.ty != null) and (st.ty == "layer") { # enable/disable layer L
emit(" store i32 "); emit(val); emit(", ptr @LE_"); emit(st.s); emit("\n") emit(" store i32 "); emit(val); emit(", ptr @LE_"); emit(st.s); emit("\n")

View file

@ -1,7 +1,7 @@
# emit_stmt.ludic — lower statements. Terminators set g_term so the rest of a # emit_stmt.ludic — lower statements. Terminators set g_term so the rest of a
# block is skipped until a new basic block opens. # block is skipped until a new basic block opens.
fn emit_block(b: Node) -> void { function emit_block(b: Node) -> void {
var i = 0 var i = 0
while i < len(b.kids) { while i < len(b.kids) {
if g_term { return } if g_term { return }
@ -11,11 +11,11 @@ fn emit_block(b: Node) -> void {
} }
# store `val` (llvm type `lt`) into address `addr` # store `val` (llvm type `lt`) into address `addr`
fn store_at(lt: ptr, v: ptr, addr: ptr) -> void { function store_at(lt: ptr, v: ptr, addr: ptr) -> void {
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n") emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
} }
fn emit_assign(st: Node) -> void { function emit_assign(st: Node) -> void {
# resolve the target's address and type # resolve the target's address and type
let t = st.a let t = st.a
var addr = "0" var addr = "0"
@ -53,7 +53,7 @@ fn emit_assign(st: Node) -> void {
store_at(lt, v, addr) store_at(lt, v, addr)
} }
fn emit_if(st: Node) -> void { function emit_if(st: Node) -> void {
let c = emit_expr(st.a) let c = emit_expr(st.a)
let cc = emit_bind(`icmp ne i32 {c.code}, 0`) let cc = emit_bind(`icmp ne i32 {c.code}, 0`)
let has_else = (st.c != null) let has_else = (st.c != null)
@ -72,7 +72,7 @@ fn emit_if(st: Node) -> void {
emit(en); emit(":\n"); g_term = false emit(en); emit(":\n"); g_term = false
} }
fn emit_while(st: Node) -> void { function emit_while(st: Node) -> void {
let cl = lbl("wcond"); let bl = lbl("wbody"); let en = lbl("wend") let cl = lbl("wcond"); let bl = lbl("wbody"); let en = lbl("wend")
emit(" br label %"); emit(cl); emit("\n") emit(" br label %"); emit(cl); emit("\n")
emit(cl); emit(":\n") emit(cl); emit(":\n")
@ -87,7 +87,7 @@ fn emit_while(st: Node) -> void {
emit(en); emit(":\n"); g_term = false emit(en); emit(":\n"); g_term = false
} }
fn emit_for(st: Node) -> void { function emit_for(st: Node) -> void {
let slot = emit_alloca("i32") let slot = emit_alloca("i32")
let lo = emit_expr(st.a) let lo = emit_expr(st.a)
store_at("i32", lo.code, slot) store_at("i32", lo.code, slot)
@ -112,7 +112,7 @@ fn emit_for(st: Node) -> void {
emit(en); emit(":\n"); g_term = false emit(en); emit(":\n"); g_term = false
} }
fn emit_return(st: Node) -> void { function emit_return(st: Node) -> void {
if (st.a != null) { if (st.a != null) {
let v = emit_expr(st.a) let v = emit_expr(st.a)
store_at(llty(ret_ty), coerce_code(v, ret_ty), "%retval") store_at(llty(ret_ty), coerce_code(v, ret_ty), "%retval")
@ -121,13 +121,13 @@ fn emit_return(st: Node) -> void {
g_term = true g_term = true
} }
fn arm_is_default(arm: Node) -> bool { function arm_is_default(arm: Node) -> bool {
var p = 0 var p = 0
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and (arm.kids[p].s == "_") { return true }; p = p + 1 } while p < len(arm.kids) { if arm.kids[p].kind == E_ID and (arm.kids[p].s == "_") { return true }; p = p + 1 }
return false return false
} }
fn emit_match(st: Node) -> void { function emit_match(st: Node) -> void {
let sv = emit_expr(st.a) let sv = emit_expr(st.a)
let endl = lbl("mend") let endl = lbl("mend")
var deflt: Node = null var deflt: Node = null
@ -163,7 +163,7 @@ fn emit_match(st: Node) -> void {
# emit E(field: v, ...) — evaluate the payload args in the event's declared field # emit E(field: v, ...) — evaluate the payload args in the event's declared field
# order (so call args line up with the `@ev_<E>` signature), then a direct call. # order (so call args line up with the `@ev_<E>` signature), then a direct call.
# A missing arg falls back to the field's default; an unset scalar/ptr to 0/null. # A missing arg falls back to the field's default; an unset scalar/ptr to 0/null.
fn emit_emit(st: Node) -> Val { function emit_emit(st: Node) -> Val {
let ev = find_event(st.s) let ev = find_event(st.s)
if (ev == null) { perr(`emit: unknown event {st.s}`) } if (ev == null) { perr(`emit: unknown event {st.s}`) }
# evaluate each payload field in declared order (default for a missing arg) # evaluate each payload field in declared order (default for a missing arg)
@ -221,7 +221,7 @@ fn emit_emit(st: Node) -> Val {
return val("0", "int") return val("0", "int")
} }
fn emit_stmt(st: Node) -> void { function emit_stmt(st: Node) -> void {
if st.kind == S_LET { if st.kind == S_LET {
var ty = st.ty var ty = st.ty
if (ty == null) { let v0 = emit_expr(st.a); ty = v0.ty if (ty == null) { let v0 = emit_expr(st.a); ty = v0.ty
@ -261,9 +261,9 @@ fn emit_stmt(st: Node) -> void {
perr("cannot emit statement") perr("cannot emit statement")
} }
fn loop_push(cont: ptr, brk: ptr) -> void { function loop_push(cont: ptr, brk: ptr) -> void {
if nloop < len(cnt_lbl) { cnt_lbl[nloop] = cont; brk_lbl[nloop] = brk } if nloop < len(cnt_lbl) { cnt_lbl[nloop] = cont; brk_lbl[nloop] = brk }
else { push(cnt_lbl, cont); push(brk_lbl, brk) } else { push(cnt_lbl, cont); push(brk_lbl, brk) }
nloop = nloop + 1 nloop = nloop + 1
} }
fn loop_pop() -> void { nloop = nloop - 1 } function loop_pop() -> void { nloop = nloop - 1 }

View file

@ -3,7 +3,7 @@
# contains/index_of/to_int) allocate nothing; slice/from_int/equals/concat reuse # contains/index_of/to_int) allocate nothing; slice/from_int/equals/concat reuse
# the string preludes that the `+`, `s[a..b]` and str(int) operators emit. # the string preludes that the `+`, `s[a..b]` and str(int) operators emit.
fn is_text_ns(meth: ptr) -> bool { function is_text_ns(meth: ptr) -> bool {
if (meth == "length") or (meth == "char_at") or (meth == "slice") { return true } if (meth == "length") or (meth == "char_at") or (meth == "slice") { return true }
if (meth == "equals") or (meth == "concat") or (meth == "to_int") or (meth == "from_int") { return true } if (meth == "equals") or (meth == "concat") or (meth == "to_int") or (meth == "from_int") { return true }
if (meth == "starts_with") or (meth == "ends_with") { return true } if (meth == "starts_with") or (meth == "ends_with") { return true }
@ -14,7 +14,7 @@ fn is_text_ns(meth: ptr) -> bool {
return false return false
} }
fn emit_text_ns(meth: ptr, e: Node) -> Val { function emit_text_ns(meth: ptr, e: Node) -> Val {
if (meth == "from_int") { # int -> str, same as str(n) if (meth == "from_int") { # int -> str, same as str(n)
let n = emit_expr(e.kids[0]) let n = emit_expr(e.kids[0])
g_uses_intstr = true g_uses_intstr = true
@ -138,7 +138,7 @@ fn emit_text_ns(meth: ptr, e: Node) -> Val {
# emit_text_prelude — string builders that allocate: upper/lower/trim/ # emit_text_prelude — string builders that allocate: upper/lower/trim/
# repeat/pad. Emitted once per program that uses them (g_uses_textrt). Plain # repeat/pad. Emitted once per program that uses them (g_uses_textrt). Plain
# libc (strlen/malloc/memcpy), deterministic, C-string in and out. # libc (strlen/malloc/memcpy), deterministic, C-string in and out.
fn emit_text_prelude() -> void { function emit_text_prelude() -> void {
emith("define ptr @fn_str_upper(ptr %s) {\n") emith("define ptr @fn_str_upper(ptr %s) {\n")
emith("entry:\n") emith("entry:\n")
emith(" %n = call i64 @strlen(ptr %s)\n") emith(" %n = call i64 @strlen(ptr %s)\n")
@ -343,7 +343,7 @@ fn emit_text_prelude() -> void {
# replace, join (over a []str), split (returns a []str). Emitted once per # replace, join (over a []str), split (returns a []str). Emitted once per
# program that uses them (g_uses_textrt2). Slices are the {data,len,cap} # program that uses them (g_uses_textrt2). Slices are the {data,len,cap}
# %LSlice with str (ptr) elements. # %LSlice with str (ptr) elements.
fn emit_text2_prelude() -> void { function emit_text2_prelude() -> void {
emith("define ptr @fn_str_replace(ptr %s, ptr %from, ptr %to) {\n") emith("define ptr @fn_str_replace(ptr %s, ptr %from, ptr %to) {\n")
emith("entry:\n") emith("entry:\n")
emith(" %lf = call i64 @strlen(ptr %from)\n") emith(" %lf = call i64 @strlen(ptr %from)\n")

View file

@ -3,12 +3,12 @@
# the wall clock and is explicitly non-deterministic. The frame clock ticks at a # the wall clock and is explicitly non-deterministic. The frame clock ticks at a
# fixed 60 per second, so delta is the constant 1/60 s = 1092 in Q16.16. # fixed 60 per second, so delta is the constant 1/60 s = 1092 in Q16.16.
fn is_time_ns(meth: ptr) -> bool { function is_time_ns(meth: ptr) -> bool {
if (meth == "frame") or (meth == "delta") or (meth == "elapsed") or (meth == "now") { return true } if (meth == "frame") or (meth == "delta") or (meth == "elapsed") or (meth == "now") { return true }
return false return false
} }
fn emit_time_ns(meth: ptr, e: Node) -> Val { function emit_time_ns(meth: ptr, e: Node) -> Val {
if (meth == "frame") { # completed frames since start if (meth == "frame") { # completed frames since start
return val(emit_bind("load i32, ptr @L_frame"), "int") return val(emit_bind("load i32, ptr @L_frame"), "int")
} }

View file

@ -7,25 +7,25 @@ var uiw: []Node # flattened widgets, pre-order
var uiw_parent: []int var uiw_parent: []int
var ui_roots: []int # first-widget index of each ui block var ui_roots: []int # first-widget index of each ui block
fn ui_wtype(w: Node) -> int { function ui_wtype(w: Node) -> int {
let s = w.s let s = w.s
if (s == "panel") { return 0 }; if (s == "col") { return 1 }; if (s == "row") { return 2 } if (s == "panel") { return 0 }; if (s == "col") { return 1 }; if (s == "row") { return 2 }
if (s == "label") { return 3 }; if (s == "button") { return 4 } if (s == "label") { return 3 }; if (s == "button") { return 4 }
if (s == "image") { return 5 }; if (s == "spacer") { return 6 } if (s == "image") { return 5 }; if (s == "spacer") { return 6 }
return 0 return 0
} }
fn ui_prop(w: Node, key: ptr) -> Node { function ui_prop(w: Node, key: ptr) -> Node {
var i = 0 var i = 0
while i < len(w.b.kids) { if (w.b.kids[i].s == key) { return w.b.kids[i].a }; i = i + 1 } while i < len(w.b.kids) { if (w.b.kids[i].s == key) { return w.b.kids[i].a }; i = i + 1 }
return null return null
} }
fn ui_flatten(w: Node, parent: int) -> void { function ui_flatten(w: Node, parent: int) -> void {
let idx = len(uiw) let idx = len(uiw)
push(uiw, w); push(uiw_parent, parent) push(uiw, w); push(uiw_parent, parent)
var i = 0 var i = 0
while i < len(w.kids) { ui_flatten(w.kids[i], idx); i = i + 1 } while i < len(w.kids) { ui_flatten(w.kids[i], idx); i = i + 1 }
} }
fn ui_flatten_all() -> void { function ui_flatten_all() -> void {
uiw = new []Node; uiw_parent = new []int; ui_roots = new []int uiw = new []Node; uiw_parent = new []int; ui_roots = new []int
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
@ -33,14 +33,14 @@ fn ui_flatten_all() -> void {
i = i + 1 i = i + 1
} }
} }
fn has_ui() -> bool { function has_ui() -> bool {
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_UI and prog[i].ival == 1 { return true }; i = i + 1 } while i < len(prog) { if prog[i].kind == N_UI and prog[i].ival == 1 { return true }; i = i + 1 }
return false return false
} }
# index of a UI_<name>: a ui block's root, or a widget's id= # index of a UI_<name>: a ui block's root, or a widget's id=
fn ui_index_of(nm: ptr) -> int { function ui_index_of(nm: ptr) -> int {
let s = nm[3..len(nm)] # strip "UI_" let s = nm[3..len(nm)] # strip "UI_"
let u = 0; var bi = 0 let u = 0; var bi = 0
var i = 0 var i = 0
@ -56,14 +56,14 @@ fn ui_index_of(nm: ptr) -> int {
} }
return 0 return 0
} }
fn is_ui_ident(nm: ptr) -> bool { function is_ui_ident(nm: ptr) -> bool {
return len(nm) > 3 and nm[0] == 85 and nm[1] == 73 and nm[2] == 95 # "UI_" return len(nm) > 3 and nm[0] == 85 and nm[1] == 73 and nm[2] == 95 # "UI_"
} }
fn ll_ui_set(idx: int, key: int, val: ptr) -> void { function ll_ui_set(idx: int, key: int, val: ptr) -> void {
emit(" call void @fn_rt_ui_set(i32 "); emit(itoa(idx)); emit(", i32 "); emit(itoa(key)); emit(", i32 "); emit(val); emit(")\n") emit(" call void @fn_rt_ui_set(i32 "); emit(itoa(idx)); emit(", i32 "); emit(itoa(key)); emit(", i32 "); emit(val); emit(")\n")
} }
fn ui_prop_key(k: ptr) -> int { function ui_prop_key(k: ptr) -> int {
if (k == "w") { return 2 }; if (k == "h") { return 3 }; if (k == "x") { return 4 }; if (k == "y") { return 5 } if (k == "w") { return 2 }; if (k == "h") { return 3 }; if (k == "x") { return 4 }; if (k == "y") { return 5 }
if (k == "pad") { return 7 }; if (k == "gap") { return 8 }; if (k == "bg") { return 9 }; if (k == "fg") { return 10 } if (k == "pad") { return 7 }; if (k == "gap") { return 8 }; if (k == "bg") { return 9 }; if (k == "fg") { return 10 }
if (k == "border") { return 11 }; if (k == "grow") { return 13 }; if (k == "font") { return 14 }; if (k == "size") { return 15 } if (k == "border") { return 11 }; if (k == "grow") { return 13 }; if (k == "font") { return 14 }; if (k == "size") { return 15 }
@ -71,7 +71,7 @@ fn ui_prop_key(k: ptr) -> int {
return 0 - 1 return 0 - 1
} }
fn emit_ui_build() -> void { function emit_ui_build() -> void {
ui_flatten_all() ui_flatten_all()
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0 ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
let fbody = buf_new() let fbody = buf_new()

View file

@ -1,7 +1,7 @@
# io.ludic — reading the input file and writing the output, plus tiny stdio. # io.ludic — reading the input file and writing the output, plus tiny stdio.
# The compiler reads one .ludic file whole and emits LLVM IR text to stdout. # The compiler reads one .ludic file whole and emits LLVM IR text to stdout.
fn read_file(path: str) -> ptr { function read_file(path: str) -> ptr {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if (f == null) { return null } if (f == null) { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)
@ -15,7 +15,7 @@ fn read_file(path: str) -> ptr {
} }
# length of a NUL-terminated buffer # length of a NUL-terminated buffer
fn cstr_len(s: ptr) -> int { function cstr_len(s: ptr) -> int {
var n = 0 var n = 0
while s[n] != 0 { n = n + 1 } while s[n] != 0 { n = n + 1 }
return n return n

View file

@ -15,18 +15,18 @@ property Tok { kind: int = 0, text: ptr = null, ival: int = 0, line: int = 0 }
var toks: []Tok var toks: []Tok
fn tok_push(kind: int, text: ptr, ival: int, line: int) -> void { function tok_push(kind: int, text: ptr, ival: int, line: int) -> void {
let t = new Tok let t = new Tok
t.kind = kind; t.text = text; t.ival = ival; t.line = line t.kind = kind; t.text = text; t.ival = ival; t.line = line
push(toks, t) push(toks, t)
} }
# does src match the 2-char operator op at position i? # does src match the 2-char operator op at position i?
fn two_at(src: ptr, i: int, a: int, b: int) -> bool { function two_at(src: ptr, i: int, a: int, b: int) -> bool {
return src[i] == a and src[i + 1] == b return src[i] == a and src[i + 1] == b
} }
fn is_op1(c: int) -> bool { function is_op1(c: int) -> bool {
# + - * / % < > = ( ) { } [ ] , : . ! @ # + - * / % < > = ( ) { } [ ] , : . ! @
if c == 43 or c == 45 or c == 42 or c == 47 or c == 37 { return true } if c == 43 or c == 45 or c == 42 or c == 47 or c == 37 { return true }
if c == 60 or c == 62 or c == 61 { return true } if c == 60 or c == 62 or c == 61 { return true }
@ -37,7 +37,7 @@ fn is_op1(c: int) -> bool {
return false return false
} }
fn lex(src: ptr) -> void { function lex(src: ptr) -> void {
toks = new []Tok toks = new []Tok
var i = 0 var i = 0
var line = 1 var line = 1

View file

@ -301,7 +301,7 @@ declare void @win_close()
@.str200 = private unnamed_addr constant [3 x i8] c"ui\00" @.str200 = private unnamed_addr constant [3 x i8] c"ui\00"
@.str201 = private unnamed_addr constant [4 x i8] c"var\00" @.str201 = private unnamed_addr constant [4 x i8] c"var\00"
@.str202 = private unnamed_addr constant [6 x i8] c"const\00" @.str202 = private unnamed_addr constant [6 x i8] c"const\00"
@.str203 = private unnamed_addr constant [3 x i8] c"fn\00" @.str203 = private unnamed_addr constant [9 x i8] c"function\00"
@.str204 = private unnamed_addr constant [7 x i8] c"extern\00" @.str204 = private unnamed_addr constant [7 x i8] c"extern\00"
@.str205 = private unnamed_addr constant [6 x i8] c"entry\00" @.str205 = private unnamed_addr constant [6 x i8] c"entry\00"
@.str206 = private unnamed_addr constant [21 x i8] c"expected declaration\00" @.str206 = private unnamed_addr constant [21 x i8] c"expected declaration\00"

View file

@ -15,7 +15,7 @@
# reseed.sh rely on, so the bootstrap is untouched. # reseed.sh rely on, so the bootstrap is untouched.
# basename: the part of a path after the last '/'. # basename: the part of a path after the last '/'.
fn base_name(path: ptr) -> ptr { function base_name(path: ptr) -> ptr {
var last = 0 - 1 var last = 0 - 1
var i = 0 var i = 0
while path[i] != 0 { if path[i] == 47 { last = i }; i = i + 1 } while path[i] != 0 { if path[i] == 47 { last = i }; i = i + 1 }
@ -23,7 +23,7 @@ fn base_name(path: ptr) -> ptr {
} }
# drop a trailing ".ludic" if present # drop a trailing ".ludic" if present
fn strip_ludic(name: ptr) -> ptr { function strip_ludic(name: ptr) -> ptr {
let n = len(name) let n = len(name)
if n > 6 { if n > 6 {
if (name[n - 6..n] == ".ludic") { return name[0..n - 6] } if (name[n - 6..n] == ".ludic") { return name[0..n - 6] }
@ -32,21 +32,21 @@ fn strip_ludic(name: ptr) -> ptr {
} }
# env var with a fallback when unset # env var with a fallback when unset
fn getenv_or(name: ptr, dflt: ptr) -> ptr { function getenv_or(name: ptr, dflt: ptr) -> ptr {
let v = getenv(name) let v = getenv(name)
if (v == null) { return dflt } if (v == null) { return dflt }
return v return v
} }
# guarantee a directory string ends in '/' so path_join concatenates cleanly # guarantee a directory string ends in '/' so path_join concatenates cleanly
fn ensure_slash(d: ptr) -> ptr { function ensure_slash(d: ptr) -> ptr {
let n = len(d) let n = len(d)
if n == 0 { return d } if n == 0 { return d }
if d[n - 1] == 47 { return d } if d[n - 1] == 47 { return d }
return (d + ("/")) return (d + ("/"))
} }
fn die(msg: ptr) -> void { function die(msg: ptr) -> void {
file_write(file_stderr(), msg, len(msg)) file_write(file_stderr(), msg, len(msg))
exit(1) exit(1)
} }

View file

@ -6,13 +6,13 @@ var pi: int = 0
var prog: []Node # the top-level declarations var prog: []Node # the top-level declarations
var g_game_name: ptr # the `game`/`module` name var g_game_name: ptr # the `game`/`module` name
fn cur() -> Tok { return toks[pi] } function cur() -> Tok { return toks[pi] }
fn pk(o: int) -> Tok { return toks[pi + o] } function pk(o: int) -> Tok { return toks[pi + o] }
fn is_op(v: ptr) -> bool { let t = toks[pi]; return t.kind == TK_OP and (t.text == v) } function is_op(v: ptr) -> bool { let t = toks[pi]; return t.kind == TK_OP and (t.text == v) }
fn is_id(v: ptr) -> bool { let t = toks[pi]; return t.kind == TK_ID and (t.text == v) } function is_id(v: ptr) -> bool { let t = toks[pi]; return t.kind == TK_ID and (t.text == v) }
fn is_kw(v: ptr) -> bool { return is_id(v) } function is_kw(v: ptr) -> bool { return is_id(v) }
fn perr(msg: ptr) -> void { function perr(msg: ptr) -> void {
let e = file_stderr() let e = file_stderr()
file_write(e, "ludicc(self): parse error: ", 27) file_write(e, "ludicc(self): parse error: ", 27)
file_write(e, msg, len(msg)) file_write(e, msg, len(msg))
@ -20,17 +20,17 @@ fn perr(msg: ptr) -> void {
exit(1) exit(1)
} }
fn eat_op(v: ptr) -> void { if not is_op(v) { perr(v) }; pi = pi + 1 } function eat_op(v: ptr) -> void { if not is_op(v) { perr(v) }; pi = pi + 1 }
fn eat_id() -> ptr { function eat_id() -> ptr {
let t = toks[pi] let t = toks[pi]
if t.kind != TK_ID { perr("expected identifier") } if t.kind != TK_ID { perr("expected identifier") }
pi = pi + 1 pi = pi + 1
return t.text return t.text
} }
fn skipnl() -> void { while toks[pi].kind == TK_NL { pi = pi + 1 } } function skipnl() -> void { while toks[pi].kind == TK_NL { pi = pi + 1 } }
# a type: `[]T` slice, or a plain name (int/ptr/str/bool/struct) # a type: `[]T` slice, or a plain name (int/ptr/str/bool/struct)
fn ptype() -> ptr { function ptype() -> ptr {
if is_op("[") { if is_op("[") {
pi = pi + 1 pi = pi + 1
eat_op("]") eat_op("]")
@ -46,13 +46,13 @@ fn ptype() -> ptr {
} }
# ---- expressions ----------------------------------------------------------- # ---- expressions -----------------------------------------------------------
fn expr() -> Node { return p_or() } function expr() -> Node { return p_or() }
# Call arguments. Each argument is either positional (`expr`) or named # Call arguments. Each argument is either positional (`expr`) or named
# (`name: expr`) — a named argument is an identifier immediately followed by a # (`name: expr`) — a named argument is an identifier immediately followed by a
# colon, which is unambiguous inside a call. Named args are stored as E_FINIT # colon, which is unambiguous inside a call. Named args are stored as E_FINIT
# (s=label, a=value) and reordered to the callee's parameter order at emit time. # (s=label, a=value) and reordered to the callee's parameter order at emit time.
fn args_call(call: Node) -> void { function args_call(call: Node) -> void {
eat_op("("); skipnl() eat_op("("); skipnl()
while not is_op(")") { while not is_op(")") {
let t = toks[pi] let t = toks[pi]
@ -70,22 +70,22 @@ fn args_call(call: Node) -> void {
# ---- string interpolation -------------------------------------------------- # ---- string interpolation --------------------------------------------------
# `text {expr} text` desugars to a `+` chain of string literals and `str(expr)` # `text {expr} text` desugars to a `+` chain of string literals and `str(expr)`
# holes, so it reuses the string-concat operator and needs no new runtime. # holes, so it reuses the string-concat operator and needs no new runtime.
fn interp_lit(buf: ptr, len: int) -> Node { let n = node(E_STR); n.s = buf[0..0 + len]; return n } function interp_lit(buf: ptr, len: int) -> Node { let n = node(E_STR); n.s = buf[0..0 + len]; return n }
fn interp_add(acc: Node, part: Node) -> Node { function interp_add(acc: Node, part: Node) -> Node {
if acc == null { return part } if acc == null { return part }
return mkbin("+", acc, part) return mkbin("+", acc, part)
} }
fn interp_str(e: Node) -> Node { # wrap a hole in str(...) function interp_str(e: Node) -> Node { # wrap a hole in str(...)
let c = node(E_CALL); let id = node(E_ID); id.s = "str"; c.a = id; push(c.kids, e); return c let c = node(E_CALL); let id = node(E_ID); id.s = "str"; c.a = id; push(c.kids, e); return c
} }
fn parse_hole(inner: ptr) -> Node { # re-lex+parse an embedded expression function parse_hole(inner: ptr) -> Node { # re-lex+parse an embedded expression
let saved_toks = toks; let saved_pi = pi let saved_toks = toks; let saved_pi = pi
lex(inner); pi = 0; skipnl() lex(inner); pi = 0; skipnl()
let e = expr() let e = expr()
toks = saved_toks; pi = saved_pi toks = saved_toks; pi = saved_pi
return e return e
} }
fn parse_interp(raw: ptr) -> Node { function parse_interp(raw: ptr) -> Node {
let n = len(raw) let n = len(raw)
var acc: Node = null var acc: Node = null
let lit = bytes(n + 1) let lit = bytes(n + 1)
@ -125,7 +125,7 @@ fn parse_interp(raw: ptr) -> Node {
# emit E(field: v, ...) — shared by the statement form and the expression form. # emit E(field: v, ...) — shared by the statement form and the expression form.
# As an expression it yields a cancellable event's cancelled flag (0/1); a # As an expression it yields a cancellable event's cancelled flag (0/1); a
# non-cancellable event yields 0. # non-cancellable event yields 0.
fn parse_emit() -> Node { function parse_emit() -> Node {
pi = pi + 1; let n = node(S_EMIT); n.s = eat_id() pi = pi + 1; let n = node(S_EMIT); n.s = eat_id()
let r = node(E_REC) let r = node(E_REC)
eat_op("("); skipnl() eat_op("("); skipnl()
@ -138,7 +138,7 @@ fn parse_emit() -> Node {
return n return n
} }
fn p_primary() -> Node { function p_primary() -> Node {
let t = toks[pi] let t = toks[pi]
if t.kind == TK_INTERP { pi = pi + 1; return parse_interp(t.text) } if t.kind == TK_INTERP { pi = pi + 1; return parse_interp(t.text) }
if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() } if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
@ -157,7 +157,7 @@ fn p_primary() -> Node {
return node(E_INT) return node(E_INT)
} }
fn p_postfix() -> Node { function p_postfix() -> Node {
var e = p_primary() var e = p_primary()
while true { while true {
if is_op(".") { pi = pi + 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m } if is_op(".") { pi = pi + 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m }
@ -169,42 +169,42 @@ fn p_postfix() -> Node {
return e return e
} }
fn p_unary() -> Node { function p_unary() -> Node {
if is_op("-") { pi = pi + 1; let n = node(E_UN); n.s = "-"; n.a = p_unary(); return n } if is_op("-") { pi = pi + 1; let n = node(E_UN); n.s = "-"; n.a = p_unary(); return n }
if is_op("~") { pi = pi + 1; let n = node(E_UN); n.s = "~"; n.a = p_unary(); return n } # bitwise not if is_op("~") { pi = pi + 1; let n = node(E_UN); n.s = "~"; n.a = p_unary(); return n } # bitwise not
if is_id("not") { pi = pi + 1; let n = node(E_UN); n.s = "not"; n.a = p_unary(); return n } if is_id("not") { pi = pi + 1; let n = node(E_UN); n.s = "not"; n.a = p_unary(); return n }
return p_postfix() return p_postfix()
} }
fn mkbin(op: ptr, l: Node, r: Node) -> Node { let b = node(E_BIN); b.s = op; b.a = l; b.b = r; return b } function mkbin(op: ptr, l: Node, r: Node) -> Node { let b = node(E_BIN); b.s = op; b.a = l; b.b = r; return b }
# precedence (Go-style, so `flags & MASK == 0` needs no parens): shifts and `&` # precedence (Go-style, so `flags & MASK == 0` needs no parens): shifts and `&`
# bind like `*`; `|` and `^` bind like `+`; both tighter than comparison. # bind like `*`; `|` and `^` bind like `+`; both tighter than comparison.
fn p_mul() -> Node { function p_mul() -> Node {
var l = p_unary() var l = p_unary()
while is_op("*") or is_op("/") or is_op("%") or is_op("<<") or is_op(">>") or is_op("&") { while is_op("*") or is_op("/") or is_op("%") or is_op("<<") or is_op(">>") or is_op("&") {
let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_unary()) } let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_unary()) }
return l return l
} }
fn p_add() -> Node { function p_add() -> Node {
var l = p_mul() var l = p_mul()
while is_op("+") or is_op("-") or is_op("|") or is_op("^") { while is_op("+") or is_op("-") or is_op("|") or is_op("^") {
let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_mul()) } let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_mul()) }
return l return l
} }
fn p_cmp() -> Node { function p_cmp() -> Node {
var l = p_add() var l = p_add()
while is_op("<") or is_op("<=") or is_op(">") or is_op(">=") or is_op("==") or is_op("!=") { while is_op("<") or is_op("<=") or is_op(">") or is_op(">=") or is_op("==") or is_op("!=") {
let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_add()) let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_add())
} }
return l return l
} }
fn p_and() -> Node { function p_and() -> Node {
var l = p_cmp() var l = p_cmp()
while is_id("and") { pi = pi + 1; l = mkbin("and", l, p_cmp()) } while is_id("and") { pi = pi + 1; l = mkbin("and", l, p_cmp()) }
return l return l
} }
fn p_or() -> Node { function p_or() -> Node {
var l = p_and() var l = p_and()
while is_id("or") { pi = pi + 1; l = mkbin("or", l, p_and()) } while is_id("or") { pi = pi + 1; l = mkbin("or", l, p_and()) }
return l return l
@ -212,7 +212,7 @@ fn p_or() -> Node {
# a record literal `{ field: value, ... }` — used by spawn component inits. # a record literal `{ field: value, ... }` — used by spawn component inits.
# Rule A: a named part uses `:` (`=` is assignment/binding only). # Rule A: a named part uses `:` (`=` is assignment/binding only).
fn record() -> Node { function record() -> Node {
eat_op("{") eat_op("{")
let r = node(E_REC) let r = node(E_REC)
while true { skipnl(); if is_op("}") { break } while true { skipnl(); if is_op("}") { break }
@ -222,7 +222,7 @@ fn record() -> Node {
} }
# ---- statements ------------------------------------------------------------ # ---- statements ------------------------------------------------------------
fn block() -> Node { function block() -> Node {
skipnl(); eat_op("{") skipnl(); eat_op("{")
let b = node(N_BLOCK) let b = node(N_BLOCK)
while true { skipnl(); if is_op("}") { break } while true { skipnl(); if is_op("}") { break }
@ -238,7 +238,7 @@ fn block() -> Node {
return b return b
} }
fn stmt() -> Node { function stmt() -> Node {
let t = toks[pi] let t = toks[pi]
if t.kind == TK_ID { if t.kind == TK_ID {
if (t.text == "let") or (t.text == "var") { if (t.text == "let") or (t.text == "var") {
@ -333,16 +333,16 @@ fn stmt() -> Node {
} }
# ---- declarations ---------------------------------------------------------- # ---- declarations ----------------------------------------------------------
fn parse_var() -> Node { function parse_var() -> Node {
pi = pi + 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype() pi = pi + 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype()
if is_op("=") { pi = pi + 1; n.a = expr() } if is_op("=") { pi = pi + 1; n.a = expr() }
return n return n
} }
fn parse_const() -> Node { function parse_const() -> Node {
pi = pi + 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("="); n.a = expr() pi = pi + 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("="); n.a = expr()
return n return n
} }
fn parse_fn() -> Node { function parse_fn() -> Node {
pi = pi + 1; let n = node(N_FN); n.s = eat_id(); eat_op("(") pi = pi + 1; let n = node(N_FN); n.s = eat_id(); eat_op("(")
while not is_op(")") { while not is_op(")") {
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p) let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
@ -354,17 +354,17 @@ fn parse_fn() -> Node {
n.a = block() n.a = block()
return n return n
} }
fn parse_main() -> Node { pi = pi + 1; let n = node(N_MAIN); n.a = block(); return n } function parse_main() -> Node { pi = pi + 1; let n = node(N_MAIN); n.a = block(); return n }
# directory part of a path, including the trailing '/', or "" if none # directory part of a path, including the trailing '/', or "" if none
fn dir_of(path: ptr) -> ptr { function dir_of(path: ptr) -> ptr {
var last = 0 - 1 var last = 0 - 1
var i = 0 var i = 0
while path[i] != 0 { if path[i] == 47 { last = i }; i = i + 1 } while path[i] != 0 { if path[i] == 47 { last = i }; i = i + 1 }
if last < 0 { return "" } if last < 0 { return "" }
return path[0..0 + (last + 1)] return path[0..0 + (last + 1)]
} }
fn path_join(dir: ptr, rel: ptr) -> ptr { function path_join(dir: ptr, rel: ptr) -> ptr {
if rel[0] == 47 { return rel } # absolute if rel[0] == 47 { return rel } # absolute
return (dir + rel) return (dir + rel)
} }
@ -372,7 +372,7 @@ fn path_join(dir: ptr, rel: ptr) -> ptr {
var loaded_paths: []ptr var loaded_paths: []ptr
var cur_dir: ptr var cur_dir: ptr
fn already_loaded(full: ptr) -> bool { function already_loaded(full: ptr) -> bool {
var i = 0 var i = 0
while i < len(loaded_paths) { if (loaded_paths[i] == full) { return true }; i = i + 1 } while i < len(loaded_paths) { if (loaded_paths[i] == full) { return true }; i = i + 1 }
return false return false
@ -381,7 +381,7 @@ fn already_loaded(full: ptr) -> bool {
# parse one top-level declaration (or resolve an import) into `prog`. # parse one top-level declaration (or resolve an import) into `prog`.
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`, # Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords. # `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
fn parse_one_decl() -> void { function parse_one_decl() -> void {
var is_export = false var is_export = false
var qspec: Node = null var qspec: Node = null
var onspawn_model: ptr = null var onspawn_model: ptr = null
@ -482,14 +482,14 @@ fn parse_one_decl() -> void {
if is_id("ui") { push(prog, parse_ui()); return } if is_id("ui") { push(prog, parse_ui()); return }
if is_id("var") { push(prog, parse_var()); return } if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return } if is_id("const") { push(prog, parse_const()); return }
if is_id("fn") { let f = parse_fn(); if is_export { f.ival = 1 }; push(prog, f); return } if is_id("function") { let f = parse_fn(); if is_export { f.ival = 1 }; push(prog, f); return }
if is_id("extern") { push(prog, parse_extern()); return } if is_id("extern") { push(prog, parse_extern()); return }
if is_id("entry") { push(prog, parse_main()); return } if is_id("entry") { push(prog, parse_main()); return }
perr("expected declaration") perr("expected declaration")
} }
# lex and parse an imported fragment into `prog`, saving/restoring lexer state # lex and parse an imported fragment into `prog`, saving/restoring lexer state
fn do_import(rel: ptr) -> void { function do_import(rel: ptr) -> void {
let full = path_join(cur_dir, rel) let full = path_join(cur_dir, rel)
if already_loaded(full) { return } if already_loaded(full) { return }
push(loaded_paths, full) push(loaded_paths, full)
@ -506,7 +506,7 @@ fn do_import(rel: ptr) -> void {
# a game (has systems/components) links the Ludic runtime; auto-splice it the # a game (has systems/components) links the Ludic runtime; auto-splice it the
# way the C compiler does. Tools (a `main` block, no ECS) get nothing. # way the C compiler does. Tools (a `main` block, no ECS) get nothing.
fn maybe_splice_runtime() -> void { function maybe_splice_runtime() -> void {
if not has_ecs() { return } if not has_ecs() { return }
let saved = cur_dir let saved = cur_dir
cur_dir = "" cur_dir = ""
@ -514,7 +514,7 @@ fn maybe_splice_runtime() -> void {
cur_dir = saved cur_dir = saved
} }
fn parse_program() -> void { function parse_program() -> void {
prog = new []Node prog = new []Node
g_computed = new []Node g_computed = new []Node
g_onspawn = new []Node g_onspawn = new []Node

View file

@ -2,7 +2,7 @@
# `for (vars) in query [terms] where cond`, spawn and despawn. Mirrors the # `for (vars) in query [terms] where cond`, spawn and despawn. Mirrors the
# game-construct parsing in compiler/front/parse.c. # game-construct parsing in compiler/front/parse.c.
fn parse_component() -> Node { function parse_component() -> Node {
pi = pi + 1; let n = node(N_COMP); n.s = eat_id(); skipnl(); eat_op("{") pi = pi + 1; let n = node(N_COMP); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break } while true { skipnl(); if is_op("}") { break }
var is_computed = false var is_computed = false
@ -20,7 +20,7 @@ fn parse_component() -> Node {
# event Name { field: T = default, ... } — a public event's POD payload. Same # event Name { field: T = default, ... } — a public event's POD payload. Same
# field grammar as a `property`, but stored in g_events, not prog: an event is a # field grammar as a `property`, but stored in g_events, not prog: an event is a
# signal shape, not per-entity storage. Zero fields is allowed (`event Ping {}`). # signal shape, not per-entity storage. Zero fields is allowed (`event Ping {}`).
fn parse_event() -> Node { function parse_event() -> Node {
pi = pi + 1; let n = node(N_EVENT) pi = pi + 1; let n = node(N_EVENT)
if is_id("cancellable") { pi = pi + 1; n.ival = 1 } # a decision event: listeners may `cancel` it if is_id("cancellable") { pi = pi + 1; n.ival = 1 } # a decision event: listeners may `cancel` it
n.s = eat_id(); skipnl(); eat_op("{") n.s = eat_id(); skipnl(); eat_op("{")
@ -32,7 +32,7 @@ fn parse_event() -> Node {
eat_op("}"); return n eat_op("}"); return n
} }
fn parse_system() -> Node { function parse_system() -> Node {
pi = pi + 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update" pi = pi + 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update"
# postfix clauses on `handler Name …`: @anno(...) (parsed and reserved, e.g. # postfix clauses on `handler Name …`: @anno(...) (parsed and reserved, e.g.
# @deterministic / @Reads(...) / @Writes(...)) and `phase X`. The handler's # @deterministic / @Reads(...) / @Writes(...)) and `phase X`. The handler's
@ -53,7 +53,7 @@ fn parse_system() -> Node {
# `[Term, ...]` with optional `where <expr>`, returning a node whose kids are # `[Term, ...]` with optional `where <expr>`, returning a node whose kids are
# the terms (E_ID with ival=1 for {Tag} filters) and .a the where-expr or null. # the terms (E_ID with ival=1 for {Tag} filters) and .a the where-expr or null.
fn parse_query_tail() -> Node { function parse_query_tail() -> Node {
eat_op("[") eat_op("[")
let q = node(N_BLOCK) let q = node(N_BLOCK)
while not is_op("]") { while not is_op("]") {
@ -67,7 +67,7 @@ fn parse_query_tail() -> Node {
} }
# `for (a, b) in query [Pos, Vel] where ... { body }` # `for (a, b) in query [Pos, Vel] where ... { body }`
fn parse_query_for() -> Node { function parse_query_for() -> Node {
let n = node(S_QUERY) let n = node(S_QUERY)
eat_op("(") eat_op("(")
while not is_op(")") { let v = node(E_ID); v.s = eat_id(); push(n.kids, v); if is_op(",") { pi = pi + 1 } } while not is_op(")") { let v = node(E_ID); v.s = eat_id(); push(n.kids, v); if is_op(",") { pi = pi + 1 } }
@ -87,7 +87,7 @@ fn parse_query_for() -> Node {
# where <constraints> { body }` loop. It desugars to the same S_QUERY node, so # where <constraints> { body }` loop. It desugars to the same S_QUERY node, so
# the whole query backend (iteration, filters, binding, break/continue) is reused. # the whole query backend (iteration, filters, binding, break/continue) is reused.
fn mk_and(a: Node, b: Node) -> Node { function mk_and(a: Node, b: Node) -> Node {
if (a == null) { return b } if (a == null) { return b }
let n = node(E_BIN); n.s = "and"; n.a = a; n.b = b; return n let n = node(E_BIN); n.s = "and"; n.a = a; n.b = b; return n
} }
@ -96,7 +96,7 @@ fn mk_and(a: Node, b: Node) -> Node {
# `Prop{constraint}` (base is the property binding) and by @Computed field # `Prop{constraint}` (base is the property binding) and by @Computed field
# expansion (base is the accessed value). Non-destructive: builds a fresh tree, # expansion (base is the accessed value). Non-destructive: builds a fresh tree,
# so a stored computed expression can be expanded at many access sites. # so a stored computed expression can be expanded at many access sites.
fn qualify_fields(e: Node, base: Node) -> Node { function qualify_fields(e: Node, base: Node) -> Node {
if (e == null) { return e } if (e == null) { return e }
if e.kind == E_ID { if e.kind == E_ID {
let m = node(E_MEMBER); m.a = base; m.s = e.s; return m let m = node(E_MEMBER); m.a = base; m.s = e.s; return m
@ -112,7 +112,7 @@ fn qualify_fields(e: Node, base: Node) -> Node {
# parse `(these: [...], on: Model)`, returning an S_QUERY with its vars/terms/where # parse `(these: [...], on: Model)`, returning an S_QUERY with its vars/terms/where
# filled in (the body `.a` is attached by the caller once the handler is parsed). # filled in (the body `.a` is attached by the caller once the handler is parsed).
fn parse_queries_anno() -> Node { function parse_queries_anno() -> Node {
eat_op("(") eat_op("(")
let qn = node(S_QUERY) let qn = node(S_QUERY)
let terms = node(N_BLOCK) let terms = node(N_BLOCK)
@ -144,7 +144,7 @@ fn parse_queries_anno() -> Node {
return qn return qn
} }
fn parse_spawn() -> Node { function parse_spawn() -> Node {
pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{") pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
while true { while true {
skipnl(); if is_op("}") { break } skipnl(); if is_op("}") { break }
@ -162,7 +162,7 @@ fn parse_spawn() -> Node {
# blocks (scene .a/.b); each layer's handlers are pushed straight into `prog` as # blocks (scene .a/.b); each layer's handlers are pushed straight into `prog` as
# ordinary N_SYS nodes, tagged with the owning scene in `.c`, so the whole # ordinary N_SYS nodes, tagged with the owning scene in `.c`, so the whole
# system backend (functions, phases, enable/disable) is reused unchanged. # system backend (functions, phases, enable/disable) is reused unchanged.
fn parse_scene() -> void { function parse_scene() -> void {
pi = pi + 1 # 'scene' pi = pi + 1 # 'scene'
let n = node(N_SCENE); n.s = eat_id() let n = node(N_SCENE); n.s = eat_id()
n.ival = g_scene_count n.ival = g_scene_count
@ -210,7 +210,7 @@ fn parse_scene() -> void {
# enum Name { A, B, C } — named int constants; a variant's value is its index. # enum Name { A, B, C } — named int constants; a variant's value is its index.
# Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces # Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces
# (state ids, menu selections, mode registers) without a runtime cost. # (state ids, menu selections, mode registers) without a runtime cost.
fn parse_enum() -> Node { function parse_enum() -> Node {
pi = pi + 1; let n = node(N_ENUM); n.s = eat_id(); skipnl(); eat_op("{") pi = pi + 1; let n = node(N_ENUM); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break } while true { skipnl(); if is_op("}") { break }
let v = node(E_ID); v.s = eat_id(); push(n.kids, v) let v = node(E_ID); v.s = eat_id(); push(n.kids, v)
@ -223,7 +223,7 @@ fn parse_enum() -> Node {
# N2): its @Sync-marked fields cross the wire for this model. Participation is # N2): its @Sync-marked fields cross the wire for this model. Participation is
# per model use-site — the same property syncs in one model, not another. The # per model use-site — the same property syncs in one model, not another. The
# per-member @Sync sets the member E_ID's ival=1 (read by emit_net). # per-member @Sync sets the member E_ID's ival=1 (read by emit_net).
fn parse_archetype() -> Node { function parse_archetype() -> Node {
pi = pi + 1; let n = node(N_ARCH); n.s = eat_id(); skipnl(); eat_op("{") pi = pi + 1; let n = node(N_ARCH); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break } while true { skipnl(); if is_op("}") { break }
let c = node(E_ID) let c = node(E_ID)
@ -233,8 +233,8 @@ fn parse_archetype() -> Node {
eat_op("}"); return n eat_op("}"); return n
} }
# extern fn name(params) -> T = "symbol" # extern function name(params) -> T = "symbol"
fn parse_extern() -> Node { function parse_extern() -> Node {
pi = pi + 1 # 'extern' pi = pi + 1 # 'extern'
let fnkw = eat_id() # 'fn' let fnkw = eat_id() # 'fn'
let n = node(N_EXTERN); n.s = eat_id(); eat_op("(") let n = node(N_EXTERN); n.s = eat_id(); eat_op("(")
@ -250,7 +250,7 @@ fn parse_extern() -> Node {
} }
# ui Name { widget-tree } — parsed into a widget node tree (emitted later) # ui Name { widget-tree } — parsed into a widget node tree (emitted later)
fn parse_widget() -> Node { function parse_widget() -> Node {
let w = node(N_UI); w.s = eat_id() # widget type name let w = node(N_UI); w.s = eat_id() # widget type name
w.b = node(N_BLOCK) # b.kids = props (E_FINIT) w.b = node(N_BLOCK) # b.kids = props (E_FINIT)
while toks[pi].kind == TK_ID and toks[pi + 1].kind == TK_OP and (toks[pi + 1].text == ":") { while toks[pi].kind == TK_ID and toks[pi + 1].kind == TK_OP and (toks[pi + 1].text == ":") {
@ -262,7 +262,7 @@ fn parse_widget() -> Node {
eat_op("}") } eat_op("}") }
return w return w
} }
fn parse_ui() -> Node { function parse_ui() -> Node {
pi = pi + 1; let n = node(N_UI); n.s = eat_id(); n.ival = 1 # ival=1 marks the top ui block pi = pi + 1; let n = node(N_UI); n.s = eat_id(); n.ival = 1 # ival=1 marks the top ui block
skipnl(); eat_op("{"); skipnl() skipnl(); eat_op("{"); skipnl()
n.a = parse_widget() n.a = parse_widget()

View file

@ -4,16 +4,16 @@
# a fresh NUL-terminated copy of src[start .. start+n] # a fresh NUL-terminated copy of src[start .. start+n]
fn char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 } function char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 }
fn char_is_alpha(c: int) -> bool { function char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true } if c >= 65 and c <= 90 { return true }
if c >= 97 and c <= 122 { return true } if c >= 97 and c <= 122 { return true }
return c == 95 return c == 95
} }
fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
# integer -> fresh decimal string # integer -> fresh decimal string
fn itoa(v: int) -> ptr { function itoa(v: int) -> ptr {
if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z } if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z }
var neg = false var neg = false
var x = v var x = v

View file

@ -1,5 +1,5 @@
program T { program T {
fn fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) } function fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) }
entry { entry {
print(fib(10)) # 55 print(fib(10)) # 55
var s = 0 var s = 0

View file

@ -1,5 +1,5 @@
program T { program T {
fn wide(a: long, b: int) -> long { return a * b } # int arg widens to i64 function wide(a: long, b: int) -> long { return a * b } # int arg widens to i64
entry { entry {
let big: long = 1000000 # int literal widens to a long let big: long = 1000000 # int literal widens to a long

View file

@ -1,5 +1,5 @@
program T { program T {
fn classify(c: int) -> int { function classify(c: int) -> int {
match c { 65, 66 => { return 1 }; 67 => { return 2 }; _ => { return 9 } } match c { 65, 66 => { return 1 }; 67 => { return 2 }; _ => { return 9 } }
return 0 return 0
} }

View file

@ -1,6 +1,6 @@
program T { program T {
property P { x: int = 0, y: int = 7, next: P } property P { x: int = 0, y: int = 7, next: P }
fn bump(p: P) -> void { p.x = p.x + 100 } function bump(p: P) -> void { p.x = p.x + 100 }
entry { entry {
let a = new P let a = new P
print(a.y) # 7 default print(a.y) # 7 default

View file

@ -34,7 +34,7 @@ game Atlas {
# build "<DIR>/tile_%04d.png" without a formatter: the runtime's string # build "<DIR>/tile_%04d.png" without a formatter: the runtime's string
# helpers write into a scratch buffer we own # helpers write into a scratch buffer we own
fn tile_path(i: int) -> str { function tile_path(i: int) -> str {
let buf = atlas_buf() let buf = atlas_buf()
let n = rt_put_str(buf, 0, DIR) let n = rt_put_str(buf, 0, DIR)
n = rt_put_str(buf, n, "/tile_") n = rt_put_str(buf, n, "/tile_")
@ -49,7 +49,7 @@ game Atlas {
var atlas_scratch: ptr = ptr_null() var atlas_scratch: ptr = ptr_null()
fn atlas_buf() -> ptr { function atlas_buf() -> ptr {
if ptr_is_null(atlas_scratch) { atlas_scratch = mem_alloc(512) } if ptr_is_null(atlas_scratch) { atlas_scratch = mem_alloc(512) }
return atlas_scratch return atlas_scratch
} }

View file

@ -19,7 +19,7 @@ A bare fragment is wrapped first: a declaration list goes inside
import re, subprocess, sys, os, tempfile import re, subprocess, sys, os, tempfile
LC = './build/ludicc' LC = './build/ludicc'
DECL = ('program','property','model','handler','enum','fn', DECL = ('program','property','model','handler','enum','function',
'extern','const','var','ui','struct','import') 'extern','const','var','ui','struct','import')
def classify(body): def classify(body):

View file

@ -33,12 +33,12 @@ def read(*parts):
def fn_body(text, name): def fn_body(text, name):
"""The source of `fn <name>` up to the next top-level `fn ` (or EOF).""" """The source of `function <name>` up to the next top-level `function ` (or EOF)."""
m = re.search(r"^fn %s\b" % re.escape(name), text, re.M) m = re.search(r"^function %s\b" % re.escape(name), text, re.M)
if not m: if not m:
return "" return ""
rest = text[m.end():] rest = text[m.end():]
nxt = re.search(r"^fn ", rest, re.M) nxt = re.search(r"^function ", rest, re.M)
return text[m.start(): m.end() + (nxt.start() if nxt else len(rest))] return text[m.start(): m.end() + (nxt.start() if nxt else len(rest))]

View file

@ -266,7 +266,7 @@
"kw-const", "kw-const",
"kw-var", "kw-var",
"kw-let", "kw-let",
"kw-fn", "kw-function",
"kw-return", "kw-return",
"kw-import", "kw-import",
"kw-extern", "kw-extern",

View file

@ -269,7 +269,7 @@ def emit_ludic(path):
lines.append("# Edit the palette there and regenerate; do not hand-edit this file.") lines.append("# Edit the palette there and regenerate; do not hand-edit this file.")
lines.append("# ============================================================================") lines.append("# ============================================================================")
lines.append("") lines.append("")
lines.append("fn color_lookup(name: ptr) -> int {") lines.append("function color_lookup(name: ptr) -> int {")
for (nm, hexv, grp) in PALETTE: for (nm, hexv, grp) in PALETTE:
lines.append(f' if (name == "{nm}") {{ return 0x{hexv:06X} }}') lines.append(f' if (name == "{nm}") {{ return 0x{hexv:06X} }}')
lines.append(" return -1") lines.append(" return -1")

View file

@ -5,7 +5,7 @@ Full programs (start with program/module) compile directly; fragments are wrappe
import os, re, subprocess, tempfile, sys import os, re, subprocess, tempfile, sys
REPO=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); LC=os.path.join(REPO,"bin","ludicc") REPO=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); LC=os.path.join(REPO,"bin","ludicc")
FENCE=re.compile(r"```ludic\n(.*?)\n```", re.S) FENCE=re.compile(r"```ludic\n(.*?)\n```", re.S)
DECL=("program","property","model","handler","enum","fn","extern","const","var","ui","event","module","import","scene","machine") DECL=("program","property","model","handler","enum","function","extern","const","var","ui","event","module","import","scene","machine")
def first_kw(body): def first_kw(body):
for l in body.split("\n"): for l in body.split("\n"):

View file

@ -20,7 +20,7 @@ object LudicTokens {
val IDENTIFIER = LudicTokenType("IDENTIFIER") val IDENTIFIER = LudicTokenType("IDENTIFIER")
val TYPE_NAME = LudicTokenType("TYPE_NAME") // Capitalised: component, archetype, scene val TYPE_NAME = LudicTokenType("TYPE_NAME") // Capitalised: component, archetype, scene
val KEYWORD = LudicTokenType("KEYWORD") val KEYWORD = LudicTokenType("KEYWORD")
val DECL_KEYWORD = LudicTokenType("DECL_KEYWORD") // game, component, system, fn, ... val DECL_KEYWORD = LudicTokenType("DECL_KEYWORD") // game, component, system, function, ...
val CLAUSE = LudicTokenType("CLAUSE") // phase, query, reads, writes, ... val CLAUSE = LudicTokenType("CLAUSE") // phase, query, reads, writes, ...
val PRIMITIVE = LudicTokenType("PRIMITIVE") // int, fixed, bool, entity, str, ptr, void val PRIMITIVE = LudicTokenType("PRIMITIVE") // int, fixed, bool, entity, str, ptr, void
val PHASE = LudicTokenType("PHASE") // Start, Update, Render, ... val PHASE = LudicTokenType("PHASE") // Start, Update, Render, ...
@ -42,7 +42,7 @@ object LudicTokens {
object LudicVocabulary { object LudicVocabulary {
val DECL = setOf( val DECL = setOf(
"program", "import", "property", "model", "enum", "ui", "program", "import", "property", "model", "enum", "ui",
"const", "var", "fn", "extern", "handler", "entry", "event", "scene" "const", "var", "function", "extern", "handler", "entry", "event", "scene"
) )
val CLAUSE = setOf( val CLAUSE = setOf(
"phase", "query", "on", "cancellable", "public", "layer", "start" "phase", "query", "on", "cancellable", "public", "layer", "start"

View file

@ -100,7 +100,7 @@
} }
}, },
{ {
"match": "\\b(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(extern\\s+)?(function)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.modifier.extern.ludic" }, "1": { "name": "storage.modifier.extern.ludic" },
"2": { "name": "storage.type.function.ludic" }, "2": { "name": "storage.type.function.ludic" },
@ -176,7 +176,7 @@
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" }, { "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" }, { "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" }, { "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" } { "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }

View file

@ -100,7 +100,7 @@
} }
}, },
{ {
"match": "\\b(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(extern\\s+)?(function)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.modifier.extern.ludic" }, "1": { "name": "storage.modifier.extern.ludic" },
"2": { "name": "storage.type.function.ludic" }, "2": { "name": "storage.type.function.ludic" },
@ -176,7 +176,7 @@
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" }, { "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" }, { "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|str|ptr|byte|words|fixeds|ptrs|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" }, { "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" } { "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }

View file

@ -28,7 +28,7 @@ program LudicFmt {
const LT_ERR: int = 14 const LT_ERR: int = 14
# ---- tiny stdio + string helpers (self-contained) ---- # ---- tiny stdio + string helpers (self-contained) ----
fn read_file(path: str) -> ptr { function read_file(path: str) -> ptr {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if (f == null) { return null } if (f == null) { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)
@ -40,16 +40,16 @@ program LudicFmt {
file_close(f) file_close(f)
return buf return buf
} }
fn cstr_len(s: ptr) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n } function cstr_len(s: ptr) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n }
fn char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 } function char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 }
fn char_is_alpha(c: int) -> bool { function char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true } if c >= 65 and c <= 90 { return true }
if c >= 97 and c <= 122 { return true } if c >= 97 and c <= 122 { return true }
return c == 95 return c == 95
} }
fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
fn char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) } function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) }
fn itoa(v: int) -> ptr { function itoa(v: int) -> ptr {
if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z } if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z }
var neg = false; var x = v var neg = false; var x = v
if x < 0 { neg = true; x = 0 - x } if x < 0 { neg = true; x = 0 - x }
@ -67,18 +67,18 @@ program LudicFmt {
# ---- a growable byte buffer ---- # ---- a growable byte buffer ----
property Buf { data: ptr = null, len: int = 0, cap: int = 0 } property Buf { data: ptr = null, len: int = 0, cap: int = 0 }
fn buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b } function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b }
fn buf_ensure(b: Buf, extra: int) -> void { function buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return } if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 } while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 }
b.data = resize(b.data, b.cap) b.data = resize(b.data, b.cap)
} }
fn buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 } function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 }
fn buf_puts(b: Buf, s: ptr) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } } function buf_puts(b: Buf, s: ptr) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } }
fn buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, 32); i = i + 1 } } function buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, 32); i = i + 1 } }
fn buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data } function buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data }
# append src[a..b) raw # append src[a..b) raw
fn buf_addrange(b: Buf, s: ptr, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } } function buf_addrange(b: Buf, s: ptr, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } }
# ---- the token stream (parallel slices) ---- # ---- the token stream (parallel slices) ----
var src: ptr = null var src: ptr = null
@ -88,35 +88,35 @@ program LudicFmt {
var tk_line: []int var tk_line: []int
var linestart: []int var linestart: []int
fn ntok() -> int { return len(tk_kind) } function ntok() -> int { return len(tk_kind) }
fn tok_len(i: int) -> int { return tk_end[i] - tk_start[i] } function tok_len(i: int) -> int { return tk_end[i] - tk_start[i] }
fn tok_text(i: int) -> ptr { return src[tk_start[i]..tk_end[i]] } function tok_text(i: int) -> ptr { return src[tk_start[i]..tk_end[i]] }
fn push_tok(k: int, st: int, en: int, ln: int) -> void { function push_tok(k: int, st: int, en: int, ln: int) -> void {
push(tk_kind, k); push(tk_start, st); push(tk_end, en); push(tk_line, ln) push(tk_kind, k); push(tk_start, st); push(tk_end, en); push(tk_line, ln)
} }
# ---- vocabulary classifiers ---- # ---- vocabulary classifiers ----
fn is_type_word(w: ptr) -> bool { function is_type_word(w: ptr) -> bool {
return (w == "int") or (w == "long") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void") return (w == "int") or (w == "long") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
} }
fn is_phase_word(w: ptr) -> bool { function is_phase_word(w: ptr) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")
} }
fn is_keyword_word(w: ptr) -> bool { function is_keyword_word(w: ptr) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true } if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "const") or (w == "var") or (w == "fn") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true } if (w == "const") or (w == "var") or (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true } if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true } if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true }
if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") { return true } if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") { return true }
if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true } if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true }
return false return false
} }
fn is_clause_word(w: ptr) -> bool { function is_clause_word(w: ptr) -> bool {
return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects") return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects")
} }
# ---- the lexer: keeps comments, newlines, byte spans; never exits on bad input ---- # ---- the lexer: keeps comments, newlines, byte spans; never exits on bad input ----
fn is_op2(c0: int, c1: int) -> bool { function is_op2(c0: int, c1: int) -> bool {
if c0 == 45 and c1 == 62 { return true } # -> if c0 == 45 and c1 == 62 { return true } # ->
if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } # += -= *= /= == != <= >= if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } # += -= *= /= == != <= >=
if c0 == 38 and c1 == 38 { return true } # && if c0 == 38 and c1 == 38 { return true } # &&
@ -125,11 +125,11 @@ program LudicFmt {
if c0 == 61 and c1 == 62 { return true } # => if c0 == 61 and c1 == 62 { return true } # =>
return false return false
} }
fn is_op1(c: int) -> bool { function is_op1(c: int) -> bool {
return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59 return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59
} }
fn lex(s: ptr) -> void { function lex(s: ptr) -> void {
src = s src = s
tk_kind = new []int; tk_start = new []int; tk_end = new []int; tk_line = new []int tk_kind = new []int; tk_start = new []int; tk_end = new []int; tk_line = new []int
linestart = new []int linestart = new []int
@ -197,19 +197,19 @@ program LudicFmt {
} }
# ---- token-stream helpers ---- # ---- token-stream helpers ----
fn next_sig(i: int) -> int { function next_sig(i: int) -> int {
var j = i + 1 var j = i + 1
while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 } while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 }
return 0 - 1 return 0 - 1
} }
fn prev_sig(i: int) -> int { function prev_sig(i: int) -> int {
var j = i - 1 var j = i - 1
while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 } while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 }
return 0 - 1 return 0 - 1
} }
fn name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL } function name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL }
# a '.' hugs an operand, a closing bracket, and another dot # a '.' hugs an operand, a closing bracket, and another dot
fn dot_tight(t: int) -> bool { function dot_tight(t: int) -> bool {
if name_like(tk_kind[t]) { return true } if name_like(tk_kind[t]) { return true }
if tk_kind[t] != LT_OP { return false } if tk_kind[t] != LT_OP { return false }
let n = tok_len(t); let c0 = src[tk_start[t]] let n = tok_len(t); let c0 = src[tk_start[t]]
@ -218,7 +218,7 @@ program LudicFmt {
return false return false
} }
# is the token at index i a unary '-'/'!' rather than a binary operator? # is the token at index i a unary '-'/'!' rather than a binary operator?
fn is_unary(i: int) -> bool { function is_unary(i: int) -> bool {
if tk_kind[i] != LT_OP { return false } if tk_kind[i] != LT_OP { return false }
let n = tok_len(i) let n = tok_len(i)
if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33)) { return false } if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33)) { return false }
@ -234,7 +234,7 @@ program LudicFmt {
} }
# whitespace between the previous emitted token (prev) and the current one (cur) # whitespace between the previous emitted token (prev) and the current one (cur)
fn space_before(prev: int, cur: int) -> int { function space_before(prev: int, cur: int) -> int {
if prev < 0 { return 0 } if prev < 0 { return 0 }
let pk = tk_kind[prev]; let ck = tk_kind[cur] let pk = tk_kind[prev]; let ck = tk_kind[cur]
let plen = tok_len(prev); let clen = tok_len(cur) let plen = tok_len(prev); let clen = tok_len(cur)
@ -248,7 +248,7 @@ program LudicFmt {
if pk == LT_OP and is_unary(prev) { return 0 } if pk == LT_OP and is_unary(prev) { return 0 }
if pk == LT_ANNO and c1 and c0 == 40 { return 0 } # @anno( if pk == LT_ANNO and c1 and c0 == 40 { return 0 } # @anno(
if c1 and c0 == 40 and ck == LT_OP { if c1 and c0 == 40 and ck == LT_OP {
if pk == LT_ID or pk == LT_TYPE or pk == LT_PHASE { return 0 } # fn move( / clear( if pk == LT_ID or pk == LT_TYPE or pk == LT_PHASE { return 0 } # function move( / clear(
if pk == LT_OP { if p1 and (p0 == 41 or p0 == 93) { return 1 }; return 0 } if pk == LT_OP { if p1 and (p0 == 41 or p0 == 93) { return 1 }; return 0 }
return 1 return 1
} }
@ -256,7 +256,7 @@ program LudicFmt {
} }
# ---- the formatting pass ---- # ---- the formatting pass ----
fn format(indent_width: int) -> Buf { function format(indent_width: int) -> Buf {
let o = buf_new() let o = buf_new()
let stack = words(600) let stack = words(600)
let hang = words(600) let hang = words(600)
@ -394,7 +394,7 @@ program LudicFmt {
var g_info: int = 0 var g_info: int = 0
var g_marker: int = 0 var g_marker: int = 0
# detect a fence at line offset i; sets g_flen/g_info/g_marker; returns bool # detect a fence at line offset i; sets g_flen/g_info/g_marker; returns bool
fn md_fence_at(s: ptr, i: int) -> bool { function md_fence_at(s: ptr, i: int) -> bool {
var j = i; while s[j] == 32 { j = j + 1 } var j = i; while s[j] == 32 { j = j + 1 }
let m = s[j] let m = s[j]
if m != 96 and m != 126 { return false } # ` or ~ if m != 96 and m != 126 { return false } # ` or ~
@ -403,16 +403,16 @@ program LudicFmt {
g_flen = n; g_info = j; g_marker = m g_flen = n; g_info = j; g_marker = m
return true return true
} }
fn md_info_is_ludic(s: ptr, at: int) -> bool { function md_info_is_ludic(s: ptr, at: int) -> bool {
var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 } var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 }
# case-insensitive "ludic" # case-insensitive "ludic"
if not (((s[a] == 108 or s[a] == 76)) and ((s[a + 1] == 117 or s[a + 1] == 85)) and ((s[a + 2] == 100 or s[a + 2] == 68)) and ((s[a + 3] == 105 or s[a + 3] == 73)) and ((s[a + 4] == 99 or s[a + 4] == 67))) { return false } if not (((s[a] == 108 or s[a] == 76)) and ((s[a + 1] == 117 or s[a + 1] == 85)) and ((s[a + 2] == 100 or s[a + 2] == 68)) and ((s[a + 3] == 105 or s[a + 3] == 73)) and ((s[a + 4] == 99 or s[a + 4] == 67))) { return false }
let af = s[a + 5] let af = s[a + 5]
return af == 0 or af == 10 or af == 32 or af == 9 or af == 13 return af == 0 or af == 10 or af == 32 or af == 9 or af == 13
} }
fn line_end(s: ptr, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e } function line_end(s: ptr, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e }
fn format_markdown(s: ptr, indent_width: int) -> Buf { function format_markdown(s: ptr, indent_width: int) -> Buf {
let o = buf_new() let o = buf_new()
var i = 0 var i = 0
while s[i] != 0 { while s[i] != 0 {
@ -478,14 +478,14 @@ program LudicFmt {
} }
# ---- ends-with helpers for extension detection ---- # ---- ends-with helpers for extension detection ----
fn ends_with(s: ptr, suf: ptr) -> bool { function ends_with(s: ptr, suf: ptr) -> bool {
let n = cstr_len(s); let m = cstr_len(suf) let n = cstr_len(s); let m = cstr_len(suf)
if m > n { return false } if m > n { return false }
return (s[n - m..n] == suf) return (s[n - m..n] == suf)
} }
# ---- stdin slurp (for `-`) ---- # ---- stdin slurp (for `-`) ----
fn slurp_stdin() -> ptr { function slurp_stdin() -> ptr {
let b = buf_new() let b = buf_new()
var c = read_char() var c = read_char()
while c >= 0 { buf_putc(b, c); c = read_char() } while c >= 0 { buf_putc(b, c); c = read_char() }
@ -493,14 +493,14 @@ program LudicFmt {
} }
# format one source string according to its kind (markdown vs ludic) # format one source string according to its kind (markdown vs ludic)
fn format_source(text: ptr, is_md: bool, indent_width: int) -> ptr { function format_source(text: ptr, is_md: bool, indent_width: int) -> ptr {
if is_md { let m = format_markdown(text, indent_width); return buf_str(m) } if is_md { let m = format_markdown(text, indent_width); return buf_str(m) }
lex(text) lex(text)
let f = format(indent_width) let f = format(indent_width)
return buf_str(f) return buf_str(f)
} }
fn streq(a: ptr, b: ptr) -> bool { return (a == b) } function streq(a: ptr, b: ptr) -> bool { return (a == b) }
entry { entry {
var write = false var write = false

View file

@ -72,7 +72,7 @@ program LudicLsp {
const SC_UNKNOWN: int = 24 const SC_UNKNOWN: int = 24
# ---- tiny helpers ---- # ---- tiny helpers ----
fn read_file(path: ptr) -> ptr { function read_file(path: ptr) -> ptr {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if (f == null) { return null } if (f == null) { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)
@ -84,16 +84,16 @@ program LudicLsp {
file_close(f) file_close(f)
return buf return buf
} }
fn cstr_len(s: ptr) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n } function cstr_len(s: ptr) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n }
fn char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 } function char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 }
fn char_is_alpha(c: int) -> bool { function char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true } if c >= 65 and c <= 90 { return true }
if c >= 97 and c <= 122 { return true } if c >= 97 and c <= 122 { return true }
return c == 95 return c == 95
} }
fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
fn char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) } function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) }
fn itoa(v: int) -> ptr { function itoa(v: int) -> ptr {
if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z } if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z }
var neg = false; var x = v var neg = false; var x = v
if x < 0 { neg = true; x = 0 - x } if x < 0 { neg = true; x = 0 - x }
@ -108,7 +108,7 @@ program LudicLsp {
out[total] = 0 out[total] = 0
return out return out
} }
fn atoi(s: ptr) -> int { function atoi(s: ptr) -> int {
var n = 0; var i = 0; var neg = false var n = 0; var i = 0; var neg = false
while s[i] == 32 or s[i] == 9 { i = i + 1 } while s[i] == 32 or s[i] == 9 { i = i + 1 }
if s[i] == 45 { neg = true; i = i + 1 } if s[i] == 45 { neg = true; i = i + 1 }
@ -119,27 +119,27 @@ program LudicLsp {
# ---- growable byte buffer ---- # ---- growable byte buffer ----
property Buf { data: ptr = null, len: int = 0, cap: int = 0 } property Buf { data: ptr = null, len: int = 0, cap: int = 0 }
fn buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b } function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b }
fn buf_ensure(b: Buf, extra: int) -> void { function buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return } if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 } while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 }
b.data = resize(b.data, b.cap) b.data = resize(b.data, b.cap)
} }
fn buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 } function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 }
fn buf_puts(b: Buf, s: ptr) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } } function buf_puts(b: Buf, s: ptr) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } }
fn buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) } function buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) }
fn buf_addrange(b: Buf, s: ptr, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } } function buf_addrange(b: Buf, s: ptr, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } }
fn buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data } function buf_str(b: Buf) -> ptr { b.data[b.len] = 0; return b.data }
# ---- string helpers ---- # ---- string helpers ----
fn streq(a: ptr, b: ptr) -> bool { return (a == b) } function streq(a: ptr, b: ptr) -> bool { return (a == b) }
fn concat3(a: ptr, b: ptr, c: ptr) -> ptr { return ((a + b) + c) } function concat3(a: ptr, b: ptr, c: ptr) -> ptr { return ((a + b) + c) }
fn ends_with(s: ptr, suf: ptr) -> bool { function ends_with(s: ptr, suf: ptr) -> bool {
let n = cstr_len(s); let m = cstr_len(suf) let n = cstr_len(s); let m = cstr_len(suf)
if m > n { return false } if m > n { return false }
return (s[n - m..n] == suf) return (s[n - m..n] == suf)
} }
fn starts_with(s: ptr, pre: ptr) -> bool { function starts_with(s: ptr, pre: ptr) -> bool {
let m = cstr_len(pre) let m = cstr_len(pre)
if m > cstr_len(s) { return false } if m > cstr_len(s) { return false }
return (s[0..m] == pre) return (s[0..m] == pre)
@ -191,7 +191,7 @@ program LudicLsp {
var g_shutdown: int = 0 var g_shutdown: int = 0
# ---- the lexer, over a Doc ---- # ---- the lexer, over a Doc ----
fn is_op2(c0: int, c1: int) -> bool { function is_op2(c0: int, c1: int) -> bool {
if c0 == 45 and c1 == 62 { return true } if c0 == 45 and c1 == 62 { return true }
if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true }
if c0 == 38 and c1 == 38 { return true } if c0 == 38 and c1 == 38 { return true }
@ -200,29 +200,29 @@ program LudicLsp {
if c0 == 61 and c1 == 62 { return true } if c0 == 61 and c1 == 62 { return true }
return false return false
} }
fn is_op1(c: int) -> bool { function is_op1(c: int) -> bool {
return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59 return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59
} }
fn is_type_word(w: ptr) -> bool { function is_type_word(w: ptr) -> bool {
return (w == "int") or (w == "long") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void") return (w == "int") or (w == "long") or (w == "fixed") or (w == "bool") or (w == "entity") or (w == "str") or (w == "ptr") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "ptrs") or (w == "void")
} }
fn is_phase_word(w: ptr) -> bool { function is_phase_word(w: ptr) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")
} }
fn is_keyword_word(w: ptr) -> bool { function is_keyword_word(w: ptr) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true } if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "const") or (w == "var") or (w == "fn") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true } if (w == "const") or (w == "var") or (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true } if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true } if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true }
if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") { return true } if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") { return true }
if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true } if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true }
return false return false
} }
fn is_widget_word(w: ptr) -> bool { function is_widget_word(w: ptr) -> bool {
return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer") return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer")
} }
fn lex_doc(D: Doc) -> void { function lex_doc(D: Doc) -> void {
let s = D.src let s = D.src
D.tk_kind = new []int; D.tk_start = new []int; D.tk_end = new []int; D.tk_line = new []int D.tk_kind = new []int; D.tk_start = new []int; D.tk_end = new []int; D.tk_line = new []int
D.linestart = new []int D.linestart = new []int
@ -265,15 +265,15 @@ program LudicLsp {
push(D.tk_kind, LT_EOF); push(D.tk_start, i); push(D.tk_end, i); push(D.tk_line, line) push(D.tk_kind, LT_EOF); push(D.tk_start, i); push(D.tk_end, i); push(D.tk_line, line)
} }
fn ntok(D: Doc) -> int { return len(D.tk_kind) } function ntok(D: Doc) -> int { return len(D.tk_kind) }
fn tlen(D: Doc, i: int) -> int { return D.tk_end[i] - D.tk_start[i] } function tlen(D: Doc, i: int) -> int { return D.tk_end[i] - D.tk_start[i] }
fn ttext(D: Doc, i: int) -> ptr { return D.src[D.tk_start[i]..D.tk_end[i]] } function ttext(D: Doc, i: int) -> ptr { return D.src[D.tk_start[i]..D.tk_end[i]] }
fn tis(D: Doc, i: int, s: ptr) -> bool { if i < 0 or i >= ntok(D) { return false }; return (ttext(D, i) == s) } function tis(D: Doc, i: int, s: ptr) -> bool { if i < 0 or i >= ntok(D) { return false }; return (ttext(D, i) == s) }
fn next_sig(D: Doc, i: int) -> int { var j = i + 1; while j < ntok(D) { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 }; return 0 - 1 } function next_sig(D: Doc, i: int) -> int { var j = i + 1; while j < ntok(D) { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 }; return 0 - 1 }
fn prev_sig(D: Doc, i: int) -> int { var j = i - 1; while j >= 0 { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 }; return 0 - 1 } function prev_sig(D: Doc, i: int) -> int { var j = i - 1; while j >= 0 { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 }; return 0 - 1 }
# ---- positions (UTF-16 for LSP) ---- # ---- positions (UTF-16 for LSP) ----
fn utf16_len(s: ptr, a: int, nbytes: int) -> int { function utf16_len(s: ptr, a: int, nbytes: int) -> int {
var u = 0; var i = 0 var u = 0; var i = 0
while i < nbytes { while i < nbytes {
let c = s[a + i] let c = s[a + i]
@ -284,13 +284,13 @@ program LudicLsp {
} }
return u return u
} }
fn line_of(D: Doc, off: int) -> int { function line_of(D: Doc, off: int) -> int {
var lo = 0; var hi = len(D.linestart) - 1 var lo = 0; var hi = len(D.linestart) - 1
while lo < hi { let mid = (lo + hi + 1) / 2; if D.linestart[mid] <= off { lo = mid } else { hi = mid - 1 } } while lo < hi { let mid = (lo + hi + 1) / 2; if D.linestart[mid] <= off { lo = mid } else { hi = mid - 1 } }
return lo return lo
} }
fn col_of(D: Doc, off: int) -> int { let l = line_of(D, off); return utf16_len(D.text, D.linestart[l], off - D.linestart[l]) } function col_of(D: Doc, off: int) -> int { let l = line_of(D, off); return utf16_len(D.text, D.linestart[l], off - D.linestart[l]) }
fn offset_of(D: Doc, line: int, ch: int) -> int { function offset_of(D: Doc, line: int, ch: int) -> int {
if line < 0 { return 0 } if line < 0 { return 0 }
if line >= len(D.linestart) { return cstr_len(D.text) } if line >= len(D.linestart) { return cstr_len(D.text) }
var off = D.linestart[line] var off = D.linestart[line]
@ -306,7 +306,7 @@ program LudicLsp {
} }
return off return off
} }
fn tok_at(D: Doc, off: int) -> int { function tok_at(D: Doc, off: int) -> int {
var touching = 0 - 1 var touching = 0 - 1
var i = 0 var i = 0
let N = ntok(D) let N = ntok(D)
@ -323,7 +323,7 @@ program LudicLsp {
} }
# ---- brace matching ---- # ---- brace matching ----
fn match_braces(D: Doc) -> void { function match_braces(D: Doc) -> void {
let N = ntok(D) let N = ntok(D)
D.tmatch = new []int D.tmatch = new []int
var i = 0 var i = 0
@ -342,7 +342,7 @@ program LudicLsp {
} }
# ---- doc comment: the comment block directly above a declaration ---- # ---- doc comment: the comment block directly above a declaration ----
fn collect_doc(D: Doc, decl_tok: int) -> ptr { function collect_doc(D: Doc, decl_tok: int) -> ptr {
var line = D.tk_line[decl_tok] var line = D.tk_line[decl_tok]
var first = 0 - 1 var first = 0 - 1
var i = decl_tok - 1 var i = decl_tok - 1
@ -379,17 +379,17 @@ program LudicLsp {
var pD: Doc = null var pD: Doc = null
var pi: int = 0 var pi: int = 0
fn pk() -> int { if pi < ntok(pD) { return pD.tk_kind[pi] }; return LT_EOF } function pk() -> int { if pi < ntok(pD) { return pD.tk_kind[pi] }; return LT_EOF }
fn pis(s: ptr) -> bool { return tis(pD, pi, s) } function pis(s: ptr) -> bool { return tis(pD, pi, s) }
fn pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi = pi + 1 } } function pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi = pi + 1 } }
fn padv() -> void { if pi < ntok(pD) { pi = pi + 1 }; pskipnl() } function padv() -> void { if pi < ntok(pD) { pi = pi + 1 }; pskipnl() }
fn pword() -> ptr { if pi < ntok(pD) { return ttext(pD, pi) }; return "" } function pword() -> ptr { if pi < ntok(pD) { return ttext(pD, pi) }; return "" }
fn pname() -> int { function pname() -> int {
let k = pk() let k = pk()
if k == LT_ID or k == LT_TYPE or k == LT_PHASE or k == LT_KW or k == LT_BOOL { let t = pi; padv(); return t } if k == LT_ID or k == LT_TYPE or k == LT_PHASE or k == LT_KW or k == LT_BOOL { let t = pi; padv(); return t }
return 0 - 1 return 0 - 1
} }
fn sym_add(kind: int, nametok: int, parent: int) -> int { function sym_add(kind: int, nametok: int, parent: int) -> int {
let s = new Sym let s = new Sym
s.kind = kind; s.tok = nametok; s.parent = parent s.kind = kind; s.tok = nametok; s.parent = parent
s.name = ""; s.ty = ""; s.detail = ""; s.doc = "" s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""
@ -398,11 +398,11 @@ program LudicLsp {
push(pD.syms, s) push(pD.syms, s)
return len(pD.syms) - 1 return len(pD.syms) - 1
} }
fn cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return 0 - 1 } function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return 0 - 1 }
fn skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() } function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() }
# `name: Type` pairs inside ( ) — record params, build the signature into sig # `name: Type` pairs inside ( ) — record params, build the signature into sig
fn lp_params(owner: int, kind: int, sig: Buf) -> void { function lp_params(owner: int, kind: int, sig: Buf) -> void {
if not pis("(") { return } if not pis("(") { return }
let close = cur_match() let close = cur_match()
buf_putc(sig, 40) # '(' buf_putc(sig, 40) # '('
@ -428,7 +428,7 @@ program LudicLsp {
} }
# (a, b) variable list -> token indices into out; returns count # (a, b) variable list -> token indices into out; returns count
fn lp_varlist(out: []int) -> int { function lp_varlist(out: []int) -> int {
var n = 0 var n = 0
if not pis("(") { return 0 } if not pis("(") { return 0 }
let close = cur_match() let close = cur_match()
@ -446,7 +446,7 @@ program LudicLsp {
} }
# [Pos, Vel, {Enemy}] — bind listed components to vars in order (skip {Tag}) # [Pos, Vel, {Enemy}] — bind listed components to vars in order (skip {Tag})
fn lp_query_terms(owner: int, vars: []int, nvars: int, scope_start: int, scope_end: int) -> void { function lp_query_terms(owner: int, vars: []int, nvars: int, scope_start: int, scope_end: int) -> void {
if not pis("[") { return } if not pis("[") { return }
let close = cur_match() let close = cur_match()
padv() padv()
@ -470,7 +470,7 @@ program LudicLsp {
if pis("where") { padv() } if pis("where") { padv() }
} }
fn lp_block(owner: int, scope_end: int) -> void { function lp_block(owner: int, scope_end: int) -> void {
if not pis("{") { return } if not pis("{") { return }
let close = cur_match() let close = cur_match()
var endo = scope_end var endo = scope_end
@ -487,7 +487,7 @@ program LudicLsp {
if close >= 0 { pi = close + 1 } else { padv() } if close >= 0 { pi = close + 1 } else { padv() }
pskipnl() pskipnl()
} }
fn lp_stmt(owner: int, scope_end: int) -> void { function lp_stmt(owner: int, scope_end: int) -> void {
if pis("let") { if pis("let") {
padv(); let nt = pname(); if nt < 0 { return } padv(); let nt = pname(); if nt < 0 { return }
var ty = "" var ty = ""
@ -522,7 +522,7 @@ program LudicLsp {
} }
# panel id=Root w=288 { … } — each id= mints a UI_<Name> handle # panel id=Root w=288 { … } — each id= mints a UI_<Name> handle
fn lp_widget(owner: int, uisym: int) -> void { function lp_widget(owner: int, uisym: int) -> void {
let type_tok = pname() let type_tok = pname()
if type_tok < 0 { return } if type_tok < 0 { return }
while pi < ntok(pD) and pk() == LT_ID { while pi < ntok(pD) and pk() == LT_ID {
@ -565,7 +565,7 @@ program LudicLsp {
} }
# one top-level declaration # one top-level declaration
fn lp_decl(parent: int) -> void { function lp_decl(parent: int) -> void {
let w = pword() let w = pword()
let decl_tok = pi let decl_tok = pi
@ -638,11 +638,11 @@ program LudicLsp {
return return
} }
if w == "extern" { if w == "extern" {
padv(); if pis("fn") { padv() } padv(); if pis("function") { padv() }
let nt = pname(); if nt < 0 { return } let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_EXTERN, nt, parent) let sidx = sym_add(LS_EXTERN, nt, parent)
pD.syms[sidx].doc = collect_doc(pD, decl_tok) pD.syms[sidx].doc = collect_doc(pD, decl_tok)
let sig = buf_new(); buf_puts(sig, "extern fn "); buf_puts(sig, pD.syms[sidx].name) let sig = buf_new(); buf_puts(sig, "extern function "); buf_puts(sig, pD.syms[sidx].name)
lp_params(sidx, LS_PARAM, sig) lp_params(sidx, LS_PARAM, sig)
if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt); buf_puts(sig, " -> "); buf_puts(sig, pD.syms[sidx].ty) } } if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt); buf_puts(sig, " -> "); buf_puts(sig, pD.syms[sidx].ty) } }
pD.syms[sidx].detail = buf_str(sig) pD.syms[sidx].detail = buf_str(sig)
@ -650,10 +650,10 @@ program LudicLsp {
pskipnl() pskipnl()
return return
} }
if (w == "fn") or (w == "pure") or (w == "export") { if (w == "function") or (w == "pure") or (w == "export") {
let exported = (w == "export") let exported = (w == "export")
if (w == "pure") or exported { padv(); if pis("pure") { padv() } } if (w == "pure") or exported { padv(); if pis("pure") { padv() } }
if pis("fn") { padv() } if pis("function") { padv() }
let nt = pname(); if nt < 0 { return } let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_FN, nt, parent) let sidx = sym_add(LS_FN, nt, parent)
if exported { pD.syms[sidx].exported = 1 } if exported { pD.syms[sidx].exported = 1 }
@ -795,7 +795,7 @@ program LudicLsp {
padv() padv()
} }
fn parse_doc(D: Doc) -> void { function parse_doc(D: Doc) -> void {
D.syms = new []Sym D.syms = new []Sym
D.imports = new []ptr D.imports = new []ptr
D.is_unit = 0; D.is_module = 0; D.unit = "" D.is_unit = 0; D.is_module = 0; D.unit = ""
@ -833,22 +833,22 @@ program LudicLsp {
# ============================================================================ # ============================================================================
# workspace: many documents, one project # workspace: many documents, one project
# ============================================================================ # ============================================================================
fn is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE } function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE }
fn is_toplevel_kind(k: int) -> bool { function is_toplevel_kind(k: int) -> bool {
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT
} }
fn is_outline_kind(k: int) -> bool { function is_outline_kind(k: int) -> bool {
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER
} }
# ---- paths & URIs ---- # ---- paths & URIs ----
fn hexval(c: int) -> int { function hexval(c: int) -> int {
if c >= 48 and c <= 57 { return c - 48 } if c >= 48 and c <= 57 { return c - 48 }
let l = c | 32 let l = c | 32
if l >= 97 and l <= 102 { return l - 97 + 10 } if l >= 97 and l <= 102 { return l - 97 + 10 }
return 0 - 1 return 0 - 1
} }
fn uri_to_path(uri: ptr) -> ptr { function uri_to_path(uri: ptr) -> ptr {
var s = uri var s = uri
if starts_with(uri, "file://") { s = uri[7..cstr_len(uri)] } if starts_with(uri, "file://") { s = uri[7..cstr_len(uri)] }
let n = cstr_len(s) let n = cstr_len(s)
@ -861,12 +861,12 @@ program LudicLsp {
out[j] = 0 out[j] = 0
return out return out
} }
fn uri_safe(c: int) -> bool { function uri_safe(c: int) -> bool {
if char_is_alnum(c) { return true } if char_is_alnum(c) { return true }
return c == 45 or c == 46 or c == 126 or c == 47 # - . ~ / return c == 45 or c == 46 or c == 126 or c == 47 # - . ~ /
} }
fn hexch(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } function hexch(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n }
fn path_to_uri(path: ptr) -> ptr { function path_to_uri(path: ptr) -> ptr {
let out = buf_new() let out = buf_new()
buf_puts(out, "file://") buf_puts(out, "file://")
var i = 0 var i = 0
@ -878,7 +878,7 @@ program LudicLsp {
} }
return buf_str(out) return buf_str(out)
} }
fn dirname(p: ptr) -> ptr { function dirname(p: ptr) -> ptr {
var last = 0 - 1 var last = 0 - 1
var i = 0 var i = 0
while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 } while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 }
@ -886,7 +886,7 @@ program LudicLsp {
if last == 0 { return "/" } if last == 0 { return "/" }
return p[0..last] return p[0..last]
} }
fn normpath(p: ptr) -> ptr { function normpath(p: ptr) -> ptr {
let n = cstr_len(p) let n = cstr_len(p)
let abs = (n > 0 and p[0] == 47) let abs = (n > 0 and p[0] == 47)
let list = new []ptr let list = new []ptr
@ -914,16 +914,16 @@ program LudicLsp {
while k < cnt { if k > 0 { buf_putc(out, 47) }; buf_puts(out, list[k]); k = k + 1 } while k < cnt { if k > 0 { buf_putc(out, 47) }; buf_puts(out, list[k]); k = k + 1 }
return buf_str(out) return buf_str(out)
} }
fn join_path(dir: ptr, rel: ptr) -> ptr { function join_path(dir: ptr, rel: ptr) -> ptr {
if rel[0] == 47 { return normpath(rel) } if rel[0] == 47 { return normpath(rel) }
return normpath(concat3(dir, "/", rel)) return normpath(concat3(dir, "/", rel))
} }
# ---- markdown scrub: blank everything outside a ```ludic fence ---- # ---- markdown scrub: blank everything outside a ```ludic fence ----
fn word_ludic_at(s: ptr, at: int) -> bool { function word_ludic_at(s: ptr, at: int) -> bool {
return ((s[at] == 108 or s[at] == 76)) and ((s[at + 1] == 117 or s[at + 1] == 85)) and ((s[at + 2] == 100 or s[at + 2] == 68)) and ((s[at + 3] == 105 or s[at + 3] == 73)) and ((s[at + 4] == 99 or s[at + 4] == 67)) return ((s[at] == 108 or s[at] == 76)) and ((s[at + 1] == 117 or s[at + 1] == 85)) and ((s[at + 2] == 100 or s[at + 2] == 68)) and ((s[at + 3] == 105 or s[at + 3] == 73)) and ((s[at + 4] == 99 or s[at + 4] == 67))
} }
fn scrub_markdown(src: ptr) -> ptr { function scrub_markdown(src: ptr) -> ptr {
let n = cstr_len(src) let n = cstr_len(src)
let out = bytes(n + 1) let out = bytes(n + 1)
var c0 = 0 var c0 = 0
@ -974,12 +974,12 @@ program LudicLsp {
} }
# ---- documents ---- # ---- documents ----
fn set_text(D: Doc, raw: ptr) -> void { function set_text(D: Doc, raw: ptr) -> void {
D.raw = raw D.raw = raw
if D.is_md == 1 { D.text = scrub_markdown(raw) } else { D.text = raw } if D.is_md == 1 { D.text = scrub_markdown(raw) } else { D.text = raw }
D.src = D.text D.src = D.text
} }
fn reindex(D: Doc) -> void { function reindex(D: Doc) -> void {
lex_doc(D) lex_doc(D)
match_braces(D) match_braces(D)
parse_doc(D) parse_doc(D)
@ -988,7 +988,7 @@ program LudicLsp {
let N = ntok(D) let N = ntok(D)
while i < N { push(D.cls, SC_NONE); i = i + 1 } while i < N { push(D.cls, SC_NONE); i = i + 1 }
} }
fn new_doc(path: ptr, text: ptr) -> Doc { function new_doc(path: ptr, text: ptr) -> Doc {
let D = new Doc let D = new Doc
D.path = normpath(path) D.path = normpath(path)
D.uri = path_to_uri(D.path) D.uri = path_to_uri(D.path)
@ -999,13 +999,13 @@ program LudicLsp {
reindex(D) reindex(D)
return D return D
} }
fn by_path(path: ptr) -> Doc { function by_path(path: ptr) -> Doc {
let p = normpath(path) let p = normpath(path)
var i = 0 var i = 0
while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i = i + 1 } while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i = i + 1 }
return null return null
} }
fn by_uri(uri: ptr) -> Doc { function by_uri(uri: ptr) -> Doc {
let p = uri_to_path(uri) let p = uri_to_path(uri)
let D = by_path(p) let D = by_path(p)
if (D != null) { return D } if (D != null) { return D }
@ -1013,7 +1013,7 @@ program LudicLsp {
while i < len(g_docs) { if (g_docs[i].uri == uri) { return g_docs[i] }; i = i + 1 } while i < len(g_docs) { if (g_docs[i].uri == uri) { return g_docs[i] }; i = i + 1 }
return null return null
} }
fn ensure_doc(path: ptr) -> Doc { function ensure_doc(path: ptr) -> Doc {
let D = by_path(path) let D = by_path(path)
if (D != null) { return D } if (D != null) { return D }
let text = read_file(path) let text = read_file(path)
@ -1023,7 +1023,7 @@ program LudicLsp {
return nd return nd
} }
fn scan_root(root: ptr) -> void { function scan_root(root: ptr) -> void {
if (root == null) { return } if (root == null) { return }
let listfile = "/tmp/ludic_lsp_scan.txt" let listfile = "/tmp/ludic_lsp_scan.txt"
run(concat3(concat3("find '", root, "' -name '*.ludic' -not -path '*/build/*' -not -path '*/node_modules/*' -not -path '*/.*' > "), listfile, " 2>/dev/null")) run(concat3(concat3("find '", root, "' -name '*.ludic' -not -path '*/build/*' -not -path '*/node_modules/*' -not -path '*/.*' > "), listfile, " 2>/dev/null"))
@ -1041,7 +1041,7 @@ program LudicLsp {
var g_owner: Doc = null var g_owner: Doc = null
var g_symidx: int = 0 var g_symidx: int = 0
fn imports_of(D: Doc, out: []Doc) -> void { function imports_of(D: Doc, out: []Doc) -> void {
var i = 0 var i = 0
while i < len(D.imports) { while i < len(D.imports) {
let full = join_path(dirname(D.path), D.imports[i]) let full = join_path(dirname(D.path), D.imports[i])
@ -1055,7 +1055,7 @@ program LudicLsp {
i = i + 1 i = i + 1
} }
} }
fn related(D: Doc) -> []Doc { function related(D: Doc) -> []Doc {
let out = new []Doc let out = new []Doc
if (D == null) { return out } if (D == null) { return out }
push(out, D) push(out, D)
@ -1088,7 +1088,7 @@ program LudicLsp {
} }
return out return out
} }
fn unit_root(D: Doc) -> Doc { function unit_root(D: Doc) -> Doc {
if (D == null) or D.is_md == 1 { return null } if (D == null) or D.is_md == 1 { return null }
if D.is_unit == 1 { return D } if D.is_unit == 1 { return D }
let rel = related(D) let rel = related(D)
@ -1096,7 +1096,7 @@ program LudicLsp {
while i < len(rel) { if rel[i].is_unit == 1 and rel[i].is_md == 0 { return rel[i] }; i = i + 1 } while i < len(rel) { if rel[i].is_unit == 1 and rel[i].is_md == 0 { return rel[i] }; i = i + 1 }
return null return null
} }
fn find_local(D: Doc, name: ptr, off: int) -> int { function find_local(D: Doc, name: ptr, off: int) -> int {
var best = 0 - 1 var best = 0 - 1
var i = 0 var i = 0
while i < len(D.syms) { while i < len(D.syms) {
@ -1109,7 +1109,7 @@ program LudicLsp {
return best return best
} }
# returns sym index in g_owner (set), or -1 # returns sym index in g_owner (set), or -1
fn find_top(D: Doc, name: ptr) -> int { function find_top(D: Doc, name: ptr) -> int {
let rel = related(D) let rel = related(D)
var i = 0 var i = 0
while i < len(rel) { while i < len(rel) {
@ -1132,7 +1132,7 @@ program LudicLsp {
return 0 - 1 return 0 - 1
} }
# component sym index (in g_owner) for a dotted receiver, or -1 # component sym index (in g_owner) for a dotted receiver, or -1
fn receiver_component(D: Doc, dot_tok: int) -> int { function receiver_component(D: Doc, dot_tok: int) -> int {
let recv = prev_sig(D, dot_tok) let recv = prev_sig(D, dot_tok)
if recv < 0 or D.tk_kind[recv] != LT_ID { return 0 - 1 } if recv < 0 or D.tk_kind[recv] != LT_ID { return 0 - 1 }
let name = ttext(D, recv) let name = ttext(D, recv)
@ -1145,7 +1145,7 @@ program LudicLsp {
return find_top(D, ty) return find_top(D, ty)
} }
# resolve the token: sets g_owner/g_symidx; returns the token index (or -1) # resolve the token: sets g_owner/g_symidx; returns the token index (or -1)
fn resolve_tok(D: Doc, tok: int) -> int { function resolve_tok(D: Doc, tok: int) -> int {
g_owner = null; g_symidx = 0 - 1 g_owner = null; g_symidx = 0 - 1
if tok < 0 { return 0 - 1 } if tok < 0 { return 0 - 1 }
if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return 0 - 1 } if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return 0 - 1 }
@ -1173,10 +1173,10 @@ program LudicLsp {
g_owner = null; g_symidx = 0 - 1 g_owner = null; g_symidx = 0 - 1
return tok return tok
} }
fn resolve_at(D: Doc, off: int) -> int { return resolve_tok(D, tok_at(D, off)) } function resolve_at(D: Doc, off: int) -> int { return resolve_tok(D, tok_at(D, off)) }
fn first_on_line(D: Doc, tok: int) -> bool { let p = prev_sig(D, tok); return p < 0 or D.tk_line[p] != D.tk_line[tok] } function first_on_line(D: Doc, tok: int) -> bool { let p = prev_sig(D, tok); return p < 0 or D.tk_line[p] != D.tk_line[tok] }
fn in_ui_body(D: Doc, off: int) -> bool { function in_ui_body(D: Doc, off: int) -> bool {
var i = 0 var i = 0
while i < len(D.syms) { if D.syms[i].kind == LS_UI and off > D.syms[i].bstart and off < D.syms[i].bend { return true }; i = i + 1 } while i < len(D.syms) { if D.syms[i].kind == LS_UI and off > D.syms[i].bstart and off < D.syms[i].bend { return true }; i = i + 1 }
return false return false
@ -1185,16 +1185,16 @@ program LudicLsp {
# ---- builtins / intrinsics (name -> signature) ---- # ---- builtins / intrinsics (name -> signature) ----
var g_builtins: []ptr var g_builtins: []ptr
var g_bsigs: []ptr var g_bsigs: []ptr
fn badd(name: ptr, sig: ptr) -> void { push(g_builtins, name); push(g_bsigs, sig) } function badd(name: ptr, sig: ptr) -> void { push(g_builtins, name); push(g_bsigs, sig) }
fn is_builtin_name(name: ptr) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i = i + 1 }; return false } function is_builtin_name(name: ptr) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i = i + 1 }; return false }
fn builtin_sig(name: ptr) -> ptr { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null } function builtin_sig(name: ptr) -> ptr { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null }
# The namespaced standard-library surface: `Namespace.method`, mirroring the # The namespaced standard-library surface: `Namespace.method`, mirroring the
# compiler's emit_ns_call (selfhost/emit_expr.ludic). Screen/Random/Input/Map # compiler's emit_ns_call (selfhost/emit_expr.ludic). Screen/Random/Input/Map
# methods resolve to the underlying bare builtin's signature; Math/Text/List # methods resolve to the underlying bare builtin's signature; Math/Text/List
# are pure fixed-point / string / slice ops with their own signatures. Returns # are pure fixed-point / string / slice ops with their own signatures. Returns
# the signature label for signature help, or null if it is not a known method. # the signature label for signature help, or null if it is not a known method.
fn ns_method_sig(ns: ptr, meth: ptr) -> ptr { function ns_method_sig(ns: ptr, meth: ptr) -> ptr {
if (ns == "Screen") { if (ns == "Screen") {
if (meth == "clear") { return builtin_sig("clear") } if (meth == "clear") { return builtin_sig("clear") }
if (meth == "fill_rectangle") { return builtin_sig("fill_rect") } if (meth == "fill_rectangle") { return builtin_sig("fill_rect") }
@ -1395,7 +1395,7 @@ program LudicLsp {
} }
return null return null
} }
fn init_builtins() -> void { function init_builtins() -> void {
g_builtins = new []ptr; g_bsigs = new []ptr g_builtins = new []ptr; g_bsigs = new []ptr
badd("min", "min(a: int, b: int) -> int") badd("min", "min(a: int, b: int) -> int")
badd("max", "max(a: int, b: int) -> int") badd("max", "max(a: int, b: int) -> int")
@ -1478,7 +1478,7 @@ program LudicLsp {
} }
# ---- classification ---- # ---- classification ----
fn class_of_symkind(k: int) -> int { function class_of_symkind(k: int) -> int {
if k == LS_COMPONENT { return SC_COMPONENT } if k == LS_COMPONENT { return SC_COMPONENT }
if k == LS_ARCHETYPE { return SC_ARCHETYPE } if k == LS_ARCHETYPE { return SC_ARCHETYPE }
if k == LS_FIELD { return SC_FIELD } if k == LS_FIELD { return SC_FIELD }
@ -1495,7 +1495,7 @@ program LudicLsp {
if k == LS_UNIT { return SC_UI } if k == LS_UNIT { return SC_UI }
return SC_UNKNOWN return SC_UNKNOWN
} }
fn classify(D: Doc) -> void { function classify(D: Doc) -> void {
let N = ntok(D) let N = ntok(D)
if len(D.cls) < N { D.cls = new []int; var z = 0; while z < N { push(D.cls, SC_NONE); z = z + 1 } } if len(D.cls) < N { D.cls = new []int; var z = 0; while z < N { push(D.cls, SC_NONE); z = z + 1 } }
var i = 0 var i = 0
@ -1542,7 +1542,7 @@ program LudicLsp {
# semantic token type index + modifiers (via g_semmods) # semantic token type index + modifiers (via g_semmods)
var g_semmods: int = 0 var g_semmods: int = 0
fn sem_type_of(cls: int) -> int { function sem_type_of(cls: int) -> int {
g_semmods = 0 g_semmods = 0
if cls == SC_KEYWORD { return 0 } if cls == SC_KEYWORD { return 0 }
if cls == SC_TYPE { g_semmods = 4; return 1 } if cls == SC_TYPE { g_semmods = 4; return 1 }
@ -1580,23 +1580,23 @@ program LudicLsp {
const JARR: int = 4 const JARR: int = 4
const JOBJ: int = 5 const JOBJ: int = 5
property JVal { t: int = 0, num: int = 0, b: int = 0, s: ptr = null, kids: []JVal, keys: []ptr } property JVal { t: int = 0, num: int = 0, b: int = 0, s: ptr = null, kids: []JVal, keys: []ptr }
fn jval(t: int) -> JVal { let v = new JVal; v.t = t; v.kids = new []JVal; v.keys = new []ptr; return v } function jval(t: int) -> JVal { let v = new JVal; v.t = t; v.kids = new []JVal; v.keys = new []ptr; return v }
var jsrc: ptr = null var jsrc: ptr = null
var jp: int = 0 var jp: int = 0
fn jskip() -> void { while jsrc[jp] == 32 or jsrc[jp] == 9 or jsrc[jp] == 10 or jsrc[jp] == 13 { jp = jp + 1 } } function jskip() -> void { while jsrc[jp] == 32 or jsrc[jp] == 9 or jsrc[jp] == 10 or jsrc[jp] == 13 { jp = jp + 1 } }
fn jhex4(s: ptr, at: int) -> int { function jhex4(s: ptr, at: int) -> int {
var v = 0; var i = 0 var v = 0; var i = 0
while i < 4 { let h = hexval(s[at + i]); if h < 0 { return v }; v = v * 16 + h; i = i + 1 } while i < 4 { let h = hexval(s[at + i]); if h < 0 { return v }; v = v * 16 + h; i = i + 1 }
return v return v
} }
fn utf8_emit(o: Buf, cp: int) -> void { function utf8_emit(o: Buf, cp: int) -> void {
if cp < 128 { buf_putc(o, cp) } if cp < 128 { buf_putc(o, cp) }
else { if cp < 2048 { buf_putc(o, 192 | (cp >> 6)); buf_putc(o, 128 | (cp & 63)) } else { if cp < 2048 { buf_putc(o, 192 | (cp >> 6)); buf_putc(o, 128 | (cp & 63)) }
else { if cp < 65536 { buf_putc(o, 224 | (cp >> 12)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } else { if cp < 65536 { buf_putc(o, 224 | (cp >> 12)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) }
else { buf_putc(o, 240 | (cp >> 18)); buf_putc(o, 128 | ((cp >> 12) & 63)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } } } else { buf_putc(o, 240 | (cp >> 18)); buf_putc(o, 128 | ((cp >> 12) & 63)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } } }
} }
fn jparse_string() -> ptr { function jparse_string() -> ptr {
jp = jp + 1 # skip opening quote jp = jp + 1 # skip opening quote
let o = buf_new() let o = buf_new()
while jsrc[jp] != 0 and jsrc[jp] != 34 { while jsrc[jp] != 0 and jsrc[jp] != 34 {
@ -1621,7 +1621,7 @@ program LudicLsp {
if jsrc[jp] == 34 { jp = jp + 1 } if jsrc[jp] == 34 { jp = jp + 1 }
return buf_str(o) return buf_str(o)
} }
fn jparse() -> JVal { function jparse() -> JVal {
jskip() jskip()
let c = jsrc[jp] let c = jsrc[jp]
if c == 123 { # '{' if c == 123 { # '{'
@ -1671,18 +1671,18 @@ program LudicLsp {
jp = jp + 1 jp = jp + 1
return jval(JNULL) return jval(JNULL)
} }
fn jget(v: JVal, key: ptr) -> JVal { function jget(v: JVal, key: ptr) -> JVal {
if (v == null) { return null } if (v == null) { return null }
if v.t != JOBJ { return null } if v.t != JOBJ { return null }
var i = 0 var i = 0
while i < len(v.keys) { if (v.keys[i] == key) { return v.kids[i] }; i = i + 1 } while i < len(v.keys) { if (v.keys[i] == key) { return v.kids[i] }; i = i + 1 }
return null return null
} }
fn jat(v: JVal, i: int) -> JVal { if (v == null) { return null }; if i < 0 or i >= len(v.kids) { return null }; return v.kids[i] } function jat(v: JVal, i: int) -> JVal { if (v == null) { return null }; if i < 0 or i >= len(v.kids) { return null }; return v.kids[i] }
fn jlen(v: JVal) -> int { if (v == null) { return 0 }; return len(v.kids) } function jlen(v: JVal) -> int { if (v == null) { return 0 }; return len(v.kids) }
fn jstr(v: JVal, def: ptr) -> ptr { if (v != null) and v.t == JSTR and (v.s != null) { return v.s }; return def } function jstr(v: JVal, def: ptr) -> ptr { if (v != null) and v.t == JSTR and (v.s != null) { return v.s }; return def }
fn jint(v: JVal, def: int) -> int { if (v != null) and v.t == JNUM { return v.num }; return def } function jint(v: JVal, def: int) -> int { if (v != null) and v.t == JNUM { return v.num }; return def }
fn jpath(v: JVal, path: ptr) -> JVal { function jpath(v: JVal, path: ptr) -> JVal {
var cur = v var cur = v
let n = cstr_len(path) let n = cstr_len(path)
var start = 0; var i = 0 var start = 0; var i = 0
@ -1701,8 +1701,8 @@ program LudicLsp {
# ============================================================================ # ============================================================================
# the formatter (Doc-based; reused for LSP formatting) — mirrors fmt.ludic # the formatter (Doc-based; reused for LSP formatting) — mirrors fmt.ludic
# ============================================================================ # ============================================================================
fn f_name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL } function f_name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL }
fn f_dot_tight(F: Doc, t: int) -> bool { function f_dot_tight(F: Doc, t: int) -> bool {
if f_name_like(F.tk_kind[t]) { return true } if f_name_like(F.tk_kind[t]) { return true }
if F.tk_kind[t] != LT_OP { return false } if F.tk_kind[t] != LT_OP { return false }
let n = tlen(F, t); let c0 = F.src[F.tk_start[t]] let n = tlen(F, t); let c0 = F.src[F.tk_start[t]]
@ -1710,7 +1710,7 @@ program LudicLsp {
if n == 2 and c0 == 46 and F.src[F.tk_start[t] + 1] == 46 { return true } if n == 2 and c0 == 46 and F.src[F.tk_start[t] + 1] == 46 { return true }
return false return false
} }
fn f_is_unary(F: Doc, i: int) -> bool { function f_is_unary(F: Doc, i: int) -> bool {
if F.tk_kind[i] != LT_OP { return false } if F.tk_kind[i] != LT_OP { return false }
let n = tlen(F, i) let n = tlen(F, i)
if not (n == 1 and (F.src[F.tk_start[i]] == 45 or F.src[F.tk_start[i]] == 33)) { return false } if not (n == 1 and (F.src[F.tk_start[i]] == 45 or F.src[F.tk_start[i]] == 33)) { return false }
@ -1721,7 +1721,7 @@ program LudicLsp {
if pk2 == LT_OP { let c = F.src[F.tk_start[p]]; return not (tlen(F, p) == 1 and (c == 41 or c == 93 or c == 125)) } if pk2 == LT_OP { let c = F.src[F.tk_start[p]]; return not (tlen(F, p) == 1 and (c == 41 or c == 93 or c == 125)) }
return true return true
} }
fn f_space_before(F: Doc, prev: int, cur: int) -> int { function f_space_before(F: Doc, prev: int, cur: int) -> int {
if prev < 0 { return 0 } if prev < 0 { return 0 }
let pk2 = F.tk_kind[prev]; let ck = F.tk_kind[cur] let pk2 = F.tk_kind[prev]; let ck = F.tk_kind[cur]
let plen = tlen(F, prev); let clen = tlen(F, cur) let plen = tlen(F, prev); let clen = tlen(F, cur)
@ -1741,16 +1741,16 @@ program LudicLsp {
} }
return 1 return 1
} }
fn f_is_clause(w: ptr) -> bool { function f_is_clause(w: ptr) -> bool {
return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects") return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects")
} }
fn make_text_doc(text: ptr) -> Doc { function make_text_doc(text: ptr) -> Doc {
let F = new Doc let F = new Doc
F.raw = text; F.text = text; F.src = text; F.is_md = 0 F.raw = text; F.text = text; F.src = text; F.is_md = 0
lex_doc(F) lex_doc(F)
return F return F
} }
fn format_doc(F: Doc, iw: int) -> ptr { function format_doc(F: Doc, iw: int) -> ptr {
let o = buf_new() let o = buf_new()
let stack = words(600) let stack = words(600)
let hang = words(600) let hang = words(600)
@ -1855,7 +1855,7 @@ program LudicLsp {
var mf_flen: int = 0 var mf_flen: int = 0
var mf_info: int = 0 var mf_info: int = 0
var mf_marker: int = 0 var mf_marker: int = 0
fn mf_fence_at(s: ptr, i: int) -> bool { function mf_fence_at(s: ptr, i: int) -> bool {
var j = i; while s[j] == 32 { j = j + 1 } var j = i; while s[j] == 32 { j = j + 1 }
let m = s[j] let m = s[j]
if m != 96 and m != 126 { return false } if m != 96 and m != 126 { return false }
@ -1864,8 +1864,8 @@ program LudicLsp {
mf_flen = n; mf_info = j; mf_marker = m mf_flen = n; mf_info = j; mf_marker = m
return true return true
} }
fn f_line_end(s: ptr, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e } function f_line_end(s: ptr, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e }
fn format_md(s: ptr, iw: int) -> ptr { function format_md(s: ptr, iw: int) -> ptr {
let o = buf_new() let o = buf_new()
var i = 0 var i = 0
while s[i] != 0 { while s[i] != 0 {
@ -1925,14 +1925,14 @@ program LudicLsp {
} }
return buf_str(o) return buf_str(o)
} }
fn skip_ws2(s: ptr, at: int) -> int { var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 }; return a } function skip_ws2(s: ptr, at: int) -> int { var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 }; return a }
# ============================================================================ # ============================================================================
# framing + output # framing + output
# ============================================================================ # ============================================================================
extern fn fflush(f: ptr) -> int = "fflush" extern function fflush(f: ptr) -> int = "fflush"
fn jstr_out(o: Buf, z: ptr) -> void { function jstr_out(o: Buf, z: ptr) -> void {
buf_putc(o, 34) buf_putc(o, 34)
if (z == null) { buf_putc(o, 34); return } if (z == null) { buf_putc(o, 34); return }
var i = 0 var i = 0
@ -1950,7 +1950,7 @@ program LudicLsp {
buf_putc(o, 34) buf_putc(o, 34)
} }
fn send_raw(body: ptr) -> void { function send_raw(body: ptr) -> void {
let hdr = buf_new() let hdr = buf_new()
buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, 13); buf_putc(hdr, 10); buf_putc(hdr, 13); buf_putc(hdr, 10) buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, 13); buf_putc(hdr, 10); buf_putc(hdr, 13); buf_putc(hdr, 10)
let hs = buf_str(hdr) let hs = buf_str(hdr)
@ -1959,7 +1959,7 @@ program LudicLsp {
file_write(out, body, cstr_len(body)) file_write(out, body, cstr_len(body))
let z = fflush(out) let z = fflush(out)
} }
fn send_result(idv: JVal, result: ptr) -> void { function send_result(idv: JVal, result: ptr) -> void {
let o = buf_new() let o = buf_new()
buf_puts(o, "{\"jsonrpc\":\"2.0\",\"id\":") buf_puts(o, "{\"jsonrpc\":\"2.0\",\"id\":")
if (idv == null) { buf_puts(o, "null") } if (idv == null) { buf_puts(o, "null") }
@ -1969,19 +1969,19 @@ program LudicLsp {
buf_putc(o, 125) buf_putc(o, 125)
send_raw(buf_str(o)) send_raw(buf_str(o))
} }
fn send_notify(method: ptr, params: ptr) -> void { function send_notify(method: ptr, params: ptr) -> void {
let o = buf_new() let o = buf_new()
buf_puts(o, "{\"jsonrpc\":\"2.0\",\"method\":"); jstr_out(o, method); buf_puts(o, ",\"params\":"); buf_puts(o, params); buf_putc(o, 125) buf_puts(o, "{\"jsonrpc\":\"2.0\",\"method\":"); jstr_out(o, method); buf_puts(o, ",\"params\":"); buf_puts(o, params); buf_putc(o, 125)
send_raw(buf_str(o)) send_raw(buf_str(o))
} }
fn lower(c: int) -> int { if c >= 65 and c <= 90 { return c + 32 }; return c } function lower(c: int) -> int { if c >= 65 and c <= 90 { return c + 32 }; return c }
fn starts_with_ci(s: ptr, pre: ptr) -> bool { function starts_with_ci(s: ptr, pre: ptr) -> bool {
var i = 0 var i = 0
while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i = i + 1 } while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i = i + 1 }
return true return true
} }
fn read_message() -> ptr { function read_message() -> ptr {
var length = 0 - 1 var length = 0 - 1
let line = buf_new() let line = buf_new()
while true { while true {
@ -2004,26 +2004,26 @@ program LudicLsp {
} }
# ---- position output ---- # ---- position output ----
fn put_pos(o: Buf, D: Doc, off: int) -> void { buf_puts(o, "{\"line\":"); buf_puti(o, line_of(D, off)); buf_puts(o, ",\"character\":"); buf_puti(o, col_of(D, off)); buf_putc(o, 125) } function put_pos(o: Buf, D: Doc, off: int) -> void { buf_puts(o, "{\"line\":"); buf_puti(o, line_of(D, off)); buf_puts(o, ",\"character\":"); buf_puti(o, col_of(D, off)); buf_putc(o, 125) }
fn put_range(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"start\":"); put_pos(o, D, a); buf_puts(o, ",\"end\":"); put_pos(o, D, b); buf_putc(o, 125) } function put_range(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"start\":"); put_pos(o, D, a); buf_puts(o, ",\"end\":"); put_pos(o, D, b); buf_putc(o, 125) }
fn put_location(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"range\":"); put_range(o, D, a, b); buf_putc(o, 125) } function put_location(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"range\":"); put_range(o, D, a, b); buf_putc(o, 125) }
fn doc_of(msg: JVal) -> Doc { function doc_of(msg: JVal) -> Doc {
let uri = jstr(jpath(msg, "params.textDocument.uri"), null) let uri = jstr(jpath(msg, "params.textDocument.uri"), null)
if (uri == null) { return null } if (uri == null) { return null }
let D = by_uri(uri) let D = by_uri(uri)
if (D != null) { return D } if (D != null) { return D }
return ensure_doc(uri_to_path(uri)) return ensure_doc(uri_to_path(uri))
} }
fn off_of(msg: JVal, D: Doc, base: ptr) -> int { function off_of(msg: JVal, D: Doc, base: ptr) -> int {
let line = jint(jpath(msg, concat3(base, ".line", "")), 0) let line = jint(jpath(msg, concat3(base, ".line", "")), 0)
let ch = jint(jpath(msg, concat3(base, ".character", "")), 0) let ch = jint(jpath(msg, concat3(base, ".character", "")), 0)
return offset_of(D, line, ch) return offset_of(D, line, ch)
} }
fn basename(p: ptr) -> ptr { var last = 0 - 1; var i = 0; while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 }; return p[last + 1..i] } function basename(p: ptr) -> ptr { var last = 0 - 1; var i = 0; while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 }; return p[last + 1..i] }
# ---- LSP symbol kinds & completion kinds ---- # ---- LSP symbol kinds & completion kinds ----
fn lsp_symbol_kind(k: int) -> int { function lsp_symbol_kind(k: int) -> int {
if k == LS_UNIT { return 2 } if k == LS_UNIT { return 2 }
if k == LS_COMPONENT { return 23 } if k == LS_COMPONENT { return 23 }
if k == LS_FIELD { return 8 } if k == LS_FIELD { return 8 }
@ -2038,7 +2038,7 @@ program LudicLsp {
if k == LS_LAYER { return 3 } if k == LS_LAYER { return 3 }
return 13 return 13
} }
fn comp_kind_of(k: int) -> int { function comp_kind_of(k: int) -> int {
if k == LS_COMPONENT { return 22 } if k == LS_COMPONENT { return 22 }
if k == LS_ARCHETYPE { return 7 } if k == LS_ARCHETYPE { return 7 }
if k == LS_CONST { return 21 } if k == LS_CONST { return 21 }
@ -2054,7 +2054,7 @@ program LudicLsp {
} }
# ---- diagnostics (structural) ---- # ---- diagnostics (structural) ----
fn diag_structural(D: Doc, o: Buf) -> void { function diag_structural(D: Doc, o: Buf) -> void {
let ds_s = new []int; let ds_e = new []int; let ds_m = new []ptr let ds_s = new []int; let ds_e = new []int; let ds_m = new []ptr
let stack = words(1200); var top = 0 let stack = words(1200); var top = 0
var i = 0; let N = ntok(D) var i = 0; let N = ntok(D)
@ -2081,7 +2081,7 @@ program LudicLsp {
j = j + 1 j = j + 1
} }
} }
fn publish_diag(D: Doc) -> void { function publish_diag(D: Doc) -> void {
let o = buf_new() let o = buf_new()
buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"diagnostics\":[") buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"diagnostics\":[")
if D.is_md == 0 { diag_structural(D, o) } if D.is_md == 0 { diag_structural(D, o) }
@ -2090,7 +2090,7 @@ program LudicLsp {
} }
# ---- document symbols ---- # ---- document symbols ----
fn put_doc_symbols(o: Buf, D: Doc, parent: int) -> void { function put_doc_symbols(o: Buf, D: Doc, parent: int) -> void {
buf_putc(o, 91) buf_putc(o, 91)
var first = 1 var first = 1
var i = 0 var i = 0
@ -2116,13 +2116,13 @@ program LudicLsp {
} }
# ---- hover ---- # ---- hover ----
fn hover_md(o: Buf, code: ptr, docstr: ptr) -> void { function hover_md(o: Buf, code: ptr, docstr: ptr) -> void {
let m = buf_new() let m = buf_new()
buf_puts(m, "```ludic\n"); buf_puts(m, code); buf_puts(m, "\n```") buf_puts(m, "```ludic\n"); buf_puts(m, code); buf_puts(m, "\n```")
if (docstr != null) and (docstr[0] != 0) { buf_puts(m, "\n\n"); buf_puts(m, docstr) } if (docstr != null) and (docstr[0] != 0) { buf_puts(m, "\n\n"); buf_puts(m, docstr) }
buf_puts(o, "{\"contents\":{\"kind\":\"markdown\",\"value\":"); jstr_out(o, buf_str(m)); buf_puts(o, "}}") buf_puts(o, "{\"contents\":{\"kind\":\"markdown\",\"value\":"); jstr_out(o, buf_str(m)); buf_puts(o, "}}")
} }
fn on_hover(msg: JVal, id: JVal) -> void { function on_hover(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return } if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position") let off = off_of(msg, D, "params.position")
@ -2147,7 +2147,7 @@ program LudicLsp {
} }
# ---- definition ---- # ---- definition ----
fn on_definition(msg: JVal, id: JVal) -> void { function on_definition(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return } if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position") let off = off_of(msg, D, "params.position")
@ -2172,7 +2172,7 @@ program LudicLsp {
# ---- occurrences (references / rename) ---- # ---- occurrences (references / rename) ----
var g_occ_doc: []Doc var g_occ_doc: []Doc
var g_occ_tok: []int var g_occ_tok: []int
fn find_occurrences(from: Doc, tdoc: Doc, tsym: int) -> void { function find_occurrences(from: Doc, tdoc: Doc, tsym: int) -> void {
g_occ_doc = new []Doc; g_occ_tok = new []int g_occ_doc = new []Doc; g_occ_tok = new []int
let want = tdoc.syms[tsym].name let want = tdoc.syms[tsym].name
var docs = new []Doc var docs = new []Doc
@ -2191,7 +2191,7 @@ program LudicLsp {
i = i + 1 i = i + 1
} }
} }
fn on_references(msg: JVal, id: JVal) -> void { function on_references(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "[]"); return } if (D == null) { send_result(id, "[]"); return }
let off = off_of(msg, D, "params.position") let off = off_of(msg, D, "params.position")
@ -2209,7 +2209,7 @@ program LudicLsp {
} }
buf_putc(o, 93); send_result(id, buf_str(o)) buf_putc(o, 93); send_result(id, buf_str(o))
} }
fn on_rename(msg: JVal, id: JVal) -> void { function on_rename(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
let newname = jstr(jpath(msg, "params.newName"), null) let newname = jstr(jpath(msg, "params.newName"), null)
if (D == null) or (newname == null) or (newname[0] == 0) { send_result(id, "null"); return } if (D == null) or (newname == null) or (newname[0] == 0) { send_result(id, "null"); return }
@ -2250,14 +2250,14 @@ program LudicLsp {
# ---- completion ---- # ---- completion ----
var g_first: int = 1 var g_first: int = 1
fn comp_item(o: Buf, label: ptr, kind: int, detail: ptr) -> void { function comp_item(o: Buf, label: ptr, kind: int, detail: ptr) -> void {
if g_first == 0 { buf_putc(o, 44) } if g_first == 0 { buf_putc(o, 44) }
g_first = 0 g_first = 0
buf_puts(o, "{\"label\":"); jstr_out(o, label); buf_puts(o, ",\"kind\":"); buf_puti(o, kind) buf_puts(o, "{\"label\":"); jstr_out(o, label); buf_puts(o, ",\"kind\":"); buf_puti(o, kind)
if (detail != null) and (detail[0] != 0) { buf_puts(o, ",\"detail\":"); jstr_out(o, detail) } if (detail != null) and (detail[0] != 0) { buf_puts(o, ",\"detail\":"); jstr_out(o, detail) }
buf_putc(o, 125) buf_putc(o, 125)
} }
fn comp_scope(o: Buf, D: Doc, off: int, types_only: int) -> void { function comp_scope(o: Buf, D: Doc, off: int, types_only: int) -> void {
if types_only == 0 { if types_only == 0 {
var i = 0 var i = 0
while i < len(D.syms) { while i < len(D.syms) {
@ -2285,12 +2285,12 @@ program LudicLsp {
r = r + 1 r = r + 1
} }
} }
fn comp_types(o: Buf) -> void { function comp_types(o: Buf) -> void {
comp_item(o, "int", 14, "built-in type"); comp_item(o, "long", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type") comp_item(o, "int", 14, "built-in type"); comp_item(o, "long", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type")
comp_item(o, "entity", 14, "built-in type"); comp_item(o, "str", 14, "built-in type"); comp_item(o, "ptr", 14, "built-in type"); comp_item(o, "void", 14, "built-in type") comp_item(o, "entity", 14, "built-in type"); comp_item(o, "str", 14, "built-in type"); comp_item(o, "ptr", 14, "built-in type"); comp_item(o, "void", 14, "built-in type")
} }
fn comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i = i + 1 } } function comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i = i + 1 } }
fn on_completion(msg: JVal, id: JVal) -> void { function on_completion(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "{\"isIncomplete\":false,\"items\":[]}"); return } if (D == null) { send_result(id, "{\"isIncomplete\":false,\"items\":[]}"); return }
let off = off_of(msg, D, "params.position") let off = off_of(msg, D, "params.position")
@ -2350,7 +2350,7 @@ program LudicLsp {
} }
# ---- inlay hints, signature help, semantic tokens, folding ---- # ---- inlay hints, signature help, semantic tokens, folding ----
fn on_inlay_hint(msg: JVal, id: JVal) -> void { function on_inlay_hint(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "[]"); return } if (D == null) { send_result(id, "[]"); return }
let o = buf_new(); buf_putc(o, 91) let o = buf_new(); buf_putc(o, 91)
@ -2367,7 +2367,7 @@ program LudicLsp {
} }
buf_putc(o, 93); send_result(id, buf_str(o)) buf_putc(o, 93); send_result(id, buf_str(o))
} }
fn on_signature_help(msg: JVal, id: JVal) -> void { function on_signature_help(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return } if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position") let off = off_of(msg, D, "params.position")
@ -2412,7 +2412,7 @@ program LudicLsp {
buf_puts(o, "}],\"activeSignature\":0,\"activeParameter\":"); buf_puti(o, arg); buf_putc(o, 125) buf_puts(o, "}],\"activeSignature\":0,\"activeParameter\":"); buf_puti(o, arg); buf_putc(o, 125)
send_result(id, buf_str(o)) send_result(id, buf_str(o))
} }
fn on_semantic_tokens(msg: JVal, id: JVal) -> void { function on_semantic_tokens(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return } if (D == null) { send_result(id, "null"); return }
classify(D) classify(D)
@ -2442,7 +2442,7 @@ program LudicLsp {
} }
buf_puts(o, "]}"); send_result(id, buf_str(o)) buf_puts(o, "]}"); send_result(id, buf_str(o))
} }
fn on_folding(msg: JVal, id: JVal) -> void { function on_folding(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "[]"); return } if (D == null) { send_result(id, "[]"); return }
let o = buf_new(); buf_putc(o, 91); var first = 1 let o = buf_new(); buf_putc(o, 91); var first = 1
@ -2463,7 +2463,7 @@ program LudicLsp {
} }
buf_putc(o, 93); send_result(id, buf_str(o)) buf_putc(o, 93); send_result(id, buf_str(o))
} }
fn on_workspace_symbol(msg: JVal, id: JVal) -> void { function on_workspace_symbol(msg: JVal, id: JVal) -> void {
let q = jstr(jpath(msg, "params.query"), "") let q = jstr(jpath(msg, "params.query"), "")
let o = buf_new(); buf_putc(o, 91); var first = 1 let o = buf_new(); buf_putc(o, 91); var first = 1
var d = 0 var d = 0
@ -2487,7 +2487,7 @@ program LudicLsp {
} }
buf_putc(o, 93); send_result(id, buf_str(o)) buf_putc(o, 93); send_result(id, buf_str(o))
} }
fn contains_ci(hay: ptr, needle: ptr) -> bool { function contains_ci(hay: ptr, needle: ptr) -> bool {
let hn = cstr_len(hay); let nn = cstr_len(needle) let hn = cstr_len(hay); let nn = cstr_len(needle)
if nn == 0 { return true } if nn == 0 { return true }
var i = 0 var i = 0
@ -2500,7 +2500,7 @@ program LudicLsp {
} }
return false return false
} }
fn on_formatting(msg: JVal, id: JVal) -> void { function on_formatting(msg: JVal, id: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return } if (D == null) { send_result(id, "null"); return }
var tab = jint(jpath(msg, "params.options.tabSize"), 2) var tab = jint(jpath(msg, "params.options.tabSize"), 2)
@ -2518,7 +2518,7 @@ program LudicLsp {
} }
# ---- document lifecycle ---- # ---- document lifecycle ----
fn on_did_open(msg: JVal) -> void { function on_did_open(msg: JVal) -> void {
let uri = jstr(jpath(msg, "params.textDocument.uri"), null) let uri = jstr(jpath(msg, "params.textDocument.uri"), null)
let text = jstr(jpath(msg, "params.textDocument.text"), "") let text = jstr(jpath(msg, "params.textDocument.text"), "")
if (uri == null) { return } if (uri == null) { return }
@ -2529,7 +2529,7 @@ program LudicLsp {
D.isopen = 1 D.isopen = 1
publish_diag(D) publish_diag(D)
} }
fn on_did_change(msg: JVal) -> void { function on_did_change(msg: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { return } if (D == null) { return }
let changes = jpath(msg, "params.contentChanges") let changes = jpath(msg, "params.contentChanges")
@ -2540,7 +2540,7 @@ program LudicLsp {
reindex(D) reindex(D)
publish_diag(D) publish_diag(D)
} }
fn on_did_close(msg: JVal) -> void { function on_did_close(msg: JVal) -> void {
let D = doc_of(msg) let D = doc_of(msg)
if (D == null) { return } if (D == null) { return }
D.isopen = 0 D.isopen = 0
@ -2548,7 +2548,7 @@ program LudicLsp {
if (disk != null) { set_text(D, disk); reindex(D) } if (disk != null) { set_text(D, disk); reindex(D) }
} }
fn caps() -> ptr { function caps() -> ptr {
let o = buf_new() let o = buf_new()
buf_puts(o, "{\"capabilities\":{") buf_puts(o, "{\"capabilities\":{")
buf_puts(o, "\"positionEncoding\":\"utf-16\"") buf_puts(o, "\"positionEncoding\":\"utf-16\"")
@ -2572,14 +2572,14 @@ program LudicLsp {
buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}") buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}")
return buf_str(o) return buf_str(o)
} }
fn on_initialize(msg: JVal, id: JVal) -> void { function on_initialize(msg: JVal, id: JVal) -> void {
let root = jpath(msg, "params.rootUri") let root = jpath(msg, "params.rootUri")
if (root != null) and root.t == JSTR { g_root = uri_to_path(root.s) } if (root != null) and root.t == JSTR { g_root = uri_to_path(root.s) }
if (g_root != null) { scan_root(g_root) } if (g_root != null) { scan_root(g_root) }
send_result(id, caps()) send_result(id, caps())
} }
fn handle(msg: JVal) -> void { function handle(msg: JVal) -> void {
let method = jstr(jget(msg, "method"), null) let method = jstr(jget(msg, "method"), null)
let id = jget(msg, "id") let id = jget(msg, "id")
if (method == null) { return } if (method == null) { return }

View file

@ -53,7 +53,7 @@ typedef struct {
* declaration keyword, anything stmt() dispatches on is a statement keyword. */ * declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = { static const char* LUDIC_KW_DECL[] = {
"program","import","property","model","enum","ui", "program","import","property","model","enum","ui",
"const","var","fn","extern","handler","entry","event","scene", 0 "const","var","function","extern","handler","entry","event","scene", 0
}; };
static const char* LUDIC_KW_CLAUSE[] = { static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","on","cancellable","public","layer","start", 0 "phase","query","on","cancellable","public","layer","start", 0

View file

@ -246,7 +246,7 @@ def main():
fh.write("program R {\n" fh.write("program R {\n"
" property Pos { x: int = 0, y: int = 0 }\n" " property Pos { x: int = 0, y: int = 0 }\n"
" property Vel { x: int = 0 }\n" " property Vel { x: int = 0 }\n"
" fn helper(x: int) -> int { return x + 1 }\n" " function helper(x: int) -> int { return x + 1 }\n"
" handler A phase Update {\n" " handler A phase Update {\n"
" for (p) in query [Pos] {\n" " for (p) in query [Pos] {\n"
" let x = 5\n" " let x = 5\n"
@ -275,7 +275,7 @@ def main():
lines_touched(field), [1, 7]) lines_touched(field), [1, 7])
local = rename_at(position_of(atext, "let x = 5", 4)) local = rename_at(position_of(atext, "let x = 5", 4))
check("renaming a local touches only that local", lines_touched(local), [6, 7]) check("renaming a local touches only that local", lines_touched(local), [6, 7])
param = rename_at(position_of(atext, "fn helper(x", 10)) param = rename_at(position_of(atext, "function helper(x", 16))
check("renaming a parameter touches only that function", lines_touched(param), [3]) check("renaming a parameter touches only that function", lines_touched(param), [3])
check("renaming a parameter makes exactly two edits", len(param), 2) check("renaming a parameter makes exactly two edits", len(param), 2)

View file

@ -5,7 +5,7 @@
# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2). # compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2).
# Returns true on success. Used for x itself and the editor tools. # Returns true on success. Used for x itself and the editor tools.
fn build_tool(name: ptr, src: ptr) -> bool { function build_tool(name: ptr, src: ptr) -> bool {
let ll = `build/{name}.ll` let ll = `build/{name}.ll`
if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
# write to a temp then move, so a running bin/x can rebuild itself in place # write to a temp then move, so a running bin/x can rebuild itself in place
@ -18,7 +18,7 @@ fn build_tool(name: ptr, src: ptr) -> bool {
# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed # Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed
# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just # with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just
# compiles. # compiles.
fn cmd_build_cli() -> int { function cmd_build_cli() -> int {
run("mkdir -p bin build") run("mkdir -p bin build")
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc, bin/ludic (from the IR seed, no C compiler)") print("cc: selfhost/ludicc.seed.ll -> bin/ludicc, bin/ludic (from the IR seed, no C compiler)")
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("x: seed did not assemble\n"); return 1 } if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("x: seed did not assemble\n"); return 1 }
@ -28,7 +28,7 @@ fn cmd_build_cli() -> int {
} }
# ---- build: the whole toolchain (ludicc, ludic, x, ludic-fmt, ludic-lsp) ----- # ---- build: the whole toolchain (ludicc, ludic, x, ludic-fmt, ludic-lsp) -----
fn cmd_build() -> int { function cmd_build() -> int {
if cmd_build_cli() != 0 { return 1 } if cmd_build_cli() != 0 { return 1 }
print("ludicc: tools/x/main.ludic -> bin/x (the task runner rebuilds itself)") print("ludicc: tools/x/main.ludic -> bin/x (the task runner rebuilds itself)")
if not build_tool("x", "tools/x/main.ludic") { return 1 } if not build_tool("x", "tools/x/main.ludic") { return 1 }
@ -44,7 +44,7 @@ fn cmd_build() -> int {
# ---- app: compile a Ludic program to a native app ---------------------------- # ---- app: compile a Ludic program to a native app ----------------------------
# usage: x app <file.ludic> [--headless|--windowed] [--save-temps] # usage: x app <file.ludic> [--headless|--windowed] [--save-temps]
# windowed is the default for a game; headless renders the last frame to out.ppm. # windowed is the default for a game; headless renders the last frame to out.ppm.
fn cmd_app() -> int { function cmd_app() -> int {
ensure_ludicc() ensure_ludicc()
var src = null var src = null
var mode = 1 # 1 = windowed, 2 = headless var mode = 1 # 1 = windowed, 2 = headless

View file

@ -18,7 +18,7 @@ program X {
import "selfhost_test.ludic" import "selfhost_test.ludic"
import "test.ludic" import "test.ludic"
fn usage() -> void { function usage() -> void {
print("x — the Ludic task runner (run from the repository root)") print("x — the Ludic task runner (run from the repository root)")
print("") print("")
print("build & run:") print("build & run:")
@ -43,7 +43,7 @@ program X {
} }
# a positional argument, or a default when absent # a positional argument, or a default when absent
fn argn(i: int, dflt: ptr) -> ptr { function argn(i: int, dflt: ptr) -> ptr {
if (i < arg_count()) { return arg(i) } if (i < arg_count()) { return arg(i) }
return dflt return dflt
} }

View file

@ -13,7 +13,7 @@
# ---- file IO ---------------------------------------------------------------- # ---- file IO ----------------------------------------------------------------
# read a whole file into a fresh NUL-terminated buffer (null if it cannot open) # read a whole file into a fresh NUL-terminated buffer (null if it cannot open)
fn read_file(path: ptr) -> ptr { function read_file(path: ptr) -> ptr {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if (f == null) { return null } if (f == null) { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)
@ -27,7 +27,7 @@ fn read_file(path: ptr) -> ptr {
} }
# overwrite `path` with `s`; returns false if it could not be opened # overwrite `path` with `s`; returns false if it could not be opened
fn write_file(path: ptr, s: ptr) -> bool { function write_file(path: ptr, s: ptr) -> bool {
let f = file_open(path, "wb") let f = file_open(path, "wb")
if (f == null) { return false } if (f == null) { return false }
file_write(f, s, len(s)) file_write(f, s, len(s))
@ -35,22 +35,22 @@ fn write_file(path: ptr, s: ptr) -> bool {
return true return true
} }
fn file_exists(path: ptr) -> bool { return shq(`test -e {path}`) } function file_exists(path: ptr) -> bool { return shq(`test -e {path}`) }
fn is_exec(path: ptr) -> bool { return shq(`test -x {path}`) } function is_exec(path: ptr) -> bool { return shq(`test -x {path}`) }
# is `a` newer than `b` (like the shell's `-nt`)? # is `a` newer than `b` (like the shell's `-nt`)?
fn newer(a: ptr, b: ptr) -> bool { return shq(`test {a} -nt {b}`) } function newer(a: ptr, b: ptr) -> bool { return shq(`test {a} -nt {b}`) }
# ---- process control -------------------------------------------------------- # ---- process control --------------------------------------------------------
# `run` returns the raw wait status; the program's exit code is the high byte. # `run` returns the raw wait status; the program's exit code is the high byte.
fn exit_code(st: int) -> int { return (st >> 8) & 255 } function exit_code(st: int) -> int { return (st >> 8) & 255 }
# run a command, returning its exit code (0 = success) # run a command, returning its exit code (0 = success)
fn sh(cmd: ptr) -> int { return exit_code(run(cmd)) } function sh(cmd: ptr) -> int { return exit_code(run(cmd)) }
# run a command, true when it succeeded # run a command, true when it succeeded
fn shq(cmd: ptr) -> bool { return exit_code(run(cmd)) == 0 } function shq(cmd: ptr) -> bool { return exit_code(run(cmd)) == 0 }
# run `cmd` and return its stdout (stderr discarded). Never null. # run `cmd` and return its stdout (stderr discarded). Never null.
fn capture(cmd: ptr) -> ptr { function capture(cmd: ptr) -> ptr {
let tmp = "/tmp/x_capture.out" let tmp = "/tmp/x_capture.out"
run(`{cmd} > {tmp} 2>/dev/null`) run(`{cmd} > {tmp} 2>/dev/null`)
let s = read_file(tmp) let s = read_file(tmp)
@ -60,16 +60,16 @@ fn capture(cmd: ptr) -> ptr {
# run `cmd`, join its output lines with single spaces and trim — the Ludic twin # run `cmd`, join its output lines with single spaces and trim — the Ludic twin
# of the shell idiom `$(cmd | tr '\n' ' ' | sed 's/ *$//')`. # of the shell idiom `$(cmd | tr '\n' ' ' | sed 's/ *$//')`.
fn capture_line(cmd: ptr) -> ptr { function capture_line(cmd: ptr) -> ptr {
return capture(`{cmd} | tr '\n' ' ' | sed 's/ *$//'`) return capture(`{cmd} | tr '\n' ' ' | sed 's/ *$//'`)
} }
# the last line of a command's output (for one-line error messages) # the last line of a command's output (for one-line error messages)
fn capture_tail(cmd: ptr) -> ptr { function capture_tail(cmd: ptr) -> ptr {
return capture(`{cmd} 2>&1 | tail -1`) return capture(`{cmd} 2>&1 | tail -1`)
} }
fn getenv_or(name: ptr, dflt: ptr) -> ptr { function getenv_or(name: ptr, dflt: ptr) -> ptr {
let v = getenv(name) let v = getenv(name)
if (v == null) { return dflt } if (v == null) { return dflt }
return v return v
@ -78,40 +78,40 @@ fn getenv_or(name: ptr, dflt: ptr) -> ptr {
# ---- stdout helpers --------------------------------------------------------- # ---- stdout helpers ---------------------------------------------------------
# write `s` with no trailing newline (print() always adds one) # write `s` with no trailing newline (print() always adds one)
fn out(s: ptr) -> void { file_write(file_stdout(), s, len(s)) } function out(s: ptr) -> void { file_write(file_stdout(), s, len(s)) }
fn err(s: ptr) -> void { file_write(file_stderr(), s, len(s)) } function err(s: ptr) -> void { file_write(file_stderr(), s, len(s)) }
# an ESC byte — the lexer has no \033, so build it by hand # an ESC byte — the lexer has no \033, so build it by hand
fn esc() -> ptr { let b = bytes(2); b[0] = 27; b[1] = 0; return b } function esc() -> ptr { let b = bytes(2); b[0] = 27; b[1] = 0; return b }
fn c_green() -> ptr { return esc() + "[32m" } function c_green() -> ptr { return esc() + "[32m" }
fn c_red() -> ptr { return esc() + "[31m" } function c_red() -> ptr { return esc() + "[31m" }
fn c_reset() -> ptr { return esc() + "[0m" } function c_reset() -> ptr { return esc() + "[0m" }
# ---- the PASS/FAIL test harness --------------------------------------------- # ---- the PASS/FAIL test harness ---------------------------------------------
var PASS: int = 0 var PASS: int = 0
var FAIL: int = 0 var FAIL: int = 0
fn ok(msg: ptr) -> void { function ok(msg: ptr) -> void {
PASS = PASS + 1 PASS = PASS + 1
print(` {c_green()}PASS{c_reset()} {msg}`) print(` {c_green()}PASS{c_reset()} {msg}`)
} }
fn bad(msg: ptr) -> void { function bad(msg: ptr) -> void {
FAIL = FAIL + 1 FAIL = FAIL + 1
print(` {c_red()}FAIL{c_reset()} {msg}`) print(` {c_red()}FAIL{c_reset()} {msg}`)
} }
fn bad2(msg: ptr, detail: ptr) -> void { function bad2(msg: ptr, detail: ptr) -> void {
bad(msg) bad(msg)
print(` {detail}`) print(` {detail}`)
} }
# assert two strings equal, reporting the mismatch # assert two strings equal, reporting the mismatch
fn check(label: ptr, got: ptr, want: ptr) -> void { function check(label: ptr, got: ptr, want: ptr) -> void {
if (got == want) { ok(label) } if (got == want) { ok(label) }
else { bad2(label, `expected [{want}] got [{got}]`) } else { bad2(label, `expected [{want}] got [{got}]`) }
} }
# print the "== N passed, M failed ==" footer and return the process exit code # print the "== N passed, M failed ==" footer and return the process exit code
fn report() -> int { function report() -> int {
print("") print("")
print(`== {str(PASS)} passed, {str(FAIL)} failed ==`) print(`== {str(PASS)} passed, {str(FAIL)} failed ==`)
if (FAIL == 0) { return 0 } if (FAIL == 0) { return 0 }

View file

@ -6,11 +6,11 @@
# now lives here once, in selfhost_frags(). # now lives here once, in selfhost_frags().
# the C toolchain driver (clang) that assembles and links the emitted IR # the C toolchain driver (clang) that assembles and links the emitted IR
fn cc() -> ptr { return getenv_or("LUDIC_CC", "clang") } function cc() -> ptr { return getenv_or("LUDIC_CC", "clang") }
# the self-host compiler's source fragments, in link order. This is THE list; # the self-host compiler's source fragments, in link order. This is THE list;
# the old scripts each carried their own copy. # the old scripts each carried their own copy.
fn selfhost_frags() -> []ptr { function selfhost_frags() -> []ptr {
let f = new []ptr let f = new []ptr
push(f, "selfhost/str.ludic") push(f, "selfhost/str.ludic")
push(f, "selfhost/buf.ludic") push(f, "selfhost/buf.ludic")
@ -52,7 +52,7 @@ fn selfhost_frags() -> []ptr {
# concatenate the fragments into `program SelfHost { ... }` at `outpath`. # concatenate the fragments into `program SelfHost { ... }` at `outpath`.
# This is the pure-Ludic equivalent of the scripts' `{ echo; for; cat; }` block. # This is the pure-Ludic equivalent of the scripts' `{ echo; for; cat; }` block.
fn write_selfhost_src(outpath: ptr) -> bool { function write_selfhost_src(outpath: ptr) -> bool {
let f = file_open(outpath, "wb") let f = file_open(outpath, "wb")
if (f == null) { return false } if (f == null) { return false }
let hdr = "program SelfHost {\n" let hdr = "program SelfHost {\n"
@ -72,13 +72,13 @@ fn write_selfhost_src(outpath: ptr) -> bool {
} }
# count the lines in a file (for the progress notes the scripts printed) # count the lines in a file (for the progress notes the scripts printed)
fn line_count(path: ptr) -> ptr { function line_count(path: ptr) -> ptr {
return capture_line(`wc -l < {path}`) return capture_line(`wc -l < {path}`)
} }
# ensure bin/ludicc exists and is current with the seed. Built from the checked-in # ensure bin/ludicc exists and is current with the seed. Built from the checked-in
# IR seed with clang alone — no C compiler is ever involved. # IR seed with clang alone — no C compiler is ever involved.
fn ensure_ludicc() -> void { function ensure_ludicc() -> void {
run("mkdir -p bin build") run("mkdir -p bin build")
if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") { if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") {
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
@ -90,7 +90,7 @@ fn ensure_ludicc() -> void {
# ---- selfhost-build: assemble + compile the self-host compiler --------------- # ---- selfhost-build: assemble + compile the self-host compiler ---------------
# usage: x selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost) # usage: x selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost)
fn cmd_selfhost_build(lc: ptr, outbin: ptr) -> int { function cmd_selfhost_build(lc: ptr, outbin: ptr) -> int {
run("mkdir -p build") run("mkdir -p build")
let src = "build/selfhost.ludic" let src = "build/selfhost.ludic"
if not write_selfhost_src(src) { err("x: cannot write build/selfhost.ludic\n"); return 1 } if not write_selfhost_src(src) { err("x: cannot write build/selfhost.ludic\n"); return 1 }
@ -104,7 +104,7 @@ fn cmd_selfhost_build(lc: ptr, outbin: ptr) -> int {
# ---- sh-compile: compile one .ludic with a given selfhost binary and link ---- # ---- sh-compile: compile one .ludic with a given selfhost binary and link ----
# usage: x sh-compile <selfhost-binary> <input.ludic> <output-binary> # usage: x sh-compile <selfhost-binary> <input.ludic> <output-binary>
fn cmd_sh_compile(shbin: ptr, in: ptr, outbin: ptr) -> int { function cmd_sh_compile(shbin: ptr, in: ptr, outbin: ptr) -> int {
if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 } if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 }
if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 } if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 }
return 0 return 0
@ -116,7 +116,7 @@ fn cmd_sh_compile(shbin: ptr, in: ptr, outbin: ptr) -> int {
# the quiet core, reused by the test suites; returns true on success. On failure # the quiet core, reused by the test suites; returns true on success. On failure
# the self-host/link diagnostics are left in /tmp/x_gb.err. # the self-host/link diagnostics are left in /tmp/x_gb.err.
fn game_build_ok(shbin: ptr, game: ptr, outbin: ptr) -> bool { function game_build_ok(shbin: ptr, game: ptr, outbin: ptr) -> bool {
let ll = `{outbin}.ll` let ll = `{outbin}.ll`
if not shq(`{shbin} {game} > {ll} 2>/tmp/x_gb.err`) { return false } if not shq(`{shbin} {game} > {ll} 2>/tmp/x_gb.err`) { return false }
if not shq(`{cc()} -O2 {ll} -o {outbin} 2>/tmp/x_gb.err`) { return false } if not shq(`{cc()} -O2 {ll} -o {outbin} 2>/tmp/x_gb.err`) { return false }
@ -125,7 +125,7 @@ fn game_build_ok(shbin: ptr, game: ptr, outbin: ptr) -> bool {
} }
# usage: x game-build <selfhost-binary> <game.ludic> <out-binary> # usage: x game-build <selfhost-binary> <game.ludic> <out-binary>
fn cmd_game_build(shbin: ptr, game: ptr, outbin: ptr) -> int { function cmd_game_build(shbin: ptr, game: ptr, outbin: ptr) -> int {
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 } if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
print("build failed:") print("build failed:")
out(capture("grep -i error /tmp/x_gb.err | head -5")) out(capture("grep -i error /tmp/x_gb.err | head -5"))
@ -136,7 +136,7 @@ fn cmd_game_build(shbin: ptr, game: ptr, outbin: ptr) -> int {
# stage0 bin/ludicc compiles the self-host source -> gen1 (built by a different # stage0 bin/ludicc compiles the self-host source -> gen1 (built by a different
# compiler, so its IR legitimately differs); gen1 -> gen2, gen2 -> gen3, and # compiler, so its IR legitimately differs); gen1 -> gen2, gen2 -> gen3, and
# gen2.ll == gen3.ll is the fixpoint that proves the compiler reproduces itself. # gen2.ll == gen3.ll is the fixpoint that proves the compiler reproduces itself.
fn cmd_bootstrap() -> int { function cmd_bootstrap() -> int {
ensure_ludicc() ensure_ludicc()
run("mkdir -p build/boot") run("mkdir -p build/boot")
if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 } if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 }
@ -163,7 +163,7 @@ fn cmd_bootstrap() -> int {
# The seed is the compiler's own IR, a proven fixed point: clang assembles it, # The seed is the compiler's own IR, a proven fixed point: clang assembles it,
# the seed-built compiler compiles its own source, and the result must equal the # the seed-built compiler compiles its own source, and the result must equal the
# seed byte for byte. # seed byte for byte.
fn cmd_bootstrap_cfree() -> int { function cmd_bootstrap_cfree() -> int {
run("mkdir -p build/cfree") run("mkdir -p build/cfree")
if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 } if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 }
print(" seed.ll --clang--> sh_seed (no C compiler used)") print(" seed.ll --clang--> sh_seed (no C compiler used)")
@ -184,7 +184,7 @@ fn cmd_bootstrap_cfree() -> int {
# fixed point of the NEW compiler, not a one-step image of the old. Falls back to # fixed point of the NEW compiler, not a one-step image of the old. Falls back to
# bin/ludicc only if the seed itself no longer assembles. Verify with # bin/ludicc only if the seed itself no longer assembles. Verify with
# `x bootstrap-cfree` afterwards. # `x bootstrap-cfree` afterwards.
fn cmd_reseed() -> int { function cmd_reseed() -> int {
run("mkdir -p build/cfree") run("mkdir -p build/cfree")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) { if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) {

View file

@ -3,7 +3,7 @@
# fixpoint). Replaces selfhost/test.sh. # fixpoint). Replaces selfhost/test.sh.
# compile selfhost/tests/<name>.ludic with bin/ludicc, assemble, run, compare # compile selfhost/tests/<name>.ludic with bin/ludicc, assemble, run, compare
fn sh_case(name: ptr, exp: ptr) -> void { function sh_case(name: ptr, exp: ptr) -> void {
let ll = `/tmp/x_sh_{name}.ll` let ll = `/tmp/x_sh_{name}.ll`
let er = `/tmp/x_sh_{name}.err` let er = `/tmp/x_sh_{name}.err`
let bn = `/tmp/x_sh_{name}` let bn = `/tmp/x_sh_{name}`
@ -16,14 +16,14 @@ fn sh_case(name: ptr, exp: ptr) -> void {
} }
# compile examples/<name>.ludic as a game and diff its render against the golden # compile examples/<name>.ludic as a game and diff its render against the golden
fn game_case(name: ptr, keys: ptr) -> void { function game_case(name: ptr, keys: ptr) -> void {
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_g_{name}`) { bad(`{name}: build`); return } if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_g_{name}`) { bad(`{name}: build`); return }
run(`printf '%s' '{keys}' | /tmp/x_g_{name} >/dev/null 2>&1`) run(`printf '%s' '{keys}' | /tmp/x_g_{name} >/dev/null 2>&1`)
if shq(`cmp -s selfhost/golden/{name}.ppm out.ppm`) { ok(`{name} matches the golden render`) } if shq(`cmp -s selfhost/golden/{name}.ppm out.ppm`) { ok(`{name} matches the golden render`) }
else { bad(`{name}: differs from golden`) } else { bad(`{name}: differs from golden`) }
} }
fn cmd_selfhost_test() -> int { function cmd_selfhost_test() -> int {
PASS = 0 PASS = 0
FAIL = 0 FAIL = 0

View file

@ -9,7 +9,7 @@
# compile a feature example as a game, run it (optional keys on stdin) and match # compile a feature example as a game, run it (optional keys on stdin) and match
# its stdout against the expected space-joined output. # its stdout against the expected space-joined output.
fn feat_case(name: ptr, keys: ptr, exp: ptr, label: ptr) -> void { function feat_case(name: ptr, keys: ptr, exp: ptr, label: ptr) -> void {
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_e_{name}`) { if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_e_{name}`) {
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
} }
@ -20,7 +20,7 @@ fn feat_case(name: ptr, keys: ptr, exp: ptr, label: ptr) -> void {
} }
# a "does it still compile" smoke test (parse -> lower -> link), no run # a "does it still compile" smoke test (parse -> lower -> link), no run
fn qsmoke(name: ptr) -> void { function qsmoke(name: ptr) -> void {
if game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_q_{name}`) { if game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_q_{name}`) {
ok(`{name} compiles (@Queries desugars to S_QUERY)`) ok(`{name} compiles (@Queries desugars to S_QUERY)`)
} else { bad2(name, capture_line("grep -i error /tmp/x_gb.err | head -1")) } } else { bad2(name, capture_line("grep -i error /tmp/x_gb.err | head -1")) }
@ -28,7 +28,7 @@ fn qsmoke(name: ptr) -> void {
# a pure-Ludic example that drives and asserts itself: compile headless to IR, # a pure-Ludic example that drives and asserts itself: compile headless to IR,
# let clang assemble it, run, and compare stdout. No C host and no C compiled. # let clang assemble it, run, and compare stdout. No C host and no C compiled.
fn net_case(name: ptr, exp: ptr) -> void { function net_case(name: ptr, exp: ptr) -> void {
let ll = `/tmp/x_n_{name}.ll` let ll = `/tmp/x_n_{name}.ll`
let log = `/tmp/x_n_{name}.out` let log = `/tmp/x_n_{name}.out`
if not shq(`bin/ludicc --headless examples/{name}.ludic --emit-llvm -o {ll} > {log} 2>&1`) { if not shq(`bin/ludicc --headless examples/{name}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
@ -43,7 +43,7 @@ fn net_case(name: ptr, exp: ptr) -> void {
if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) } if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) }
} }
fn cmd_test() -> int { function cmd_test() -> int {
PASS = 0 PASS = 0
FAIL = 0 FAIL = 0

View file

@ -7,7 +7,7 @@
# driven through python3/node (they are not project shell scripts), invoked here. # driven through python3/node (they are not project shell scripts), invoked here.
# refresh one editor-copy of a shared grammar file if it has drifted # refresh one editor-copy of a shared grammar file if it has drifted
fn sync_one(shared: ptr, dst: ptr) -> void { function sync_one(shared: ptr, dst: ptr) -> void {
if not shq(`cmp -s {shared} {dst}`) { if not shq(`cmp -s {shared} {dst}`) {
run(`cp {shared} {dst}`) run(`cp {shared} {dst}`)
print(`sync: {shared} -> {dst}`) print(`sync: {shared} -> {dst}`)
@ -16,7 +16,7 @@ fn sync_one(shared: ptr, dst: ptr) -> void {
# VS Code needs the grammar inside its own extension directory, so it gets a # VS Code needs the grammar inside its own extension directory, so it gets a
# copy that build refreshes rather than trusts (test-tools asserts they match). # copy that build refreshes rather than trusts (test-tools asserts they match).
fn sync_vscode_grammar() -> void { function sync_vscode_grammar() -> void {
if not shq("test -d tools/editors/vscode") { return } if not shq("test -d tools/editors/vscode") { return }
run("mkdir -p tools/editors/vscode/syntaxes") run("mkdir -p tools/editors/vscode/syntaxes")
sync_one("tools/editors/shared/ludic.tmLanguage.json", "tools/editors/vscode/syntaxes/ludic.tmLanguage.json") sync_one("tools/editors/shared/ludic.tmLanguage.json", "tools/editors/vscode/syntaxes/ludic.tmLanguage.json")
@ -25,7 +25,7 @@ fn sync_vscode_grammar() -> void {
} }
# is `flag` present anywhere in argv? # is `flag` present anywhere in argv?
fn has_flag(flag: ptr) -> bool { function has_flag(flag: ptr) -> bool {
var i = 2 var i = 2
while i < arg_count() { if (arg(i) == flag) { return true }; i = i + 1 } while i < arg_count() { if (arg(i) == flag) { return true }; i = i + 1 }
return false return false
@ -33,7 +33,7 @@ fn has_flag(flag: ptr) -> bool {
# ---- tools: build the editor toolchain -------------------------------------- # ---- tools: build the editor toolchain --------------------------------------
# usage: x tools [--install] [--test] # usage: x tools [--install] [--test]
fn cmd_tools() -> int { function cmd_tools() -> int {
ensure_ludicc() ensure_ludicc()
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)") print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)")
if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 } if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 }
@ -53,7 +53,7 @@ fn cmd_tools() -> int {
} }
# ---- test-tools: the editor-toolchain regression suite ----------------------- # ---- test-tools: the editor-toolchain regression suite -----------------------
fn cmd_test_tools() -> int { function cmd_test_tools() -> int {
PASS = 0 PASS = 0
FAIL = 0 FAIL = 0
let fmt = "bin/ludic-fmt" let fmt = "bin/ludic-fmt"
@ -172,13 +172,13 @@ fn cmd_test_tools() -> int {
return report() return report()
} }
fn test_json(path: ptr) -> void { function test_json(path: ptr) -> void {
let bn = capture_line(`basename {path}`) let bn = capture_line(`basename {path}`)
if shq(`python3 -c 'import json,sys;json.load(open(sys.argv[1]))' {path} 2>/dev/null`) { if shq(`python3 -c 'import json,sys;json.load(open(sys.argv[1]))' {path} 2>/dev/null`) {
ok(`valid JSON: {bn}`) ok(`valid JSON: {bn}`)
} else { bad(`invalid JSON: {path}`) } } else { bad(`invalid JSON: {path}`) }
} }
fn test_xml(path: ptr) -> void { function test_xml(path: ptr) -> void {
let bn = capture_line(`basename {path}`) let bn = capture_line(`basename {path}`)
if shq(`python3 -c 'import xml.dom.minidom,sys;xml.dom.minidom.parse(sys.argv[1])' {path} 2>/dev/null`) { if shq(`python3 -c 'import xml.dom.minidom,sys;xml.dom.minidom.parse(sys.argv[1])' {path} 2>/dev/null`) {
ok(`valid XML: {bn}`) ok(`valid XML: {bn}`)