diff --git a/LANGUAGE.md b/LANGUAGE.md index e4bb2ef6..56342ee6 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -85,11 +85,11 @@ module bank import "ledger.ludic" # bank/ledger.ludic -var balance: int = 0 # private: only module bank sees it +export state Bank { balance: int = 0 } # its fields are bank's to change: only bank's functions do export event Deposited { amount: int } -function add(n: int) -> void { balance += n } -export function deposit(n: int) -> void { - add(n) +function add(b: mut Bank, n: int) -> void { b.balance += n } # private: only module bank sees it +export function deposit(b: mut Bank, n: int) -> void { + add(b, n) emit Deposited(amount: n) } ``` @@ -240,19 +240,25 @@ function hips_of(h: Hiker) -> int { return h.hips } - **Read-only is checked where it is written.** Through a read-only state the compiler refuses an assignment whose target starts at it (`h.hips = 1`, `h.list[i] = x`), a `push` onto something in it, and passing it where a `mut` one is wanted (`bump changes Tally (c: mut Tally), and c is - read-only here`). It does not follow a reference read out of it into a local and changed there. + read-only here`). A reference read out of it into a local (`let l = h.list`, `let r = h.rows[0]`) + is read-only too, so a write through that is refused the same way; a value read out (`let n = + h.count`) is a copy and the local's own. `machine h.mode` writes its store on every `become`. `mut` is for a state parameter only. - **The runtime supplies it at the entry points** - the only code nothing in the program calls: - a body that declares it: `entry (h: mut Hiker) { ... }`, `handler Draw(h: Hiker) phase Render - { ... }`, `@On(Ping) handler Heard(h: mut Hiker) { ... }`, `test "name" (h: mut Hiker) { ... }`; + { ... }`, `@On(Ping) handler Heard(h: mut Hiker) { ... }`, `@OnSpawn(M) handler Made(h: mut + Hiker) { ... }`, a scene's `on enter (h: mut Hiker) { ... }`, `test "name" (h: mut Hiker) { ... }`; + - a retained `ui` block, which names a state's instance by the state's name: `font: Menu.title_font`; - a function value: `fn tick` of `function tick(h: mut Hiker, t: Tick)` is `tick` with its leading states supplied, a `fn(Tick) -> void` - so a system's functions, a port's bind and any callback a package calls are entry points without saying so; - a port member bound to a state's field, `bind Purse { money: Wallet.cash }`; - - a call the compiler writes: a namespace method's target, a runtime built-in. + - a call the compiler writes: a namespace method's target (`Weapon.def(...)` of `function + weapon_def(w: mut Weapons, ...)`), an engine system, a runtime built-in. Every other call passes its states explicitly. - **Everything else module-level is immutable all the way down.** `let LIMITS: []int = [1, 2]`, - a `const`, a registry: an assignment or a `push` that starts at one is refused. + a `const`, a registry: an assignment or a `push` that starts at one is refused, and so is one + through a local that holds part of it (`let r = LIMITS; push(r, 3)`). - **Tests get fresh states.** Each test block starts from states made new, in its own process under `ludic test` and in the runner run directly. - The toolchain's own programs (the compiler, the CLI) are not part of this yet: they build with @@ -267,19 +273,34 @@ counter.ludic:5: error: this assignment: LIMITS is module-level and immutable al counter.ludic:3: error: x: mut int - mut is for a state parameter, and int is not a state ``` -**`ludic migrate state [file]` moves a program there.** It compiles the program and, from the -compiler's own view of every name: +**`ludic migrate state [file|dir...] [--runtime] [--dry-run]` moves programs there.** It compiles +each program and, from the compiler's own view of every name: -1. each module's vars become one state, `state State { ... }`, where the first of them was; -2. every reference to one is rewritten to `_st.`; -3. each function's states - those it touches, and those of everything it calls, to a fixed point - - become its leading parameters, `mut` where it or something it calls writes; -4. each call passes them on, and each entry point declares them. +1. a var nothing writes, holding a value (an int, a string, an enum...), becomes a module-level + `let` where it stands; +2. each module's other vars become one state, `state State { ... }`, where the first of + them was - a module's by its name (`module fishing`: `FishingState`), a directory with no module + line by its path (`ludic.render3d`: `Render3dState`), the program's own file by the program's + name (`program SceneDemo`: `SceneDemoState`); the fields keep their comments; +3. every reference to one is rewritten to `_st.` (`scene_demo_st.counter`), and in a + `ui` block to `.`; +4. each function's states - those it touches, and those of everything it calls, to a fixed point - + become its leading parameters, `mut` where it or something it calls writes; a state a run before + declared read-only becomes `mut` where it is now changed; +5. each call passes them on, and each entry point declares them. -It prints what it cannot decide (a var read in another global's initializer, a reference in -generated code), for a person to finish. Run it once per program that uses what changes - each of a -package's test programs, a game's entry and its lab: a later run finds the states an earlier one -made and passes them on. `--runtime` moves the runtime's own vars too. +Give it every program at once - a directory stands for the test programs under it - and it merges +their plans before it edits anything: programs that share a package agree about it, a module two of +them see different files of is one state, and a path reached as `../../packages/x` is the same +file as `packages/x`. It prints what it cannot decide (a var read in another global's initializer, a +reference in generated code), for a person to finish. A later run finds the states an earlier one +made and adds to them. `--runtime` moves the runtime's own vars too. gpp's packages and examples +were moved with one command: + +``` +ludic migrate state packages packages/ludic.lab/example/plate.ludic +migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files +``` ### Types are checked before anything is emitted @@ -943,13 +964,13 @@ panels with padding / gap / alignment / grow), drawing (9-slice skins, images, TrueType text, focus highlight) and keyboard focus + activation. ```ludic -var title_font: int = 0 +state Menu { title_font: int = 0 } ui MainMenu { panel id: Root w: 288 pad: 16 gap: 6 skin: "assets/ui/panel.png" inset: 10 align: center { - label text: "CHRONO RIFT" font: title_font size: 26 fg: Color.Gold align: center - button id: NewGame text: "New Game" font: title_font size: 16 w: 236 - button id: Quit text: "Quit" font: title_font size: 16 w: 236 + label text: "CHRONO RIFT" font: Menu.title_font size: 26 fg: Color.Gold align: center + button id: NewGame text: "New Game" font: Menu.title_font size: 16 w: 236 + button id: Quit text: "Quit" font: Menu.title_font size: 16 w: 236 } } ``` @@ -958,13 +979,14 @@ A widget inherits `font`, `size`, `fg` and `align` from the nearest ancestor tha sets them, so a panel states a menu's look once and a label only says what differs. Widget types: `panel` (container + optional skin/bg/border), `col` / `row` (pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are -evaluated at build time, so `font: title_font` reads a value the program set first. +evaluated at build time, so `font: Menu.title_font` reads a value the program set first - a `ui` +block is an entry point, and names a state's one instance by the state's name. Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from handlers: ```ludic -handler Boot phase Start { - title_font = Font.load("…Arial.ttf") +handler Boot(menu: mut Menu) phase Start { + menu.title_font = Font.load("…Arial.ttf") Ui.build() # construct the tree (loads skins/images) Ui.open(UI_MainMenu) # make it active, focus the first button } @@ -1098,7 +1120,7 @@ any record's: ```ludic # doc-check: skip — composite: declarations plus statements using them property Hero { iframes: int = 0, roll_cooldown: int = 0 } -var player: int = -1 +let player = World.query_next(World.prop_id("Hero"), 0) Hero.of(player).iframes = 20 # assign a field Hero.of(player).roll_cooldown -= 1 # compound-assign one @@ -1584,11 +1606,11 @@ snippet below does not compile today. Programs use `[]T` slices for now. ```ludic # doc-check: skip — [T; N] fixed arrays are not yet implemented (design target) -var table: [int; 8] # module-level storage -handler S phase Update { +state Grid { table: [int; 8] } # a state's storage +handler S(g: mut Grid) phase Update { let buf: [int; 4] # a local; no initializer needed buf[0] = 10 - table[2] = buf[0] + g.table[2] = buf[0] } ``` @@ -1678,19 +1700,24 @@ at the top level it is module state). loop accumulators and anything that genuinely changes. - **`const NAME = e`** — a compile-time constant (folded, no storage). -A program-scope `var` may be initialized with **any expression** — a literal, an -`Enum.Variant`, a `new Record`, a call. What the compiler can fold becomes the -global's initial value; the rest runs once at startup, in declaration order, -after the runtime boots and before the `Start` phase: +`var` is for locals. At module level there is no `var` (it is refused): what changes belongs to a +`state` (see "State" below), and what does not is a module-level `let`, immutable all the way +down. A state's field, or a module-level `let`, may be initialized with **any expression** — a +literal, an `Enum.Variant`, a `new Record`, a call. What the compiler can fold becomes the initial +value; the rest runs once at startup, in declaration order, after the runtime boots and before the +`Start` phase: ```ludic -# doc-check: skip — illustrative globals -var run: Progress = new Progress # allocated before Start -var origin: IVec2 = IVec2.zero() -var mode: HeroState = HeroState.Idle # folded +# doc-check: skip — illustrative +state Hero { + run: Progress = new Progress # allocated before Start + origin: IVec2 = IVec2.zero() + mode: HeroState = HeroState.Idle # folded +} +let ORIGIN_NAME: string = "camp" # a module-level let: a value nothing changes ``` -Declaring the same `var` twice is an error — including a name the spliced engine +Declaring the same name twice is an error — including a name the spliced engine runtime already uses, which the message says (`variable ui_font is also a variable of the engine runtime; choose another name`). @@ -1767,19 +1794,20 @@ no `= value` on any state: ```ludic # doc-check: skip — composite: declarations plus a machine over them enum HeroState { Idle, Rolling, Swinging } -var hero_state: HeroState = HeroState.Idle +state Hero { mode: HeroState = HeroState.Idle } -machine hero_state { +machine hero.mode { # (hero: mut Hero) - become writes it state Idle { if wants_roll { become Rolling } } # HeroState.Idle state Rolling { if done { become Idle } } # HeroState.Rolling state Swinging { … } } -if hero_state == HeroState.Rolling { … } # readable from anywhere +if hero.mode == HeroState.Rolling { … } # readable wherever Hero is ``` -A state that names no variant of the store's enum is a compile error. A bare -(payload-free) enum is an `int`-sized type wherever a type is written — a `var`, -a parameter, a field, a return. +A machine's store is a state's field (`machine hero.mode`), a local, or a register index; a +`become` writes it, so the function needs its state `mut`. A state that names no variant of the +store's enum is a compile error. A bare (payload-free) enum is an `int`-sized type wherever a type +is written — a `var`, a parameter, a field, a return. ## Enums diff --git a/docs/language/annotations/annot-export.md b/docs/language/annotations/annot-export.md index 3afbe7b7..ed2e3a6d 100644 --- a/docs/language/annotations/annot-export.md +++ b/docs/language/annotations/annot-export.md @@ -13,16 +13,18 @@ order: 3 ```ludic program ScoreModule { - var running_total: int = 0 - - @export function add_points(amount: int) -> int { # callable from a native host - running_total = running_total + amount - return running_total + state ScoreModuleState { + running_total: int = 0 } - entry { - print(add_points(10)) # 10 - print(add_points(5)) # 15 + @export function add_points(score_module_st: mut ScoreModuleState, amount: int) -> int { # callable from a native host + score_module_st.running_total = score_module_st.running_total + amount + return score_module_st.running_total + } + + entry (score_module_st: mut ScoreModuleState) { + print(add_points(score_module_st, 10)) # 10 + print(add_points(score_module_st, 5)) # 15 } } ``` diff --git a/docs/language/annotations/annot-onstart.md b/docs/language/annotations/annot-onstart.md index 2efa81b7..21865f17 100644 --- a/docs/language/annotations/annot-onstart.md +++ b/docs/language/annotations/annot-onstart.md @@ -16,13 +16,15 @@ program BootHook { property Health { current: int = 0, maximum: int = 100 } model Player { Health } - var elapsed_frames: int = 0 - - @OnStart handler CreateWorld { # runs once at boot - spawn Player { Health { current: 100, maximum: 100 } } - elapsed_frames = 0 + state BootHookState { + elapsed_frames: int = 0 } - handler CountFrames phase Update { elapsed_frames = elapsed_frames + 1 } + @OnStart handler CreateWorld(boot_hook_st: mut BootHookState) { # runs once at boot + spawn Player { Health { current: 100, maximum: 100 } } + boot_hook_st.elapsed_frames = 0 + } + + handler CountFrames(boot_hook_st: mut BootHookState) phase Update { boot_hook_st.elapsed_frames = boot_hook_st.elapsed_frames + 1 } } ``` diff --git a/docs/language/app/app-window_hide.md b/docs/language/app/app-window_hide.md index 38f27d71..b6b073c9 100644 --- a/docs/language/app/app-window_hide.md +++ b/docs/language/app/app-window_hide.md @@ -17,15 +17,15 @@ Keep the frame loop going while hidden so the child can be polled, but there is ```ludic skip program Launcher { - var game: int = -1 - function play() -> void { - game = Process.spawn(game_path(), new []string) - if game >= 0 { App.window_hide() } + state Child { game: int = -1 } + function play(c: mut Child) -> void { + c.game = Process.spawn(game_path(), new []string) + if c.game >= 0 { App.window_hide() } } - handler Watch phase Update { - if game >= 0 and Process.poll(game) != -1 { - Process.free(game) - game = -1 + handler Watch(c: mut Child) phase Update { + if c.game >= 0 and Process.poll(c.game) != -1 { + Process.free(c.game) + c.game = -1 App.window_show() } } diff --git a/docs/language/builtins/fn-max.md b/docs/language/builtins/fn-max.md index 6e64dce1..e48e6cfd 100644 --- a/docs/language/builtins/fn-max.md +++ b/docs/language/builtins/fn-max.md @@ -17,18 +17,20 @@ Parameters: ```ludic program HighScore { - var score: int = 0 - var best_score: int = 0 - - handler EarnPoints phase Update { - score = score + 5 - best_score = max(best_score, score) - if score >= 20 { quit() } + state HighScoreState { + score: int = 0 + best_score: int = 0 } - handler ReportBest phase Render { + handler EarnPoints(high_score_st: mut HighScoreState) phase Update { + high_score_st.score = high_score_st.score + 5 + high_score_st.best_score = max(high_score_st.best_score, high_score_st.score) + if high_score_st.score >= 20 { quit() } + } + + handler ReportBest(high_score_st: HighScoreState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 8, y: 8, value: best_score, color: Color.Gold, scale: 1) + Screen.draw_number(x: 8, y: 8, value: high_score_st.best_score, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/builtins/fn-print.md b/docs/language/builtins/fn-print.md index 785a2e39..a5be89c5 100644 --- a/docs/language/builtins/fn-print.md +++ b/docs/language/builtins/fn-print.md @@ -16,12 +16,14 @@ Parameters: ```ludic program PrintScore { - var score: int = 0 + state PrintScoreState { + score: int = 0 + } - handler CountUp phase Update { - score = score + 10 - print(score) - if score >= 30 { quit() } + handler CountUp(print_score_st: mut PrintScoreState) phase Update { + print_score_st.score = print_score_st.score + 10 + print(print_score_st.score) + if print_score_st.score >= 30 { quit() } } } ``` diff --git a/docs/language/builtins/fn-string.md b/docs/language/builtins/fn-string.md index 274c9298..c80d9c3c 100644 --- a/docs/language/builtins/fn-string.md +++ b/docs/language/builtins/fn-string.md @@ -16,7 +16,7 @@ Parameters: ```ludic program LabelValue { - var score: int = 250 + let score: int = 250 handler ReportScore phase Start { let label = string(score) diff --git a/docs/language/builtins/fn-ui_build.md b/docs/language/builtins/fn-ui_build.md index 8f9dc526..7613417a 100644 --- a/docs/language/builtins/fn-ui_build.md +++ b/docs/language/builtins/fn-ui_build.md @@ -13,18 +13,20 @@ Constructs the retained-UI tree you declared in a ui block, laying ```ludic program TitleMenu { - var title_font: int = 0 + state TitleMenuState { + title_font: int = 0 + } ui MainMenu { panel id: Root w: 240 pad: 16 gap: 6 bg: 0x1a1a2c align: center { - label text: "CHRONORIFT" font: title_font size: 24 fg: 0xffe060 align: center - button id: NewGame text: "New Game" font: title_font size: 16 w: 200 - button id: Quit text: "Quit" font: title_font size: 16 w: 200 + label text: "CHRONORIFT" font: TitleMenuState.title_font size: 24 fg: 0xffe060 align: center + button id: NewGame text: "New Game" font: TitleMenuState.title_font size: 16 w: 200 + button id: Quit text: "Quit" font: TitleMenuState.title_font size: 16 w: 200 } } - handler Boot phase Start { - title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf") + handler Boot(title_menu_st: mut TitleMenuState) phase Start { + title_menu_st.title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf") ui_build() Ui.open(UI_MainMenu) } diff --git a/docs/language/control/kw-become.md b/docs/language/control/kw-become.md index 3e1fa2c7..24f07263 100644 --- a/docs/language/control/kw-become.md +++ b/docs/language/control/kw-become.md @@ -14,11 +14,13 @@ A become statement performs a transition. Inside a machine 20 { - found = i + find_first_st.found = i break # stop at the first i whose square exceeds 20 } } diff --git a/docs/language/control/kw-continue.md b/docs/language/control/kw-continue.md index bad4c210..382ceee5 100644 --- a/docs/language/control/kw-continue.md +++ b/docs/language/control/kw-continue.md @@ -13,12 +13,14 @@ order: 10 ```ludic program SumOdds { - var total: int = 0 + state SumOddsState { + total: int = 0 + } - handler Add phase Update { + handler Add(sum_odds_st: mut SumOddsState) phase Update { for i in 0 .. 10 { if i % 2 == 0 { continue } # skip even numbers - total = total + i + sum_odds_st.total = sum_odds_st.total + i } } } diff --git a/docs/language/control/kw-machine.md b/docs/language/control/kw-machine.md index aade19d6..c0730add 100644 --- a/docs/language/control/kw-machine.md +++ b/docs/language/control/kw-machine.md @@ -14,10 +14,12 @@ A machine turns an integer store into an explicit state machine, re ```ludic program TurnOrder { enum Phase { KnightMenu, KnightResolve, EnemyTurn } - var battle_phase: int = Phase.KnightMenu + state TurnOrderState { + battle_phase: int = Phase.KnightMenu + } - handler RunTurn phase Update { - machine battle_phase { + handler RunTurn(turn_order_st: mut TurnOrderState) phase Update { + machine turn_order_st.battle_phase { state KnightMenu { if Input.key() == ' ' { become KnightResolve } } @@ -30,9 +32,9 @@ program TurnOrder { } } - handler DrawWorld phase Render { + handler DrawWorld(turn_order_st: TurnOrderState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 8, y: 8, value: battle_phase, color: Color.Gold, scale: 2) + Screen.draw_number(x: 8, y: 8, value: turn_order_st.battle_phase, color: Color.Gold, scale: 2) Screen.show() } } diff --git a/docs/language/control/kw-state.md b/docs/language/control/kw-state.md index 0ed8a595..3c9244df 100644 --- a/docs/language/control/kw-state.md +++ b/docs/language/control/kw-state.md @@ -14,17 +14,19 @@ A state declares one state of an enclosing machine: a ```ludic program DoorControl { enum DoorState { Closed, Opening, Open } - var door: int = DoorState.Closed - var elapsed_frames: int = 0 + state DoorControlState { + door: int = DoorState.Closed + elapsed_frames: int = 0 + } - handler RunDoor phase Update { - machine door { + handler RunDoor(door_control_st: mut DoorControlState) phase Update { + machine door_control_st.door { state Closed { - if Input.key() == ' ' { elapsed_frames = 0; become Opening } + if Input.key() == ' ' { door_control_st.elapsed_frames = 0; become Opening } } state Opening { - elapsed_frames = elapsed_frames + 1 - if elapsed_frames >= 30 { become Open } + door_control_st.elapsed_frames = door_control_st.elapsed_frames + 1 + if door_control_st.elapsed_frames >= 30 { become Open } } state Open { Screen.status("door open") diff --git a/docs/language/control/kw-while.md b/docs/language/control/kw-while.md index c38feb2d..f4692fd0 100644 --- a/docs/language/control/kw-while.md +++ b/docs/language/control/kw-while.md @@ -13,20 +13,22 @@ A while loop re-evaluates its condition before every pass and runs ```ludic program Countdown { - var fuse: int = 5 - var elapsed_frames: int = 0 + state CountdownState { + fuse: int = 5 + elapsed_frames: int = 0 + } - handler Tick phase Update { - elapsed_frames = elapsed_frames + 1 - while fuse > 0 and elapsed_frames % 60 == 0 { - fuse = fuse - 1 - elapsed_frames = elapsed_frames + 1 + handler Tick(countdown_st: mut CountdownState) phase Update { + countdown_st.elapsed_frames = countdown_st.elapsed_frames + 1 + while countdown_st.fuse > 0 and countdown_st.elapsed_frames % 60 == 0 { + countdown_st.fuse = countdown_st.fuse - 1 + countdown_st.elapsed_frames = countdown_st.elapsed_frames + 1 } } - handler DrawWorld phase Render { + handler DrawWorld(countdown_st: CountdownState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 8, y: 8, value: fuse, color: Color.Crimson, scale: 3) + Screen.draw_number(x: 8, y: 8, value: countdown_st.fuse, color: Color.Crimson, scale: 3) Screen.show() } } diff --git a/docs/language/ecs/fn-world_load.md b/docs/language/ecs/fn-world_load.md index 17c5260c..123df506 100644 --- a/docs/language/ecs/fn-world_load.md +++ b/docs/language/ecs/fn-world_load.md @@ -20,16 +20,18 @@ program SnapshotRoundTrip { property Position { column: int = 0, row: int = 0 } model Enemy { Position } - var snapshot_length: int = 0 + state SnapshotRoundTripState { + snapshot_length: int = 0 + } handler Boot phase Start { spawn Grunt { Position { column: 3, row: 3 } } } - handler RoundTrip phase Update { + handler RoundTrip(snapshot_round_trip_st: mut SnapshotRoundTripState) phase Update { let buffer = bytes(4096) - if Input.key() == 's' { snapshot_length = world_save(buffer) } - if Input.key() == 'l' { world_load(buffer, snapshot_length) } + if Input.key() == 's' { snapshot_round_trip_st.snapshot_length = world_save(buffer) } + if Input.key() == 'l' { world_load(buffer, snapshot_round_trip_st.snapshot_length) } } } ``` diff --git a/docs/language/events/kw-cancel.md b/docs/language/events/kw-cancel.md index 18591325..1ded343b 100644 --- a/docs/language/events/kw-cancel.md +++ b/docs/language/events/kw-cancel.md @@ -15,23 +15,25 @@ order: 2 program CancelExample { event cancellable BeforeOpenDoor { key_count: int = 0 } - var keys: int = 0 - var doors_opened: int = 0 + state CancelExampleState { + keys: int = 0 + doors_opened: int = 0 + } @On(BeforeOpenDoor) handler RequireKey { if key_count <= 0 { cancel } } - handler TryOpen phase Update { + handler TryOpen(cancel_example_st: mut CancelExampleState) phase Update { if Input.key() == 'o' { - if emit BeforeOpenDoor(key_count: keys) == 0 { - doors_opened = doors_opened + 1 - keys = keys - 1 + if emit BeforeOpenDoor(key_count: cancel_example_st.keys) == 0 { + cancel_example_st.doors_opened = cancel_example_st.doors_opened + 1 + cancel_example_st.keys = cancel_example_st.keys - 1 } } } - handler DrawWorld phase Render { + handler DrawWorld(cancel_example_st: CancelExampleState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 4, y: 4, value: doors_opened, color: Color.Gold, scale: 1) + Screen.draw_number(x: 4, y: 4, value: cancel_example_st.doors_opened, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/events/kw-cancellable.md b/docs/language/events/kw-cancellable.md index f40beb5e..2bc40850 100644 --- a/docs/language/events/kw-cancellable.md +++ b/docs/language/events/kw-cancellable.md @@ -15,19 +15,21 @@ order: 50 program CancellableExample { event cancellable BeforeHurt { amount: int = 0 } - var current_health: int = 100 + state CancellableExampleState { + current_health: int = 100 + } @On(BeforeHurt) handler AbsorbSmallHits { if amount <= 5 { cancel } } - handler TakeDamage phase Update { + handler TakeDamage(cancellable_example_st: mut CancellableExampleState) phase Update { if Input.key() == 'h' { - if emit BeforeHurt(amount: 3) == 0 { current_health = current_health - 3 } + if emit BeforeHurt(amount: 3) == 0 { cancellable_example_st.current_health = cancellable_example_st.current_health - 3 } } } - handler DrawWorld phase Render { + handler DrawWorld(cancellable_example_st: CancellableExampleState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 4, y: 4, value: current_health, color: Color.Crimson, scale: 1) + Screen.draw_number(x: 4, y: 4, value: cancellable_example_st.current_health, color: Color.Crimson, scale: 1) Screen.show() } } diff --git a/docs/language/events/kw-emit.md b/docs/language/events/kw-emit.md index b70ac33e..3c825127 100644 --- a/docs/language/events/kw-emit.md +++ b/docs/language/events/kw-emit.md @@ -15,19 +15,21 @@ order: 1 program EmitExample { event cancellable BeforeSpend { amount: int = 0 } - var coins: int = 50 + state EmitExampleState { + coins: int = 50 + } - @On(BeforeSpend) handler RejectOverdraft { if amount > coins { cancel } } + @On(BeforeSpend) handler RejectOverdraft(emit_example_st: EmitExampleState) { if amount > emit_example_st.coins { cancel } } - handler TrySpend phase Update { + handler TrySpend(emit_example_st: mut EmitExampleState) phase Update { if Input.key() == 'b' { - if emit BeforeSpend(amount: 20) == 0 { coins = coins - 20 } + if emit BeforeSpend(amount: 20) == 0 { emit_example_st.coins = emit_example_st.coins - 20 } } } - handler DrawWorld phase Render { + handler DrawWorld(emit_example_st: EmitExampleState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 4, y: 4, value: coins, color: Color.Gold, scale: 1) + Screen.draw_number(x: 4, y: 4, value: emit_example_st.coins, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/events/kw-event.md b/docs/language/events/kw-event.md index 5673f334..8155c3c7 100644 --- a/docs/language/events/kw-event.md +++ b/docs/language/events/kw-event.md @@ -15,18 +15,20 @@ order: 0 program EventExample { event EnemyDefeated { points: int = 0 } - var score: int = 0 + state EventExampleState { + score: int = 0 + } - @On(EnemyDefeated) handler AddScore { score = score + points } + @On(EnemyDefeated) handler AddScore(event_example_st: mut EventExampleState) { event_example_st.score = event_example_st.score + points } @On(EnemyDefeated) handler PlayChime { Screen.status("enemy down") } handler ScoreOnKey phase Update { if Input.key() == 'k' { emit EnemyDefeated(points: 10) } } - handler DrawWorld phase Render { + handler DrawWorld(event_example_st: EventExampleState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 4, y: 4, value: score, color: Color.Gold, scale: 1) + Screen.draw_number(x: 4, y: 4, value: event_example_st.score, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/font/font-load.md b/docs/language/font/font-load.md index f0332234..b0c2dd4f 100644 --- a/docs/language/font/font-load.md +++ b/docs/language/font/font-load.md @@ -15,9 +15,9 @@ Loads a TrueType font file and returns its handle. Load it before Ui.build ```ludic # doc-check: skip — illustrative -var menu_font: int = 0 -handler Boot phase Start { - menu_font = Font.load(path: "assets/fonts/PixelOperator.ttf") +state Menu { font: int = 0 } +handler Boot(menu: mut Menu) phase Start { + menu.font = Font.load(path: "assets/fonts/PixelOperator.ttf") Ui.build() } ``` diff --git a/docs/language/gl/_section.md b/docs/language/gl/_section.md index d4346298..01e7f403 100644 --- a/docs/language/gl/_section.md +++ b/docs/language/gl/_section.md @@ -16,20 +16,22 @@ Using Gl.* links gl.ll, the thunks and OpenGL.fr program Triangle { property Marker { on: int = 1 } model Anchor { Marker } - var prog: int = 0 - var vao: int = 0 - handler Boot phase Start { + state TriangleState { + prog: int = 0 + vao: int = 0 + } + handler Boot(triangle_st: mut TriangleState) phase Start { spawn Anchor {} if not Gl.open(width: 640, height: 360, title: "GL") { quit() } - prog = Gl.program(vs: "#version 410 core\nvoid main(){ gl_Position = vec4(float(gl_VertexID == 1) * 2.0 - 0.5, float(gl_VertexID == 2) * 2.0 - 0.5, 0.0, 1.0); }\n", + triangle_st.prog = Gl.program(vs: "#version 410 core\nvoid main(){ gl_Position = vec4(float(gl_VertexID == 1) * 2.0 - 0.5, float(gl_VertexID == 2) * 2.0 - 0.5, 0.0, 1.0); }\n", fs: "#version 410 core\nout vec4 o; void main(){ o = vec4(1.0, 0.5, 0.2, 1.0); }\n") - vao = Gl.vao() + triangle_st.vao = Gl.vao() } - handler Draw phase Render { + handler Draw(triangle_st: TriangleState) phase Render { Gl.clear_color(red: 0.1, green: 0.1, blue: 0.15, alpha: 1.0) Gl.clear(mask: GL_COLOR_BUFFER_BIT) - Gl.use_program(prog: prog) - Gl.bind_vertex_array(array: vao) + Gl.use_program(prog: triangle_st.prog) + Gl.bind_vertex_array(array: triangle_st.vao) Gl.draw_arrays(mode: GL_TRIANGLES, first: 0, count: 3) Gl.swap() } diff --git a/docs/language/http/http-received.md b/docs/language/http/http-received.md index 5d7d55e9..5c63ddd3 100644 --- a/docs/language/http/http-received.md +++ b/docs/language/http/http-received.md @@ -15,7 +15,7 @@ Returns how many bytes of the response body have arrived. For a request given 0 { - let next = List.first(waiting) + handler Serve(queue_st: QueueState) phase Update { + if List.len(queue_st.waiting) > 0 { + let next = List.first(queue_st.waiting) } } } diff --git a/docs/language/list/list-index_of.md b/docs/language/list/list-index_of.md index 64031bd6..02d4aee4 100644 --- a/docs/language/list/list-index_of.md +++ b/docs/language/list/list-index_of.md @@ -19,12 +19,14 @@ Parameters: ```ludic program FindSlot { - var ids: []int = new []int + state FindSlotState { + ids: []int = new []int + } - handler Locate phase Update { - let slot = List.index_of(ids, 99) + handler Locate(find_slot_st: FindSlotState) phase Update { + let slot = List.index_of(find_slot_st.ids, 99) if slot >= 0 { - List.swap(ids, slot, List.len(ids) - 1) + List.swap(find_slot_st.ids, slot, List.len(find_slot_st.ids) - 1) } } } diff --git a/docs/language/list/list-last.md b/docs/language/list/list-last.md index d4e91390..05b58894 100644 --- a/docs/language/list/list-last.md +++ b/docs/language/list/list-last.md @@ -18,11 +18,13 @@ Parameters: ```ludic program Trail { - var points: []int = new []int + state TrailState { + points: []int = new []int + } - handler Head phase Update { - if List.len(points) > 0 { - let newest = List.last(points) + handler Head(trail_st: TrailState) phase Update { + if List.len(trail_st.points) > 0 { + let newest = List.last(trail_st.points) } } } diff --git a/docs/language/list/list-len.md b/docs/language/list/list-len.md index b8ff06e9..92e1de86 100644 --- a/docs/language/list/list-len.md +++ b/docs/language/list/list-len.md @@ -18,10 +18,12 @@ Parameters: ```ludic program CountEnemies { - var enemies: []int = new []int + state CountEnemiesState { + enemies: []int = new []int + } - handler Report phase Update { - let n = List.len(enemies) + handler Report(count_enemies_st: CountEnemiesState) phase Update { + let n = List.len(count_enemies_st.enemies) } } ``` diff --git a/docs/language/list/list-pop.md b/docs/language/list/list-pop.md index 180664e0..10dc5072 100644 --- a/docs/language/list/list-pop.md +++ b/docs/language/list/list-pop.md @@ -18,11 +18,13 @@ Parameters: ```ludic program UndoStack { - var history: []int = new []int + state UndoStackState { + history: []int = new []int + } - handler Undo phase Update { - if List.len(history) > 0 { - let last_action = List.pop(history) + handler Undo(undo_stack_st: UndoStackState) phase Update { + if List.len(undo_stack_st.history) > 0 { + let last_action = List.pop(undo_stack_st.history) } } } diff --git a/docs/language/list/list-push.md b/docs/language/list/list-push.md index 7b848f0a..990e724c 100644 --- a/docs/language/list/list-push.md +++ b/docs/language/list/list-push.md @@ -19,10 +19,12 @@ Parameters: ```ludic program Collect { - var picked: []int = new []int + state CollectState { + picked: []int = new []int + } - handler Grab phase Update { - List.push(picked, 7) + handler Grab(collect_st: CollectState) phase Update { + List.push(collect_st.picked, 7) } } ``` diff --git a/docs/language/list/list-reverse.md b/docs/language/list/list-reverse.md index 103a9b2d..5111be07 100644 --- a/docs/language/list/list-reverse.md +++ b/docs/language/list/list-reverse.md @@ -18,11 +18,13 @@ Parameters: ```ludic program PathOrder { - var path: []int = new []int + state PathOrderState { + path: []int = new []int + } - handler Finalize phase Update { + handler Finalize(path_order_st: PathOrderState) phase Update { # path was built from the goal back to the start - List.reverse(path) # now start -> goal + List.reverse(path_order_st.path) # now start -> goal } } ``` diff --git a/docs/language/list/list-swap.md b/docs/language/list/list-swap.md index ab9f8b03..3bec3b47 100644 --- a/docs/language/list/list-swap.md +++ b/docs/language/list/list-swap.md @@ -20,13 +20,15 @@ Parameters: ```ludic program RemoveFast { - var actors: []int = new []int + state RemoveFastState { + actors: []int = new []int + } - handler Kill phase Update { + handler Kill(remove_fast_st: RemoveFastState) phase Update { let dead = 2 - let last = List.len(actors) - 1 - List.swap(actors, dead, last) # move it to the end... - let removed = List.pop(actors) # ...then drop it + let last = List.len(remove_fast_st.actors) - 1 + List.swap(remove_fast_st.actors, dead, last) # move it to the end... + let removed = List.pop(remove_fast_st.actors) # ...then drop it } } ``` diff --git a/docs/language/math/math-abs.md b/docs/language/math/math-abs.md index 8d32bc27..bb827ee7 100644 --- a/docs/language/math/math-abs.md +++ b/docs/language/math/math-abs.md @@ -18,8 +18,8 @@ Parameters: ```ludic program ChaseDistance { - var target: int = 40 - var here: int = 12 + let target: int = 40 + let here: int = 12 handler Report phase Update { let gap = Math.abs(target - here) # 28, regardless of which is larger diff --git a/docs/language/math/math-ceil.md b/docs/language/math/math-ceil.md index 7a3583c4..83fd910a 100644 --- a/docs/language/math/math-ceil.md +++ b/docs/language/math/math-ceil.md @@ -20,7 +20,7 @@ Parameters: program RowsNeeded { const ROW_HEIGHT: fixed = 12.0 - var content_height: fixed = 30.0 + let content_height: fixed = 30.0 handler Layout phase Update { let rows = Math.ceil(content_height / ROW_HEIGHT) # 3 diff --git a/docs/language/math/math-clamp.md b/docs/language/math/math-clamp.md index 98a2e4ff..9e103b71 100644 --- a/docs/language/math/math-clamp.md +++ b/docs/language/math/math-clamp.md @@ -22,10 +22,12 @@ Parameters: program CameraBounds { const WORLD_WIDTH: int = 320 - var camera_x: int = 0 + state CameraBoundsState { + camera_x: int = 0 + } - handler Follow phase Update { - camera_x = Math.clamp(camera_x, 0, WORLD_WIDTH - 1) + handler Follow(camera_bounds_st: mut CameraBoundsState) phase Update { + camera_bounds_st.camera_x = Math.clamp(camera_bounds_st.camera_x, 0, WORLD_WIDTH - 1) } } ``` diff --git a/docs/language/math/math-floor.md b/docs/language/math/math-floor.md index 9625eeaa..167e52ce 100644 --- a/docs/language/math/math-floor.md +++ b/docs/language/math/math-floor.md @@ -20,7 +20,7 @@ Parameters: program TileIndex { const TILE_SIZE: fixed = 16.0 - var world_x: fixed = 40.5 + let world_x: fixed = 40.5 handler Locate phase Update { let column = Math.floor(world_x / TILE_SIZE) # 2 diff --git a/docs/language/math/math-inverse_lerp.md b/docs/language/math/math-inverse_lerp.md index 33860365..0fab18c8 100644 --- a/docs/language/math/math-inverse_lerp.md +++ b/docs/language/math/math-inverse_lerp.md @@ -22,7 +22,7 @@ Parameters: program HealthFraction { const MAX_HP: fixed = 200.0 - var hp: fixed = 150.0 + let hp: fixed = 150.0 handler Draw phase Render { let fraction = Math.inverse_lerp(0.0, MAX_HP, hp) # 0.75 diff --git a/docs/language/math/math-lerp.md b/docs/language/math/math-lerp.md index b91a9644..a5ac1014 100644 --- a/docs/language/math/math-lerp.md +++ b/docs/language/math/math-lerp.md @@ -20,11 +20,13 @@ Parameters: ```ludic program EaseCamera { - var camera_x: fixed = 0.0 - var target_x: fixed = 100.0 + state EaseCameraState { + camera_x: fixed = 0.0 + } + let target_x: fixed = 100.0 - handler Follow phase Update { - camera_x = Math.lerp(camera_x, target_x, 0.1) # glides a tenth of the way each frame + handler Follow(ease_camera_st: mut EaseCameraState) phase Update { + ease_camera_st.camera_x = Math.lerp(ease_camera_st.camera_x, target_x, 0.1) # glides a tenth of the way each frame } } ``` diff --git a/docs/language/math/math-max.md b/docs/language/math/math-max.md index f84fa7d0..6c3d7f8e 100644 --- a/docs/language/math/math-max.md +++ b/docs/language/math/math-max.md @@ -19,10 +19,12 @@ Parameters: ```ludic program Countdown { - var timer: int = 60 + state CountdownState { + timer: int = 60 + } - handler Tick phase Update { - timer = Math.max(timer - 1, 0) # stops at 0, never negative + handler Tick(countdown_st: mut CountdownState) phase Update { + countdown_st.timer = Math.max(countdown_st.timer - 1, 0) # stops at 0, never negative } } ``` diff --git a/docs/language/math/math-min.md b/docs/language/math/math-min.md index 700bc66c..80a7491c 100644 --- a/docs/language/math/math-min.md +++ b/docs/language/math/math-min.md @@ -19,10 +19,12 @@ Parameters: ```ludic program ClampScore { - var score: int = 0 + state ClampScoreState { + score: int = 0 + } - handler AddPoint phase Update { - score = Math.min(score + 1, 100) # never climbs past 100 + handler AddPoint(clamp_score_st: mut ClampScoreState) phase Update { + clamp_score_st.score = Math.min(clamp_score_st.score + 1, 100) # never climbs past 100 } } ``` diff --git a/docs/language/math/math-remap.md b/docs/language/math/math-remap.md index 1563cc76..1e797976 100644 --- a/docs/language/math/math-remap.md +++ b/docs/language/math/math-remap.md @@ -25,7 +25,7 @@ program HealthBar { const MAX_HP: fixed = 200.0 const BAR_WIDTH: fixed = 64.0 - var hp: fixed = 150.0 + let hp: fixed = 150.0 handler Draw phase Render { let width = Math.remap(hp, 0.0, MAX_HP, 0.0, BAR_WIDTH) # 48.0 diff --git a/docs/language/math/math-round.md b/docs/language/math/math-round.md index 7e602617..43663a1a 100644 --- a/docs/language/math/math-round.md +++ b/docs/language/math/math-round.md @@ -18,7 +18,7 @@ Parameters: ```ludic program SnapToPixel { - var smooth_x: fixed = 10.5 + let smooth_x: fixed = 10.5 handler Draw phase Render { let px = Math.round(smooth_x) # 11 diff --git a/docs/language/math/math-sign.md b/docs/language/math/math-sign.md index 23fadbe5..28b05fba 100644 --- a/docs/language/math/math-sign.md +++ b/docs/language/math/math-sign.md @@ -18,11 +18,13 @@ Parameters: ```ludic program FaceTarget { - var x: int = 8 - var target: int = 20 + state FaceTargetState { + x: int = 8 + } + let target: int = 20 - handler StepToward phase Update { - x = x + Math.sign(target - x) # +1 while target is to the right + handler StepToward(face_target_st: mut FaceTargetState) phase Update { + face_target_st.x = face_target_st.x + Math.sign(target - face_target_st.x) # +1 while target is to the right } } ``` diff --git a/docs/language/operators/op-bitwise.md b/docs/language/operators/op-bitwise.md index d4847d24..65ecc132 100644 --- a/docs/language/operators/op-bitwise.md +++ b/docs/language/operators/op-bitwise.md @@ -13,6 +13,6 @@ The bitwise operators work on the bits of an `int`: `&` (and), `|` (or), `^` (xo ```ludic const FLAG_POISONED: int = 1 const FLAG_SHIELDED: int = 2 -var status_flags: int = FLAG_POISONED | FLAG_SHIELDED -var is_shielded: bool = status_flags & FLAG_SHIELDED != 0 +let status_flags: int = FLAG_POISONED | FLAG_SHIELDED +let is_shielded: bool = status_flags & FLAG_SHIELDED != 0 ``` diff --git a/docs/language/operators/op-comment.md b/docs/language/operators/op-comment.md index 138d0329..46aad812 100644 --- a/docs/language/operators/op-comment.md +++ b/docs/language/operators/op-comment.md @@ -12,6 +12,6 @@ A comment begins with `#` and runs to the end of the line; the compiler ignores ```ludic const TILE_SIZE: int = 16 # pixels per grid cell -var score: int = 0 # reset in the Start phase +let score: int = 0 # reset in the Start phase # the head is drawn brighter than the body ``` diff --git a/docs/language/phases/phase-input.md b/docs/language/phases/phase-input.md index d4c7919c..c31444c6 100644 --- a/docs/language/phases/phase-input.md +++ b/docs/language/phases/phase-input.md @@ -15,18 +15,20 @@ order: 1 program InputExample { const DIRECTION_LEFT: int = 0 const DIRECTION_RIGHT: int = 1 - var facing: int = DIRECTION_RIGHT - - handler ReadControls phase Input { - let pressed_key = Input.key() - if pressed_key == 'a' { facing = DIRECTION_LEFT } - if pressed_key == 'd' { facing = DIRECTION_RIGHT } + state InputExampleState { + facing: int = DIRECTION_RIGHT } - handler DrawWorld phase Render { + handler ReadControls(input_example_st: mut InputExampleState) phase Input { + let pressed_key = Input.key() + if pressed_key == 'a' { input_example_st.facing = DIRECTION_LEFT } + if pressed_key == 'd' { input_example_st.facing = DIRECTION_RIGHT } + } + + handler DrawWorld(input_example_st: InputExampleState) phase Render { Screen.clear(Color.MidnightBlue) var marker_x = 40 - if facing == DIRECTION_RIGHT { marker_x = 200 } + if input_example_st.facing == DIRECTION_RIGHT { marker_x = 200 } Screen.fill_rectangle(x: marker_x, y: 100, width: 16, height: 16, color: Color.LimeGreen) Screen.show() } diff --git a/docs/language/phases/phase-lateupdate.md b/docs/language/phases/phase-lateupdate.md index 332ff390..9e05415e 100644 --- a/docs/language/phases/phase-lateupdate.md +++ b/docs/language/phases/phase-lateupdate.md @@ -16,7 +16,9 @@ program FollowCamera { property Position { column: int = 0, row: int = 0 } model Player { Position } - var camera_column: int = 0 + state FollowCameraState { + camera_column: int = 0 + } handler SpawnPlayer phase Start { spawn Player { Position { column: 12, row: 0 } } @@ -29,8 +31,8 @@ program FollowCamera { # runs after MovePlayer, so the camera sees the final position @Queries(these: [Position]) - handler TrackPlayer phase LateUpdate { - camera_column = Position.column + handler TrackPlayer(follow_camera_st: mut FollowCameraState) phase LateUpdate { + follow_camera_st.camera_column = Position.column } } ``` diff --git a/docs/language/phases/phase-overlay.md b/docs/language/phases/phase-overlay.md index d8b1cd12..231ab562 100644 --- a/docs/language/phases/phase-overlay.md +++ b/docs/language/phases/phase-overlay.md @@ -17,7 +17,7 @@ program OverlayExample { model Player { Position } const TILE_SIZE: int = 16 - var score: int = 0 + let score: int = 0 handler PlacePlayer phase Start { spawn Player { Position { column: 5, row: 6 } } diff --git a/docs/language/phases/phase-render.md b/docs/language/phases/phase-render.md index 37d04b79..1af2d1d4 100644 --- a/docs/language/phases/phase-render.md +++ b/docs/language/phases/phase-render.md @@ -17,7 +17,7 @@ program RenderExample { model Player { Position } const TILE_SIZE: int = 16 - var score: int = 0 + let score: int = 0 handler PlacePlayer phase Start { spawn Player { Position { column: 5, row: 6 } } diff --git a/docs/language/phases/phase-start.md b/docs/language/phases/phase-start.md index 6357100a..33cdb78d 100644 --- a/docs/language/phases/phase-start.md +++ b/docs/language/phases/phase-start.md @@ -17,18 +17,20 @@ program StartExample { model Player { Position } const GRID_WIDTH: int = 20 - var score: int = 0 + state StartExampleState { + score: int = 0 + } - handler SetUpWorld phase Start { + handler SetUpWorld(start_example_st: mut StartExampleState) phase Start { Random.seed(value: 1) Map.size(width: GRID_WIDTH, height: 15) - score = 0 + start_example_st.score = 0 spawn Player { Position { column: 10, row: 7 } } } - handler DrawWorld phase Render { + handler DrawWorld(start_example_st: StartExampleState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 4, y: 4, value: score, color: Color.Gold, scale: 1) + Screen.draw_number(x: 4, y: 4, value: start_example_st.score, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/phases/phase-update.md b/docs/language/phases/phase-update.md index 8548ff66..1cccff53 100644 --- a/docs/language/phases/phase-update.md +++ b/docs/language/phases/phase-update.md @@ -17,24 +17,26 @@ program UpdateExample { model Enemy { Position } const GRID_WIDTH: int = 20 - var elapsed_frames: int = 0 - var score: int = 0 + state UpdateExampleState { + elapsed_frames: int = 0 + score: int = 0 + } - handler SpawnWave phase Update { - elapsed_frames = elapsed_frames + 1 - if elapsed_frames % 30 == 0 { + handler SpawnWave(update_example_st: mut UpdateExampleState) phase Update { + update_example_st.elapsed_frames = update_example_st.elapsed_frames + 1 + if update_example_st.elapsed_frames % 30 == 0 { let spawn_column = Random.range(low: 0, high: GRID_WIDTH - 1) spawn Enemy { Position { column: spawn_column, row: 0 } } - score = score + 1 + update_example_st.score = update_example_st.score + 1 } } - handler DrawWorld phase Render { + handler DrawWorld(update_example_st: UpdateExampleState) phase Render { Screen.clear(Color.MidnightBlue) for (current_position) in query [Position, {Enemy}] { Screen.fill_rectangle(x: current_position.column * 16, y: current_position.row * 16, width: 16, height: 16, color: Color.Crimson) } - Screen.draw_number(x: 4, y: 4, value: score, color: Color.Gold, scale: 1) + Screen.draw_number(x: 4, y: 4, value: update_example_st.score, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/process/process-poll.md b/docs/language/process/process-poll.md index 7a8c2652..de9cf911 100644 --- a/docs/language/process/process-poll.md +++ b/docs/language/process/process-poll.md @@ -17,14 +17,16 @@ A child ended by a signal (macOS) answers 128 plus the signal numbe ```ludic program Demo { - var game: int = -1 - handler Watch phase Update { - if game >= 0 { - let code = Process.poll(handle: game) + state DemoState { + game: int = -1 + } + handler Watch(demo_st: mut DemoState) phase Update { + if demo_st.game >= 0 { + let code = Process.poll(handle: demo_st.game) if code >= 0 or code < -1 { if code != 0 { print("the game crashed") } - Process.free(handle: game) - game = -1 + Process.free(handle: demo_st.game) + demo_st.game = -1 } } } diff --git a/docs/language/scenes/kw-enter.md b/docs/language/scenes/kw-enter.md index 66051c11..28edc52e 100644 --- a/docs/language/scenes/kw-enter.md +++ b/docs/language/scenes/kw-enter.md @@ -16,11 +16,13 @@ program EnterHookExample { property Position { column: int = 0, row: int = 0 } model Player { Position } - var score: int = 0 + state EnterHookExampleState { + score: int = 0 + } scene Overworld start { - on enter { - score = 0 + on enter (enter_hook_example_st: mut EnterHookExampleState) { + enter_hook_example_st.score = 0 spawn Player { Position { column: 5, row: 5 } } } layer World { diff --git a/docs/language/scenes/kw-layer.md b/docs/language/scenes/kw-layer.md index ce303efb..9eabd725 100644 --- a/docs/language/scenes/kw-layer.md +++ b/docs/language/scenes/kw-layer.md @@ -16,7 +16,7 @@ program LayerExample { property Position { column: int = 0, row: int = 0 } model Player { Position } - var score: int = 0 + let score: int = 0 scene Overworld start { on enter { spawn Player { Position { column: 6, row: 6 } } } diff --git a/docs/language/scenes/kw-on.md b/docs/language/scenes/kw-on.md index 7425b143..8264ad1b 100644 --- a/docs/language/scenes/kw-on.md +++ b/docs/language/scenes/kw-on.md @@ -16,7 +16,9 @@ program SceneHookExample { property Position { column: int = 0, row: int = 0 } model Enemy { Position } - var elapsed_frames: int = 0 + state SceneHookExampleState { + elapsed_frames: int = 0 + } scene Overworld start { layer World { @@ -31,8 +33,8 @@ program SceneHookExample { } scene Battle { - on enter { - elapsed_frames = 0 + on enter (scene_hook_example_st: mut SceneHookExampleState) { + scene_hook_example_st.elapsed_frames = 0 spawn Enemy { Position { column: 8, row: 4 } } } on exit { diff --git a/docs/language/scenes/kw-scene.md b/docs/language/scenes/kw-scene.md index 02f35e92..d4c8edf9 100644 --- a/docs/language/scenes/kw-scene.md +++ b/docs/language/scenes/kw-scene.md @@ -13,13 +13,15 @@ A `scene` is one of the mutually-exclusive states a game moves between — a tit ```ludic program SceneExample { - var elapsed_frames: int = 0 + state SceneExampleState { + elapsed_frames: int = 0 + } scene Title start { - on enter { elapsed_frames = 0 } + on enter (scene_example_st: mut SceneExampleState) { scene_example_st.elapsed_frames = 0 } layer Main { - handler WaitForStart phase Update { - elapsed_frames = elapsed_frames + 1 + handler WaitForStart(scene_example_st: mut SceneExampleState) phase Update { + scene_example_st.elapsed_frames = scene_example_st.elapsed_frames + 1 if Input.key() != 0 { become Overworld } } handler DrawTitle phase Render { @@ -31,7 +33,7 @@ program SceneExample { } scene Overworld { - on enter { elapsed_frames = 0 } + on enter (scene_example_st: mut SceneExampleState) { scene_example_st.elapsed_frames = 0 } layer World { handler DrawWorld phase Render { Screen.clear(Color.MidnightBlue) diff --git a/docs/language/screen/screen-clear.md b/docs/language/screen/screen-clear.md index e10596a2..a00a8af2 100644 --- a/docs/language/screen/screen-clear.md +++ b/docs/language/screen/screen-clear.md @@ -18,15 +18,17 @@ Parameters: ```ludic program ClearBackground { - var elapsed_frames: int = 0 - - handler CountFrames phase Update { - elapsed_frames = elapsed_frames + 1 + state ClearBackgroundState { + elapsed_frames: int = 0 } - handler DrawWorld phase Render { + handler CountFrames(clear_background_st: mut ClearBackgroundState) phase Update { + clear_background_st.elapsed_frames = clear_background_st.elapsed_frames + 1 + } + + handler DrawWorld(clear_background_st: ClearBackgroundState) phase Render { Screen.clear(Color.MidnightBlue) - Screen.draw_number(x: 8, y: 8, value: elapsed_frames, color: Color.White, scale: 1) + Screen.draw_number(x: 8, y: 8, value: clear_background_st.elapsed_frames, color: Color.White, scale: 1) Screen.show() } } diff --git a/docs/language/screen/screen-draw_number.md b/docs/language/screen/screen-draw_number.md index c8f41f91..1c7c4bd5 100644 --- a/docs/language/screen/screen-draw_number.md +++ b/docs/language/screen/screen-draw_number.md @@ -22,16 +22,18 @@ Parameters: ```ludic program ScoreHud { - var score: int = 0 - - handler EarnPoints phase Update { - score = score + 1 + state ScoreHudState { + score: int = 0 } - handler DrawWorld phase Render { + handler EarnPoints(score_hud_st: mut ScoreHudState) phase Update { + score_hud_st.score = score_hud_st.score + 1 + } + + handler DrawWorld(score_hud_st: ScoreHudState) phase Render { Screen.clear(Color.MidnightBlue) Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) - Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1) + Screen.draw_number(x: 52, y: 4, value: score_hud_st.score, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/screen/screen-draw_text.md b/docs/language/screen/screen-draw_text.md index 11e53020..4a281546 100644 --- a/docs/language/screen/screen-draw_text.md +++ b/docs/language/screen/screen-draw_text.md @@ -22,10 +22,12 @@ Parameters: ```ludic program TitleScreen { - var elapsed_frames: int = 0 + state TitleScreenState { + elapsed_frames: int = 0 + } - handler CountFrames phase Update { - elapsed_frames = elapsed_frames + 1 + handler CountFrames(title_screen_st: mut TitleScreenState) phase Update { + title_screen_st.elapsed_frames = title_screen_st.elapsed_frames + 1 } handler DrawWorld phase Render { diff --git a/docs/language/screen/screen-status.md b/docs/language/screen/screen-status.md index 7d029964..f3d31876 100644 --- a/docs/language/screen/screen-status.md +++ b/docs/language/screen/screen-status.md @@ -18,11 +18,13 @@ Parameters: ```ludic program StatusLine { - var elapsed_frames: int = 0 + state StatusLineState { + elapsed_frames: int = 0 + } - handler UpdateStatus phase Update { - elapsed_frames = elapsed_frames + 1 - Screen.status(text: `frame {elapsed_frames}`) + handler UpdateStatus(status_line_st: mut StatusLineState) phase Update { + status_line_st.elapsed_frames = status_line_st.elapsed_frames + 1 + Screen.status(text: `frame {status_line_st.elapsed_frames}`) } handler DrawWorld phase Render { diff --git a/docs/language/structure/kw-enum.md b/docs/language/structure/kw-enum.md index 938d4321..29aa627f 100644 --- a/docs/language/structure/kw-enum.md +++ b/docs/language/structure/kw-enum.md @@ -40,18 +40,20 @@ A tagged `match` must be **exhaustive**: it either handles every variant or ends program BattleMenu { enum Action { Attack, Guard, Item, Flee } - var current_action: int = Action.Attack - - handler ChooseAction phase Input { - let pressed = Input.key() - if pressed == 'a' { current_action = Action.Attack } - if pressed == 'g' { current_action = Action.Guard } - if pressed == 'f' { current_action = Action.Flee } + state BattleMenuState { + current_action: int = Action.Attack } - handler DrawWorld phase Render { + handler ChooseAction(battle_menu_st: mut BattleMenuState) phase Input { + let pressed = Input.key() + if pressed == 'a' { battle_menu_st.current_action = Action.Attack } + if pressed == 'g' { battle_menu_st.current_action = Action.Guard } + if pressed == 'f' { battle_menu_st.current_action = Action.Flee } + } + + handler DrawWorld(battle_menu_st: BattleMenuState) phase Render { Screen.clear(Color.MidnightBlue) - match current_action { + match battle_menu_st.current_action { Action.Attack => Screen.draw_text(x: 8, y: 8, text: "ATTACK", color: Color.Crimson, scale: 2) Action.Guard => Screen.draw_text(x: 8, y: 8, text: "GUARD", color: Color.White, scale: 2) _ => Screen.draw_text(x: 8, y: 8, text: "FLEE", color: Color.Gold, scale: 2) diff --git a/docs/language/structure/kw-program.md b/docs/language/structure/kw-program.md index dd12dafb..6febb721 100644 --- a/docs/language/structure/kw-program.md +++ b/docs/language/structure/kw-program.md @@ -16,7 +16,7 @@ program SpaceDrift { property Position { column: int = 0, row: int = 0 } model Player { Position } - var score: int = 0 + let score: int = 0 handler Boot phase Start { spawn Hero { Position { column: 10, row: 8 } } diff --git a/docs/language/structure/kw-ui.md b/docs/language/structure/kw-ui.md index f46b139e..bb3dd9fc 100644 --- a/docs/language/structure/kw-ui.md +++ b/docs/language/structure/kw-ui.md @@ -9,22 +9,24 @@ tip: Declare a retained widget tree as data; the engine lays it out and draws it order: 50 --- -A ui block declares a retained widget tree as data — panels, labels and buttons — and hands layout, drawing and keyboard focus to the engine, so you describe the interface once instead of repainting it every frame. Widget types are `panel` (a container with optional skin/background/border), `col`/`row` (pure stacks), `label`, `button` (focusable), `image` and `spacer`, and their props are evaluated at build time, so `font: title_font` reads a value the program set first. Each `id: Name` mints a `UI_Name` handle you drive from handlers: call `Ui.build()` then `Ui.open(UI_MainMenu)` at start, `Ui.tick(Input.key())` each update to move focus and activate, and `Ui.clicked(UI_Name)` to react. Draw it during `Render` with `Ui.render()` between `Screen.clear` and `Screen.show()`. +A ui block declares a retained widget tree as data — panels, labels and buttons — and hands layout, drawing and keyboard focus to the engine, so you describe the interface once instead of repainting it every frame. Widget types are `panel` (a container with optional skin/background/border), `col`/`row` (pure stacks), `label`, `button` (focusable), `image` and `spacer`, and their props are evaluated at build time, so `font: TitleScreenState.title_font` reads a value the program set first (a `ui` block names a state's one instance by the state's name). Each `id: Name` mints a `UI_Name` handle you drive from handlers: call `Ui.build()` then `Ui.open(UI_MainMenu)` at start, `Ui.tick(Input.key())` each update to move focus and activate, and `Ui.clicked(UI_Name)` to react. Draw it during `Render` with `Ui.render()` between `Screen.clear` and `Screen.show()`. ```ludic program TitleScreen { - var title_font: int = 0 + state TitleScreenState { + title_font: int = 0 + } ui MainMenu { panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c align: center { - label text: "CHRONO RIFT" font: title_font size: 26 fg: 0xffe060 align: center - button id: NewGame text: "New Game" font: title_font size: 16 w: 236 - button id: Quit text: "Quit" font: title_font size: 16 w: 236 + label text: "CHRONO RIFT" font: TitleScreenState.title_font size: 26 fg: 0xffe060 align: center + button id: NewGame text: "New Game" font: TitleScreenState.title_font size: 16 w: 236 + button id: Quit text: "Quit" font: TitleScreenState.title_font size: 16 w: 236 } } - handler Boot phase Start { - title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf") + handler Boot(title_screen_st: mut TitleScreenState) phase Start { + title_screen_st.title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf") Ui.build() Ui.open(UI_MainMenu) } diff --git a/docs/language/structure/kw-var.md b/docs/language/structure/kw-var.md index a9a79ecc..c5f95e0f 100644 --- a/docs/language/structure/kw-var.md +++ b/docs/language/structure/kw-var.md @@ -13,18 +13,20 @@ A var declares a mutable binding: unlike `let`, you may reas ```ludic program Scorekeeper { - var score: int = 0 - var elapsed_frames: int = 0 - - handler Tick phase Update { - elapsed_frames = elapsed_frames + 1 - if elapsed_frames % 60 == 0 { score = score + 1 } + state ScorekeeperState { + score: int = 0 + elapsed_frames: int = 0 } - handler DrawWorld phase Render { + handler Tick(scorekeeper_st: mut ScorekeeperState) phase Update { + scorekeeper_st.elapsed_frames = scorekeeper_st.elapsed_frames + 1 + if scorekeeper_st.elapsed_frames % 60 == 0 { scorekeeper_st.score = scorekeeper_st.score + 1 } + } + + handler DrawWorld(scorekeeper_st: ScorekeeperState) phase Render { Screen.clear(Color.MidnightBlue) Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) - Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1) + Screen.draw_number(x: 52, y: 4, value: scorekeeper_st.score, color: Color.Gold, scale: 1) Screen.show() } } diff --git a/docs/language/text/text-char_at.md b/docs/language/text/text-char_at.md index c5bfe53b..485e4da5 100644 --- a/docs/language/text/text-char_at.md +++ b/docs/language/text/text-char_at.md @@ -19,7 +19,7 @@ Parameters: ```ludic program FirstIsDigit { - var entry: string = "7up" + let entry: string = "7up" handler Check phase Update { let c = Text.char_at(entry, 0) diff --git a/docs/language/text/text-concat.md b/docs/language/text/text-concat.md index 92be1bde..01070435 100644 --- a/docs/language/text/text-concat.md +++ b/docs/language/text/text-concat.md @@ -19,7 +19,7 @@ Parameters: ```ludic program Greeting { - var name: string = "world" + let name: string = "world" handler Build phase Update { let line = Text.concat("hello ", name) # "hello world" diff --git a/docs/language/text/text-contains.md b/docs/language/text/text-contains.md index b71732ff..5d8db7b8 100644 --- a/docs/language/text/text-contains.md +++ b/docs/language/text/text-contains.md @@ -19,7 +19,7 @@ Parameters: ```ludic program Filter { - var line: string = "error: disk full" + let line: string = "error: disk full" handler Scan phase Update { if Text.contains(line, "error") { diff --git a/docs/language/text/text-ends_with.md b/docs/language/text/text-ends_with.md index b1c4ed90..b1d80770 100644 --- a/docs/language/text/text-ends_with.md +++ b/docs/language/text/text-ends_with.md @@ -19,7 +19,7 @@ Parameters: ```ludic program IsImage { - var filename: string = "hero.png" + let filename: string = "hero.png" handler Classify phase Update { let png = Text.ends_with(filename, ".png") # true diff --git a/docs/language/text/text-equals.md b/docs/language/text/text-equals.md index 1acdb6e4..0270c192 100644 --- a/docs/language/text/text-equals.md +++ b/docs/language/text/text-equals.md @@ -19,8 +19,8 @@ Parameters: ```ludic program Guess { - var answer: string = "swordfish" - var attempt: string = "swordfish" + let answer: string = "swordfish" + let attempt: string = "swordfish" handler Check phase Update { if Text.equals(attempt, answer) { diff --git a/docs/language/text/text-from_int.md b/docs/language/text/text-from_int.md index 8aaa12eb..fa47ba4e 100644 --- a/docs/language/text/text-from_int.md +++ b/docs/language/text/text-from_int.md @@ -18,7 +18,7 @@ Parameters: ```ludic program ScoreLabel { - var score: int = 1500 + let score: int = 1500 handler Build phase Update { let label = Text.concat("score: ", Text.from_int(score)) diff --git a/docs/language/text/text-index_of.md b/docs/language/text/text-index_of.md index 9eb5d5c4..6c31a8bf 100644 --- a/docs/language/text/text-index_of.md +++ b/docs/language/text/text-index_of.md @@ -19,7 +19,7 @@ Parameters: ```ludic program KeyValue { - var pair: string = "hp=42" + let pair: string = "hp=42" handler Parse phase Update { let eq = Text.index_of(pair, "=") # 2 diff --git a/docs/language/text/text-length.md b/docs/language/text/text-length.md index ec8ebb55..c87c9b6f 100644 --- a/docs/language/text/text-length.md +++ b/docs/language/text/text-length.md @@ -18,7 +18,7 @@ Parameters: ```ludic program NameWidth { - var label: string = "player one" + let label: string = "player one" handler Draw phase Render { let width = Text.length(label) * 6 # 6 px per glyph diff --git a/docs/language/text/text-slice.md b/docs/language/text/text-slice.md index 1f4529ef..725f0748 100644 --- a/docs/language/text/text-slice.md +++ b/docs/language/text/text-slice.md @@ -20,7 +20,7 @@ Parameters: ```ludic program FirstWord { - var line: string = "hello world" + let line: string = "hello world" handler Split phase Update { let space = Text.index_of(line, " ") # 5 diff --git a/docs/language/text/text-starts_with.md b/docs/language/text/text-starts_with.md index e9aa0a05..f093b34d 100644 --- a/docs/language/text/text-starts_with.md +++ b/docs/language/text/text-starts_with.md @@ -19,7 +19,7 @@ Parameters: ```ludic program Command { - var input: string = "/say hello" + let input: string = "/say hello" handler Parse phase Update { if Text.starts_with(input, "/say ") { diff --git a/docs/language/text/text-to_float.md b/docs/language/text/text-to_float.md index 52bd549c..de0044dd 100644 --- a/docs/language/text/text-to_float.md +++ b/docs/language/text/text-to_float.md @@ -18,7 +18,7 @@ Parameters: ```ludic program ParseSpeed { - var typed: string = "3.2" + let typed: string = "3.2" handler Apply phase Update { let speed = Text.to_float(typed) # 3.2 diff --git a/docs/language/text/text-to_int.md b/docs/language/text/text-to_int.md index fc52b6c1..32e7bf03 100644 --- a/docs/language/text/text-to_int.md +++ b/docs/language/text/text-to_int.md @@ -18,7 +18,7 @@ Parameters: ```ludic program ParseAmount { - var typed: string = "250" + let typed: string = "250" handler Apply phase Update { let gold = Text.to_int(typed) # 250 diff --git a/docs/language/types/type-fixed.md b/docs/language/types/type-fixed.md index 484f980d..4807603a 100644 --- a/docs/language/types/type-fixed.md +++ b/docs/language/types/type-fixed.md @@ -13,7 +13,7 @@ order: 1 ```ludic const GRAVITY: fixed = 0.5 -var velocity_y: fixed = fixed(0) -var next_velocity: fixed = velocity_y + GRAVITY -var pixel_row: int = floor(next_velocity) +let velocity_y: fixed = fixed(0) +let next_velocity: fixed = velocity_y + GRAVITY +let pixel_row: int = floor(next_velocity) ``` diff --git a/docs/language/types/type-int.md b/docs/language/types/type-int.md index fb3b050c..a746f779 100644 --- a/docs/language/types/type-int.md +++ b/docs/language/types/type-int.md @@ -12,8 +12,8 @@ order: 0 `int` is a 32-bit signed integer and the default numeric type in Ludic: a literal like `42` or a hex literal like `0x1E90FF` is an `int`. It does far more than counting — colors are `int` hex values, the key from `Input.key()` is an `int` character code, and a tile from `Map.tile` is an `int`. Arithmetic (`+ - * / %`), comparison, and the bitwise operators all work on it, and it is the natural type for a `score`, a `column`, or an `elapsed_frames` counter. When you need fractions, reach for `fixed` and convert with `fx` / `flr`. ```ludic -var score: int = 0 +let score: int = 0 const GRID_WIDTH: int = 20 -var next_column: int = Random.range(low: 0, high: GRID_WIDTH - 1) -var next_score: int = score + 1 +let next_column: int = Random.range(low: 0, high: GRID_WIDTH - 1) +let next_score: int = score + 1 ``` diff --git a/packages/ludic.base/README.md b/packages/ludic.base/README.md index 3863dfd7..e2884e12 100644 --- a/packages/ludic.base/README.md +++ b/packages/ludic.base/README.md @@ -22,8 +22,10 @@ import "ludic.base" member and never binds it. A member with a default may be left out. 3. **Queues for facts.** What HAPPENED is pushed onto a `Queue` the mechanic owns (`caught`) and drained by the game in a later phase. A mechanic never acts on another's behalf. -4. **Verbs for changes.** A mechanic's state changes only through its exported functions - (`fishing_cast`, `pack_add`). Nothing assigns another module's globals. +4. **Verbs for changes.** A mechanic's data is its `state` (`ToyFishingState`), handed to its + functions as a parameter, and it changes only through its exported functions (`fishing_cast`, + `pack_add`). Nothing writes another module's state; a function value (`fn fishing_tick`) is + called with its states supplied, so a system never passes them itself. 5. **Phases for order.** A system names its phase; within a phase the game's list is the order. No system says "after fishing". 6. **Its own save section.** Each system saves under its own key with its own version and @@ -56,26 +58,28 @@ numbers float import "ludic.base" export property Caught { species: int = 0, weight: float = 0.0 } export port FishingWorld { is_water: fn(float, float) -> bool } # its port -var dice: Rng = null -var casts: int = 0 -export var caught: Queue = null # its facts -export function fishing_cast(x: float, z: float) -> bool { # its verb +export state ToyFishingState { + dice: Rng = null + casts: int = 0 + caught: Queue = null # its facts +} +export function fishing_cast(toy_fishing_st: mut ToyFishingState, x: float, z: float) -> bool { # its verb if not FishingWorld.is_water(x, z) { return false } - casts += 1 + toy_fishing_st.casts += 1 return true } -function fishing_reset() -> void { - casts = 0 - dice = rng_new(7) - caught = queue_new("fishing.caught") +function fishing_reset(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) -> void { + toy_fishing_st.casts = 0 + toy_fishing_st.dice = rng_new(7) + toy_fishing_st.caught = queue_new(base_st, "fishing.caught") } -function fishing_tick(t: Tick) -> void { - while casts > 0 { +function fishing_tick(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, t: Tick) -> void { + while toy_fishing_st.casts > 0 { let c = new Caught - c.species = rng_between(dice, 0, 2) - c.weight = 0.5 + rng_float(dice) - q_push(caught, c) - casts -= 1 + c.species = rng_between(toy_fishing_st.dice, 0, 2) + c.weight = 0.5 + rng_float(toy_fishing_st.dice) + q_push(base_st, toy_fishing_st.caught, c) + toy_fishing_st.casts -= 1 } } export function fishing_system() -> System { @@ -102,18 +106,18 @@ program Game { bind FishingWorld { is_water: fn lake } # the route: a landed fish goes into the pack - function route_fishing_pack(t: Tick) -> void { - let fish = q_drain(caught) - for i in 0 .. len(fish) { pack_add(fish[i].species, 1) } + function route_fishing_pack(base_st: mut BaseState, fishing: ToyFishingState, pack: mut ToyPackState, t: Tick) -> void { + let fish = q_drain(base_st, fishing.caught) + for i in 0 .. len(fish) { pack_add(pack, fish[i].species, 1) } } - function game_start() -> void { - core_add(fishing_system()) + function game_start(base_st: mut BaseState) -> void { + core_add(base_st, fishing_system()) let r = system_new("route.fishing_pack", PH_RESOLVE) - r.tick = fn route_fishing_pack - core_add(r) - core_add(pack_system()) - core_reset_all() + r.tick = fn route_fishing_pack # a fn(Tick) -> void: its states are supplied + core_add(base_st, r) + core_add(base_st, pack_system()) + core_reset_all(base_st) } } ```