feat(lang): 0.S - components take states in their header; migrate writes them, never inside a name, never outside the programs given, never into a package's state

- component Name (a: mut A, b: B) { ... }: every getter, default, function and event takes the
  header's states before the instance; a member's call to another passes them on; the glue is
  supplied them; the template never sees them; a read-only one is read-only in every member
- ludic migrate state: a component's members' needs go into its header (added to an existing one,
  mut added where now changed); a field read or a member call inside a component is the compiler's,
  so nothing is written inside a name and no ', )' is left; an entry point that declares states
  already gets the rest after them
- a program's module named like a package gets <Name>AppState; a program's own file its own state;
  a friend module's files go by directory; a package's settable var stays state
- it writes only under the programs and directories given (and runtime/ with --runtime), and
  refuses the whole run naming any other file that would have to change
- a name a package already moved into its state is rewritten through it; a read of the runtime's
  var through the runtime function that answers it (gl_w: gl_width())
- a state's instance supplied by the runtime is not a uses reference
- tests: state/component, rejected/state_component_ro, rendering/ui_render3d, migrate component
  and foreign cases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 17:11:15 +03:00
parent c167ecc714
commit ce80e64b24
23 changed files with 67967 additions and 61444 deletions

View file

@ -249,6 +249,7 @@ function hips_of(h: Hiker) -> int { return h.hips }
{ ... }`, `@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 component, whose header names its states: `component Tally (score: mut Score) { ... }`;
- 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;
@ -287,15 +288,26 @@ each program and, from the compiler's own view of every name:
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.
5. each call passes them on, each entry point declares them (after any it declares already), and
each component's header declares what all its members need.
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:
them see different files of is one state, and a path reached as `../../packages/x` is the same file
as `packages/x`. A program's own file keeps a state of its own (named for the program) whatever module it says, and
a `friend module`'s other files go by their directory, since each program declares that module for
itself. A package's var nothing in the given programs writes stays changing data (a game may set
`r3d_dem_path`); only a private one of a package's module becomes a `let`. A name the program uses
that a package migrated earlier moved into its state (`cam_pos`, now `Render3dState`'s) is rewritten
through that state, and a read of the runtime's own var from outside it through the runtime function
that answers it (`gl_w` is `gl_width()`). A program's own module named like a package (a game's `module fishing` beside
`ludic.fishing`) gets a state of its own, `FishingAppState` / `fishing_app_st`, never the package's.
It writes only under the programs and directories it is given (and `runtime/` with `--runtime`):
when the programs need a change in a file anywhere else - an installed package, a module imported
from a neighbouring directory - it says which and changes nothing. 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 <every example program> packages/ludic.lab/example/plate.ludic
@ -589,6 +601,22 @@ component Counter {
files again when they change on disk and keeps every instance's state.
- **A screen is a component** with no props: `ui_show("Counter", null, x, y, w, h)`, or
`ui_nodes("Counter", null)` for a test.
- **States in the header.** A component that reads the program's data names the states it reads
and changes after its name, as an entry point does:
```ludic
# doc-check: skip — a fragment of a program that imports ludic.ui
component Tally (score: mut Score, look: Look) {
total: int = score.points
function big() -> bool { return total > look.big }
on add(n: int) { score.points += n }
}
```
Every getter, default, function and event takes them before the instance; a member's call to
another passes them on; ludic.ui's calls into the component are given the instances; and the
template never sees them - `on-click="add(5)"` passes only `5`. A header state without `mut` is
read-only in every member.
An older, lighter bridge remains: `view Name { ... }` gives a whole template file of `<screen>`s
and `<component>`s (loaded with `ui_load`) one model and one call, and the rest of this section

View file

@ -25,3 +25,12 @@ Outside the runtime, the drawable's size and scale, the screen's framebuffer and
size are read and set through its functions (`gl_width()`, `gl_set_drawable(w, h)`,
`gl_screen_fbo()`, `gl_pixel_scale()`, `rt_map_tile_size()`), and ludic.ui's render3d backend
installs itself through the `UiRenderers` registry.
A component names the states its members read and change in its header, `component Tally (score:
mut Score, look: Look) { ... }`: every getter, default, function and event takes them before the
instance, a member's call to another passes them on, ludic.ui's calls into it are supplied them, and
the template never sees them. `ludic migrate state` writes those headers (and adds to an entry
point's or a component's existing list), gives a program's module named like a package a state of
its own (`FishingAppState`), keeps a package's settable vars as state, rewrites a program's
references to vars a package already moved into its state, and writes only under the programs and
directories it is given - a change needed anywhere else stops the run with the files named.

View file

@ -0,0 +1,7 @@
# 0.S: a component's states are read-only unless its header says mut
import "ludic.ui"
program StateComponentRo {
state Look { size: int = 1 }
import "state_component_ro/Shelf.ludic"
entry { print(1) }
}

View file

@ -0,0 +1,3 @@
component Shelf (look: Look) {
on grow() { look.size += 1 }
}

View file

@ -0,0 +1 @@
<div><button on-click="grow()">+</button></div>

View file

@ -0,0 +1,13 @@
# ui_render3d.ludic - ludic.ui drawn by render3d's overlay: importing its backend is enough to
# draw templates (the suite builds it, so the backend always compiles against the renderer)
program UiRender3d {
numbers float
import "ludic.render3d/r3d.ludic"
import "ludic.ui"
import "ludic.ui/render3d.ludic"
entry (ui_st: mut UiState) {
ui_render3d(ui_st)
ui_render3d_scale(ui_st, 1.5)
print("ok")
}
}

View file

@ -0,0 +1,33 @@
# component.ludic - 0.S: a component's members take the states its header names; the runtime
# supplies them when the template asks (Tally.ludic)
import "ludic.ui"
program StateComponent {
numbers float
state Score {
points: int = 0
bigs: int = 0
}
state Look {
prefix: string = "#"
big: int = 7
}
import "component_parts/Tally.ludic"
function all_text(n: UiNode) -> string {
var out = n.text
for i in 0 .. len(n.children) { out = out + all_text(n.children[i]) }
return out
}
function frame(ui_st: mut UiState) -> UiNode {
let root: UiNode = ui_nodes(ui_st, "Tally", null)
ui_place(ui_st, root, 0.0, 0.0, 300.0, 200.0)
return root
}
entry (ui_st: mut UiState, score: Score) {
var root = frame(ui_st)
ui_press(ui_st, root.children[0].children[1])
root = frame(ui_st)
ui_press(ui_st, root.children[0].children[1])
root = frame(ui_st)
print(`{all_text(root.children[0].children[0])} {score.points} {score.bigs}`)
}
}

View file

@ -0,0 +1,12 @@
# Tally.ludic - a component that reads and changes states: its header names them, every member
# takes them, and the template never sees them
component Tally (score: mut Score, look: Look) {
prop label: string
total: int = score.points
shown: string = `{look.prefix}{total}`
function big() -> bool { return total > look.big }
on add(n: int) {
score.points += n
if big() { score.bigs += 1 }
}
}

View file

@ -0,0 +1,4 @@
<div>
<p>{label} {shown}{big() ? ' big' : ''}</p>
<button on-click="add(5)">+5</button>
</div>

View file

@ -35,6 +35,9 @@ function vis_check(d: Node, what0: pointer) -> void {
deps_edge(d, what0) # LUDIC_DEPS: the graph ludic deps reads (emit_deps.ludic)
if g_vis_off { return }
if d.file == null { return }
# 0.S: a state's instance, supplied by the runtime (a component's or a view's glue, an entry's
# parameters): the code that names it only passes it on
if d.kind == N_VAR and d.uns == 1 and is_state_ty(d.ty) and len(d.s) > 6 and (d.s[0 .. 6] == "state$") { return }
let what = vis_plain(what0)
var here = g_err_file
if here == null { return }

View file

@ -74,6 +74,11 @@ function ck_id(e: Node) -> pointer {
ck_vis(g, e.s, e)
return g.ty
}
let fv = mg_field_var(e.s) # 0.S2: a var a package moved into its state
if fv != null {
mg_ref(e, fv)
return fv.ty
}
return "?"
}
# --check: the module rules (uses, export, ports - emit_vis.ludic) asked here, where every reference

View file

@ -45,7 +45,8 @@ function ck_let_from(e: Node, ty: pointer) -> void {
return
}
if li >= 0 { return }
let g = ck_global(r.s)
var g = ck_global(r.s)
if g == null { g = mg_field_var(r.s) }
if g != null and g.kind == N_VAR { ck_alias[k] = g }
}
function ck_is_ro(li: int) -> bool {
@ -90,7 +91,8 @@ function ck_write_check(t: Node, what: pointer) -> void {
}
return
}
let g = ck_global(r.s)
var g = ck_global(r.s)
if g == null { g = mg_field_var(r.s) }
mg_write(t, g)
if ck_module_let(g) {
var name = r.s

View file

@ -93,6 +93,7 @@ property Node {
pos2: int = -1 # 0.S: a second place a rewrite needs (a declaration's end, a '(')
mg: int = -1 # 0.S2: its index in the migration's tables (migrate.ludic)
vw: int = -1 # 0.S: the view a function was generated for or written in
cm: int = -1 # 0.S: the component a function was generated for or written in
}
# every node remembers where it was parsed (file + the line of the token the

View file

@ -11,6 +11,11 @@
# on buy() { taps += 1 } # props and state are plain names here
# }
#
# 0.S: `component PauseMenu (clock: ClockState, s: mut Session) { ... }` - the states its members
# read and change. Every getter, default, function and event takes them before the instance, the
# glue's calls are supplied them by the runtime, a member's call to another passes them on, and
# the template never sees them.
#
# Each mounted instance is a record of its props and state. The compiler reads the template and the
# styles into the program, so a missing template fails the build and nothing more has to ship, and
# registers the component with ludic.ui when the program starts.
@ -27,6 +32,8 @@ var g_cm_fnames: [][]pointer = new [][]pointer
var g_cm_calls: [][]Node = new [][]Node
var g_cm_cnames: [][]pointer = new [][]pointer
var g_cm_head: []Node = new []Node
var g_cm_hdr: []int = new []int # 0.S: where state parameters go (after the name)
var g_cm_ps: [][]Node = new [][]Node # the states it declares
function cm_prefix(name: pointer) -> pointer { return `cmp_{reg_lower(name)}_` }
@ -34,6 +41,8 @@ function cm_prefix(name: pointer) -> pointer { return `cmp_{reg_lower(name)}_` }
function parse_ui_component() -> void {
pi += 1
let name = eat_id()
push(g_cm_hdr, toks[pi - 1].end)
push(g_cm_ps, parse_entry_params())
cm_unique(name)
let px = cm_prefix(name)
g_uses_value = true
@ -79,6 +88,7 @@ function cm_member(c: int, px: pointer) -> void {
pi += 1
def = cm_getter(px + "def_" + fname, ty, expr())
}
if def != null { def.cm = c }
push(g_cm_defs[c], def)
return
}
@ -86,6 +96,7 @@ function cm_member(c: int, px: pointer) -> void {
let ev = is_id("on")
if ev { toks[pi].text = "function" }
let f = parse_fn()
f.cm = c
push(g_cm_cnames[c], f.s)
if ev { f.s = px + "on_" + f.s } else { f.s = px + f.s }
push(g_cm_calls[c], f)
@ -101,7 +112,9 @@ function cm_member(c: int, px: pointer) -> void {
if not is_op("=") { perr(`component field {fname}: say what it is - {fname} = some_global`) }
pi += 1
push(g_cm_fnames[c], fname)
push(g_cm_fields[c], cm_getter(px + "get_" + fname, ty, expr()))
let g = cm_getter(px + "get_" + fname, ty, expr())
g.cm = c
push(g_cm_fields[c], g)
}
# a function of the instance that answers one expression (a field, or a default)
function cm_getter(name: pointer, ty: pointer, e: Node) -> Node {

View file

@ -64,6 +64,17 @@ function cm_infer(c: int, g: Node, fname: pointer) -> pointer {
i += 1
}
}
# 0.S: a state's field, through the header (`purse = home.purse`)
if e.kind == E_MEMBER and e.a != null and e.a.kind == E_ID {
var j = 0
while j < len(g_cm_ps[c]) {
if (g_cm_ps[c][j].s == e.a.s) {
let r = find_comp(g_cm_ps[c][j].ty)
if r != null and field_index(r, e.s) >= 0 { return field_type(r, e.s) }
}
j += 1
}
}
if e.kind == E_ID {
let d = find_global(e.s)
if d != null and d.ty != null { return d.ty }

View file

@ -55,10 +55,11 @@ function cm_call_src(c: int, px: pointer, rec: pointer, head: Node) -> pointer {
var i = 0
while i < len(calls) {
let f = calls[i]
var args = "s"
var k = 1
var args = "s" # 0.S: its states are supplied, not passed
let lead = len(g_cm_ps[c]) + 1
var k = lead
while k < len(f.kids) {
args = args + ", " + cm_arg(c, f.kids[k], k - 1)
args = args + ", " + cm_arg(c, f.kids[k], k - lead)
k += 1
}
src = src + ` if name == "{g_cm_cnames[c][i]}" {{\n`

View file

@ -25,6 +25,26 @@ function cm_rewrite(c: int, f: Node, self_ty: pointer) -> void {
}
f.kids = kids
cm_rw(c, f.a)
state_params_onto(f, g_cm_ps[c]) # 0.S: its states, before the instance
}
# a member's call to another passes its states on (they are its own parameters), then the instance
function cm_member_args(c: int, rest: []Node) -> []Node {
let args = new []Node
var i = 0
while i < len(g_cm_ps[c]) {
let id = node(E_ID)
id.s = g_cm_ps[c][i].s
id.pos = -1
push(args, id)
i += 1
}
push(args, cm_self())
i = 0
while i < len(rest) {
push(args, rest[i])
i += 1
}
return args
}
function cm_is_keep(c: int, s: pointer) -> bool {
var i = 0
@ -73,20 +93,13 @@ function cm_rw(c: int, n: Node) -> void {
let callee = node(E_ID)
callee.s = cm_prefix(g_cm_name[c]) + "get_" + n.s
n.a = callee
n.kids = new []Node
push(n.kids, cm_self())
n.kids = cm_member_args(c, new []Node)
n.pos = -1 # a call the compiler wrote: nothing in the source to edit
return
}
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and cm_is_call(c, n.a.s) and not cm_is_keep(c, n.a.s) {
n.a.s = cm_prefix(g_cm_name[c]) + n.a.s
let args = new []Node
push(args, cm_self())
var i = 0
while i < len(n.kids) {
push(args, n.kids[i])
i += 1
}
n.kids = args
n.kids = cm_member_args(c, n.kids)
}
cm_rw(c, n.a)
cm_rw(c, n.b)

View file

@ -14,6 +14,7 @@
# first var this program sees, or its declaration when it has one already: `more`)
# REF <file> <pos> <len> <text> a reference's name replaced
# INS <file> <pos> <text> text inserted (a parameter, an argument)
# CLOSE <file> <pos> <text> text inserted before the first `)` after <pos>
# ? <file>:<line> <what> what the tool cannot decide, for a person
var g_migrate: bool = false
var g_mg_runtime: bool = false # --migrate-runtime: the runtime's own vars too
@ -21,7 +22,8 @@ var g_mg_plan: pointer = null
var g_mg_vars: []Node = new []Node # the vars that move
var g_mg_vstate: []int = new []int # the state each moves into
var g_mg_written: []int = new []int # 1: something writes it
var g_mg_let: []int = new []int # 1: a value nothing writes - it stays, as a module-level let
var g_mg_let: []int = new []int # 1: a value nothing writes - it stays, as a module-level let;
# 2: moved already (a package's, migrated before this program)
var g_mr_unit: []int = new []int # per reference: the unit it is read in (-1: none)
var g_ms_name: []pointer = new []pointer
var g_ms_mod: []pointer = new []pointer
@ -91,7 +93,23 @@ function mg_state_of_file(f: pointer) -> int {
if len(b) > 6 and (b[len(b) - 6 .. len(b)] == ".ludic") { b = b[0 .. len(b) - 6] }
return mg_state_of_mod(`@{f}`, `rt_{b}`)
}
let m = module_for_uses(f)
if (f == g_mg_entry) { # the program's own file: its own state, whatever module
let p = mg_param_for(g_game_name)
return mg_state_of_mod("", p[0 .. len(p) - 3])
}
var m = module_for_uses(f)
# a friend module (`friend module lab`) is a view of the others each program declares for itself,
# not one module: its files' vars go by their directory, as a file in no module does
var fi = 0
while fi < len(g_mod_friends) {
if (g_mod_friends[fi] == m) { m = "" }
fi += 1
}
if not (m == "") and not (module_of(f) == "") and pkg_of_file(f) == "" and mg_pkg_named(mg_short(m)) {
# the program's own module named like a package's (a game's `module fishing` beside
# ludic.fishing): a state of its own, never merged into the package's
return mg_state_of_mod(m, mg_short(m) + "_app")
}
if not (m == "") { return mg_state_of_mod(m, mg_short(m)) }
if (f == g_mg_entry) { # the program's own: SceneDemo is SceneDemoState, scene_demo_st
let p = mg_param_for(g_game_name)
@ -166,6 +184,12 @@ function mg_state_key(s: int) -> pointer {
}
return `package:{m}`
}
# is there a package called ludic.<short> - installed with the toolchain, or fetched into ludic_modules
function mg_pkg_named(short: pointer) -> bool {
if read_file(`{ludic_home()}packages/ludic.{short}/package.ludic`) != null { return true }
let mods = ensure_slash(getenv_or("LUDIC_MODULES", "ludic_modules"))
return read_file(`{mods}ludic.{short}/package.ludic`) != null
}
function mg_state_decl(name: pointer) -> Node {
var i = 0
while i < len(prog) {
@ -190,6 +214,47 @@ function mg_is_target(v: Node) -> bool {
return true
}
# which vars move, and the state each moves into
# a name the program uses that is a field of a state now - a package migrated before it moved its
# var there (`cam_pos` is Render3dState's): a reference to that state's field, which the program is
# rewritten to reach through the state. Only a name exactly one state has.
var g_mg_fv_name: []pointer = new []pointer
var g_mg_fv_node: []Node = new []Node
function mg_field_var(name: pointer) -> Node {
if not g_migrate { return null }
var i = 0
while i < len(g_mg_fv_name) {
if (g_mg_fv_name[i] == name) { return g_mg_fv_node[i] }
i += 1
}
var found: Node = null
var many = false
i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_COMP and d.uns == 2 and field_index(d, name) >= 0 {
if found != null { many = true }
found = d
}
i += 1
}
var v: Node = null
if found != null and not many {
v = node(N_VAR)
v.s = name
v.ty = field_type(found, name)
v.file = found.file
v.line = found.line
v.pos = -1
push(g_mg_vars, v)
push(g_mg_vstate, mg_state_named(found.s))
push(g_mg_written, 0)
push(g_mg_let, 2)
v.mg = len(g_mg_vars) - 1
}
push(g_mg_fv_name, name)
push(g_mg_fv_node, v)
return v
}
function mg_collect() -> void {
var end = g_prog_user_end
if g_mg_runtime { end = len(prog) } # the runtime's declarations come after the program's
@ -283,15 +348,46 @@ function mg_decide_lets() -> void {
var i = 0
while i < len(g_mg_vars) {
let v = g_mg_vars[i]
if g_mg_written[i] == 0 and mg_value_ty(v.ty) and not mg_bound_var(v.s) { g_mg_let[i] = 1 }
if g_mg_let[i] == 0 and g_mg_written[i] == 0 and mg_value_ty(v.ty) and not mg_bound_var(v.s) and not mg_knob(v) { g_mg_let[i] = 1 }
i += 1
}
var r = 0
while r < len(g_mr_node) {
if g_mr_node[r].pos < 0 { g_mg_let[g_mr_var[r]] = 0 }
if g_mr_node[r].pos < 0 and g_mg_let[g_mr_var[r]] == 1 { g_mg_let[g_mr_var[r]] = 0 }
r += 1
}
}
# a package's var a program may set (render3d's r3d_dem_path): nothing in these programs writes it,
# but a game using the package may - it stays changing data. A private one of a package's module is
# the package's own business.
function mg_knob(v: Node) -> bool {
let f = mg_norm(v.file)
let pkg = not (pkg_of_file(v.file) == "") or s_has_prefix(f, "packages/") or has_sub(f, "/packages/") or has_sub(f, "ludic_modules/")
if not pkg { return false }
if module_of(v.file) == "" { return true }
return v.vis == 1
}
function s_has_prefix(s: pointer, p: pointer) -> bool { return len(s) >= len(p) and (s[0 .. len(p)] == p) }
# the runtime's function that only answers its var `name` (`function gl_width() -> int { return gl_w }`)
function mg_runtime_reader(name: pointer) -> pointer {
var i = 0
while i < len(prog) {
let f = prog[i]
if f.kind == N_FN and is_runtime_file(f.file) and f.a != null and len(f.a.kids) == 1 and f.a.kids[0].kind == S_RETURN {
var np = 0
var j = 0
while j < len(f.kids) {
if f.kids[j].kind == N_PARAM and not is_state_ty(f.kids[j].ty) { np += 1 }
j += 1
}
let x = f.a.kids[0].a
if np == 0 and x != null and x.kind == E_MEMBER and x.a != null and x.a.kind == E_ID and (x.s == name) { return f.s }
if np == 0 and x != null and x.kind == E_ID and (mg_src_name(x.s) == name) { return f.s }
}
i += 1
}
return null
}
function mg_value_ty(t: pointer) -> bool {
if t == null { return false }
if (t == "int") or (t == "float") or (t == "fixed") or (t == "long") or (t == "bool") or (t == "string") or (t == "byte") { return true }
@ -306,10 +402,14 @@ function mg_read_needs() -> void {
let u = g_mr_unit[r]
let g = g_mg_vars[v]
if is_runtime_file(g.file) and not is_runtime_file(e.file) {
# the runtime's state is the runtime's: from outside it is read through its functions
mg_say(e, `{mg_src_name(g.s)} is the runtime's; read it through the runtime's function for it`)
# the runtime's state is the runtime's: from outside it is read through its functions - the
# one that answers it, when there is one (gl_w: gl_width())
let rd = mg_runtime_reader(mg_src_name(g.s))
if rd != null and e.pos >= 0 and e.file != null {
mg_emit(`REF {e.file} {itoa(e.pos)} {itoa(len(mg_src_name(g.s)))} {mg_esc(`{rd}()`)}`, "")
} else { mg_say(e, `{mg_src_name(g.s)} is the runtime's; read it through the runtime's function for it`) }
g_mr_var[r] = -1 - v
} else if g_mg_let[v] == 0 and u != -3 {
} else if not (g_mg_let[v] == 1) and u != -3 {
if u >= 0 { mg_need(u, g_mg_vstate[v], false) }
else { mg_say(e, `{g.s} is read where no function is (a global's initializer); move it by hand`) }
if e.pos < 0 and not mg_bound_var(g.s) { mg_say(e, `a reference to {g.s} in generated code (a component, a view, a registry's lookup)`) }
@ -421,7 +521,7 @@ function mg_norm_line(line: pointer) -> pointer {
}
if len(w) < 2 { return line }
let k = w[0]
if (k == "VAR") or (k == "REF") or (k == "INS") or (k == "LET") { w[1] = mg_norm(w[1]) }
if (k == "VAR") or (k == "REF") or (k == "INS") or (k == "LET") or (k == "CLOSE") { w[1] = mg_norm(w[1]) }
if (k == "?") { w[1] = mg_norm(w[1]) }
if (k == "VAR") and len(w) > 5 { w[5] = mg_norm_key(w[5]) }
if (k == "STATE") and len(w) > 6 {
@ -571,6 +671,11 @@ function mg_units() -> void {
var u = 0
while u < len(g_mu_node) {
let n = g_mu_node[u]
if g_mu_kind[u] == 0 and n.cm >= 0 {
mg_comp_acc(u, n.cm) # a component's member: the component declares them
u += 1
continue
}
mg_mut_upgrades(u, n)
let need = mg_list(g_mu_need[u])
if len(need) > 0 {
@ -624,20 +729,26 @@ function mg_unit_edit(u: int, n: Node, text: pointer) -> void {
if kind == 5 { body = n } # a scene's `on enter` block is its own body
if body != null and len(body.kids) > 0 and body.kids[0].tps != null and (body.kids[0].tps == "state") {
# it declares some already: whatever it needs beyond them is for a person to add
# it declares some already: the rest go after them
let need = mg_list(g_mu_need[u])
var more = ""
var last: Node = null
var k = 0
while k < len(need) {
var have = false
var j = 0
while j < len(body.kids) and body.kids[j].tps != null and (body.kids[j].tps == "state") {
if (body.kids[j].ty == g_ms_name[need[k]]) { have = true }
last = body.kids[j]
j += 1
}
if not have { more = more + " " + g_ms_name[need[k]] }
if not have { more = more + ", " + mg_param_text(u, need[k]) }
k += 1
}
if len(more) > 0 { mg_say(n, `this entry point declares states already; add{more} to them by hand`) }
if len(more) > 0 {
if last == null or last.pos2 < 0 or last.file == null { mg_say(n, `this entry point declares states already; add{more} to them by hand`) }
else { mg_emit(`CLOSE {last.file} {itoa(last.pos2)} {mg_esc(more)}`, "") }
}
return
}
if kind == 1 { mg_emit(`INS {n.file} {itoa(n.pos)} {mg_esc(`({text}) `)}`, `{n.file}@{itoa(n.pos)}`) }
@ -653,6 +764,58 @@ function mg_unit_edit(u: int, n: Node, text: pointer) -> void {
mg_emit(`INS {b.file} {itoa(b.pos2)} {mg_esc(`({text})`)}`, `{b.file}@{itoa(b.pos2)}`)
}
}
var g_mcm_need: []pointer = new []pointer # per component: the states its members need
var g_mcm_mut: []pointer = new []pointer
function mg_comp_acc(u: int, c: int) -> void {
while len(g_mcm_need) < len(g_cm_name) {
push(g_mcm_need, ",")
push(g_mcm_mut, ",")
}
let need = mg_list(g_mu_need[u])
var k = 0
while k < len(need) {
let key = `,{itoa(need[k])},`
if not has_sub(g_mcm_need[c], key) { g_mcm_need[c] = g_mcm_need[c] + itoa(need[k]) + "," }
if has_sub(g_mu_mut[u], key) and not has_sub(g_mcm_mut[c], key) { g_mcm_mut[c] = g_mcm_mut[c] + itoa(need[k]) + "," }
k += 1
}
}
# each component's header: the states its members need that it does not declare yet, and `mut` on
# one it declares read-only that a member now changes
function mg_comps() -> void {
var c = 0
while c < len(g_mcm_need) {
let need = mg_list(g_mcm_need[c])
let ps = g_cm_ps[c]
var text = ""
var k = 0
while k < len(need) {
let s = need[k]
let m = has_sub(g_mcm_mut[c], `,{itoa(s)},`)
var have: Node = null
var j = 0
while j < len(ps) {
if (ps[j].ty == g_ms_name[s]) { have = ps[j] }
j += 1
}
if have == null {
if len(text) > 0 { text = text + ", " }
if m { text = text + `{g_ms_param[s]}: mut {g_ms_name[s]}` } else { text = text + `{g_ms_param[s]}: {g_ms_name[s]}` }
} else if m and have.uns == 0 and have.pos2 >= 0 {
mg_emit(`INS {g_cm_file[c]} {itoa(have.pos2)} {mg_esc("mut ")}`, "")
}
k += 1
}
if len(text) > 0 {
if len(ps) == 0 { mg_emit(`INS {g_cm_file[c]} {itoa(g_cm_hdr[c])} {mg_esc(` ({text})`)}`, "") } else {
# after the last one it declares: its type's end is not kept, so before the `)` that follows
let last = ps[len(ps) - 1]
mg_emit(`CLOSE {g_cm_file[c]} {itoa(last.pos2)} {mg_esc(`, {text}`)}`, "")
}
}
c += 1
}
}
function mg_views() -> void {
var v = 0
while v < len(g_mv_need) {
@ -680,7 +843,7 @@ function mg_calls() -> void {
let to = g_mc_callee[c]
let callee = g_mu_node[to]
let need = mg_list(g_mu_need[to])
if len(need) > 0 {
if len(need) > 0 and callee.cm < 0 { # a component's member is passed its states by the compiler
var text = ""
let passed = g_mc_passed[c] == 1
var k = 0
@ -711,7 +874,7 @@ function mg_refs() -> void {
continue
}
let v = g_mg_vars[g_mr_var[r]]
if e.pos >= 0 and e.file != null and g_mg_let[g_mr_var[r]] == 0 {
if e.pos >= 0 and e.file != null and not (g_mg_let[g_mr_var[r]] == 1) {
let s = g_mg_vstate[g_mr_var[r]]
let nm = mg_src_name(v.s)
var text = `{g_ms_param[s]}.{nm}`
@ -784,7 +947,7 @@ function mg_finish() -> void {
let v = g_mg_vars[i]
let vs = g_mg_vstate[i]
if g_mg_let[i] == 1 { mg_emit(`LET {v.file} {itoa(v.pos)}`, "") }
else { mg_emit(`VAR {v.file} {itoa(v.pos)} {itoa(v.pos2)} {g_ms_name[vs]} {anchor[vs]}`, "") }
else if g_mg_let[i] == 0 { mg_emit(`VAR {v.file} {itoa(v.pos)} {itoa(v.pos2)} {g_ms_name[vs]} {anchor[vs]}`, "") }
i += 1
}
s = 0
@ -808,6 +971,7 @@ function mg_finish() -> void {
mg_refs()
mg_binds()
mg_units()
mg_comps()
mg_views()
mg_calls()
let f = file_open(g_mg_plan, "wb")

View file

@ -1024,7 +1024,7 @@ function parse_one_decl() -> void {
if is_id("registry") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "of") { parse_registry(); return } # L8
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
if is_id("view") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_view(); return } # L11
if is_id("component") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_ui_component(); return } # L11
if is_id("component") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_ui_component(); return } # L11
if is_id("state") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_state(); return } # 0.S
if is_id("var") {
if not g_allow_globals {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ var mgx_len: []int = null
var mgx_text: []pointer = null
var mgx_texts: []pointer = null # the files' texts, read once
var mgx_names: []pointer = null
var mgx_roots: []pointer = null # where it may write: the programs and directories it was given
function mgx_unesc(s: pointer) -> pointer {
var out = ""
@ -99,32 +100,85 @@ function mgx_close(t: pointer, at: int) -> int {
}
return -1
}
# a directory as the plans spell a path (relative to here, `..` resolved), with a trailing '/'
function mgx_dir(d: pointer) -> pointer {
var p = mgx_norm(d)
if len(p) == 0 or (p == ".") { return "" }
if p[len(p) - 1] != '/' { p = p + "/" }
return p
}
function mgx_norm(p0: pointer) -> pointer {
var p = p0
let cwd = getenv_or("PWD", "")
if len(cwd) > 0 and s_starts(p, cwd + "/") { p = p[len(cwd) + 1 .. len(p)] }
let abs = len(p) > 0 and p[0] == '/'
let segs = new []pointer
var n = 0
var a = 0
for i in 0 .. len(p) + 1 {
if i == len(p) or p[i] == '/' {
let seg = p[a .. i]
if (seg == "..") and n > 0 and not (segs[n - 1] == "..") { n -= 1 }
else if not (seg == ".") and not (seg == "") {
if n < len(segs) { segs[n] = seg } else { push(segs, seg) }
n += 1
}
a = i + 1
}
}
var out = ""
if abs { out = "/" }
for i in 0 .. n {
if i > 0 { out = out + "/" }
out = out + segs[i]
}
return out
}
# may it write this file: it is under one of the programs' directories (a package the program only
# imports - installed, or a neighbour's - is not the migration's to change)
function mgx_mine(path: pointer) -> bool {
let p = mgx_norm(path)
for i in 0 .. len(mgx_roots) {
if mgx_roots[i] == "" and not (p[0] == '/') and not s_starts(p, "../") { return true }
if not (mgx_roots[i] == "") and s_starts(p, mgx_roots[i]) { return true }
}
return false
}
function cmd_migrate() -> int {
if arg_count() < 3 or not (arg(2) == "state") {
err("usage: ludic migrate state [file|dir...] [--runtime] [--unsafe] [--dry-run]\n")
return 2
}
let srcs = new []pointer
mgx_roots = new []pointer
var runtime = ""
var dry = false
var ai = 3
while ai < arg_count() {
let a = arg(ai)
if a == "--runtime" { runtime = " --migrate-runtime" }
if a == "--runtime" {
runtime = " --migrate-runtime"
push(mgx_roots, "runtime/")
}
else if a == "--unsafe" { g_unsafe_build = true }
else if a == "--dry-run" { dry = true }
else if a[0] == '-' { err(`ludic migrate: unknown option {a}\n`); return 2 }
else if shq(`test -d {sh_single(a)}`) {
let found = test_files_in(a)
for k in 0 .. len(found) { push(srcs, found[k]) }
push(mgx_roots, mgx_dir(a))
}
else {
push(srcs, a)
push(mgx_roots, mgx_dir(dir_of_path(a)))
}
else { push(srcs, a) }
ai += 1
}
if len(srcs) == 0 {
let entry = find_entry("")
if entry == "" { return no_entry() }
push(srcs, entry)
push(mgx_roots, mgx_dir(dir_of_path(entry)))
}
ensure_ludicc()
# every program's plan from the source as it is, then one pass over the files: programs that
@ -190,11 +244,21 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
let sexp = new []int
let smore = new []int # 1: the state is already declared, and gains fields
let reports = new []pointer
let foreign = new []pointer # files a plan would change that the migration may not
let lfile = new []pointer
let lpos = new []int
for i in 0 .. len(lines) {
let w = dp_words(lines[i])
if len(w) == 0 { continue }
var at = ""
if (w[0] == "VAR") or (w[0] == "REF") or (w[0] == "INS") or (w[0] == "CLOSE") or (w[0] == "LET") { at = w[1] }
if (w[0] == "STATE") and len(w) > 2 { at = w[2] }
if len(at) > 0 and mgx_roots != null and not mgx_mine(at) {
var seen = false
for k in 0 .. len(foreign) { if foreign[k] == at { seen = true } }
if not seen { push(foreign, at) }
continue
}
if w[0] == "VAR" and len(w) == 6 {
push(vfile, w[1])
push(vpos, s_to_int(w[2]))
@ -228,6 +292,11 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
mgx_edit(w[1], s_to_int(w[2]), s_to_int(w[3]), mgx_unesc(w[4]))
} else if (w[0] == "INS") and len(w) == 4 {
mgx_edit(w[1], s_to_int(w[2]), 0, mgx_unesc(w[3]))
} else if (w[0] == "CLOSE") and len(w) == 4 {
let t = mgx_text_of(w[1])
var k = s_to_int(w[2])
while t[k] != 0 and t[k] != ')' { k += 1 }
mgx_edit(w[1], k, 0, mgx_unesc(w[3]))
} else if w[0] == "LET" and len(w) == 3 {
push(lfile, w[1])
push(lpos, s_to_int(w[2]))
@ -249,6 +318,23 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
}
}
}
# a change the programs need in a file it may not write (a package's, a neighbour's): refused,
# whole - that code is migrated where it lives, or given here too
if len(foreign) > 0 {
err("ludic migrate: these files would have to change too, and are not under a program or directory given; nothing was changed:\n")
for f in 0 .. len(foreign) { err(` {foreign[f]}\n`) }
err("migrate them first (a package: in its own checkout), or name their directory as well\n")
return 1
}
# a var whose state would be declared in a file it may not write: refused, whole
for v in 0 .. len(vfile) {
var ok = false
for k in 0 .. len(sat) { if sat[k] == vstate[v] { ok = true } }
if not ok {
err(`ludic migrate: {vfile[v]} has a var for the state of {vstate[v]}, which is declared where this migration may not write; nothing was changed\n`)
return 1
}
}
# the vars: each line goes, and the first of each state's becomes the state
let del_file = new []pointer
let del_a = new []int

View file

@ -531,6 +531,51 @@ function migrate_case() -> void {
if ran == "2 names, first a" { ok(lbl) } else { bad2(lbl, `ran [{ran}]`) }
}
# a program built headless with the toolchain and run: its first line
function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
let out = `{tmp_dir()}/h_{flat(path)}`
if not shq(`bin/ludicc --headless examples/{path}.ludic -o {out} > {out}.log 2>&1`) { bad2(label, capture_line(`grep -i error {out}.log | head -1`)); return }
let got = capture_line(`{out} < /dev/null`)
if got == exp { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
}
# ludic migrate state and components: the header names what every member needs, a field read in a
# member is not edited, a module named like a package keeps a state of its own, and the program
# runs as it did
function migrate_component_case() -> void {
let lbl = "ludic migrate state: a component's header, a module named like a package, the same output"
let work = `{tmp_dir()}/migcomp`
shell(`rm -rf {work} && mkdir -p {work}/game/parts {work}/game/fishing && cp examples/state/component_parts/Tally.xml {work}/game/parts/`)
write_file(`{work}/game/parts/Tally.ludic`, "component Tally {\n prop label: string\n total: int = points\n shown: string = `{prefix}{total}`\n function big() -> bool { return total > 7 }\n on add(n: int) {\n points += n\n if big() { bigs += 1 }\n fish_cast()\n }\n}\n")
write_file(`{work}/game/fishing/index.ludic`, "module fishing\nvar casts: int = 0\nexport function fish_cast() -> void { casts += 1 }\nexport function fish_casts() -> int { return casts }\n")
var m = "import \"ludic.ui\"\nimport \"fishing\"\nprogram Mig {\n numbers float\n var points: int = 0\n var bigs: int = 0\n var prefix: string = \"#\"\n import \"parts/Tally.ludic\"\n"
m = m + " function frame(ui_st: mut UiState) -> UiNode {\n let root: UiNode = ui_nodes(ui_st, \"Tally\", null)\n ui_place(ui_st, root, 0.0, 0.0, 300.0, 200.0)\n return root\n }\n"
m = m + " entry (ui_st: mut UiState) {\n var root = frame(ui_st)\n ui_press(ui_st, root.children[0].children[1])\n root = frame(ui_st)\n ui_press(ui_st, root.children[0].children[1])\n root = frame(ui_st)\n"
m = m + " print(`{root.children[0].children[0].text} {points} {bigs} {fish_casts()}`)\n }\n}\n"
write_file(`{work}/game/main.ludic`, m)
let root = capture_line("pwd")
if not shq(`cd {work} && {root}/bin/ludic migrate state game/main.ludic > out.txt 2>&1`) { bad2(lbl, capture_line(`tail -1 {work}/out.txt`)); return }
let tally = read_file(`{work}/game/parts/Tally.ludic`)
if not s_contains(tally, "component Tally (fishing_app_st: mut FishingAppState, mig_st: mut MigState) {") or not s_contains(tally, "if big() { mig_st.bigs += 1 }") { bad2(lbl, `rewrote the component as [{tally}]`); return }
if not s_contains(read_file(`{work}/game/fishing/index.ludic`), "export state FishingAppState {") { bad2(lbl, "module fishing's state is not its own"); return }
if not shq(`cd {work} && {root}/bin/ludicc --headless game/main.ludic -o mig > cc.txt 2>&1`) { bad2(lbl, capture_line(`grep -i error {work}/cc.txt | head -1`)); return }
let ran = capture_line(`{work}/mig < /dev/null`)
if ran == " #10 big 10 1 2" { ok(lbl) } else { bad2(lbl, `ran [{ran}]`) }
}
# ludic migrate state never writes outside what it was given: a module a program imports from
# elsewhere that needs changing refuses the whole run, and nothing is written
function migrate_foreign_case() -> void {
let lbl = "ludic migrate state refuses to change a file outside the programs it was given"
let work = `{tmp_dir()}/migforeign`
shell(`rm -rf {work} && mkdir -p {work}/lib/extra {work}/game`)
write_file(`{work}/lib/extra/index.ludic`, "module extra\nvar n: int = 0\nexport function bump() -> void { n += 1 }\n")
let main = "import \"../lib/extra\"\nprogram F {\n entry { bump() }\n}\n"
write_file(`{work}/game/main.ludic`, main)
let root = capture_line("pwd")
if shq(`cd {work} && {root}/bin/ludic migrate state game/main.ludic > out.txt 2>&1`) { bad2(lbl, "it went ahead"); return }
if not shq(`grep -q 'lib/extra/index.ludic' {work}/out.txt`) { bad2(lbl, capture(`cat {work}/out.txt`)); return }
if not (read_file(`{work}/game/main.ludic`) == main) { bad2(lbl, "the program was changed anyway"); return }
ok(lbl)
}
# every package's own tests, the way a package author runs them: `ludic test packages`
function packages_test_case() -> void {
let lbl = "ludic test packages: every package's tests pass"
@ -910,6 +955,11 @@ function cmd_dev_test() -> int {
reject_case("rejected/mut_not_state", "mut is for a state parameter", "mut is for a state parameter")
reject_case("rejected/state_alias_readonly", "r holds part of a read-only Table; take the state as mut Table", "a reference out of a read-only state is read-only too")
reject_case("rejected/module_let_alias", "l holds part of LIMITS, which is module-level and immutable all the way down", "a module-level let is immutable through a local that holds it")
reject_case("rejected/state_component_ro", "look is read-only here (look: Look)", "a component's state is read-only unless its header says mut")
controller_case("state/component", "", " #10 big 10 1", "component.ludic (0.S: a component's header names its states; its getters, functions and events take them and the template never sees them)")
headless_case("rendering/ui_render3d", "ok", "ui_render3d.ludic (ludic.ui's render3d backend builds against the renderer)")
migrate_component_case()
migrate_foreign_case()
reject_case("rejected/runtime_type_clash", "PadButton is the runtime's enum", "a program's type named like one of the runtime's is refused")
migrate_case()
vis_report_case("rejected/uses_missing", "uses: examples/rejected/uses_valley/items/index.ludic:3: items.inv_add used from", "LUDIC_VIS_REPORT=1 lists a uses violation and builds")