diff --git a/LANGUAGE.md b/LANGUAGE.md index 6e3b432d..e4bb2ef6 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -213,6 +213,74 @@ fn()->int and this is a fn(int)->float`). A port nobody uses may stay unbound, a whose every member has a default: unbound, it answers with its defaults - which is how a package's fallbacks ("unbound: this machine runs the world") are written. +### State: no function writes a global (`state`, `mut`) + +A function that changes a module variable it was not given is a hidden coupling: nothing in its +signature says what it touches, and it cannot run without the whole program around it. So a +module-level `var` is refused, and a module's changing data is a **state** record instead: + +```ludic +# doc-check: skip — a fragment +state Hiker { + hips: int = -1 + spine: int = -1 +} + +function hiker_bind(h: mut Hiker, sk: Skin) -> void { + h.hips = skin_joint(sk, "hips") + h.spine = skin_joint(sk, "spine") +} +function hips_of(h: Hiker) -> int { return h.hips } +``` + +- **One instance, which no code names.** The program holds exactly one of each `state`, made + before any code runs. It reaches code only as a parameter: `h: mut Hiker` may change it, `h: Hiker` + may only read it. So a function's signature is everything it reads and writes, and a test hands + it a plain value. +- **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. + `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) { ... }`; + - 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. + 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. +- **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 + `ludicc --globals`, which lets a module-level `var` through. + +The errors: + +``` +counter.ludic:5: error: this assignment: c is read-only here (c: Tally); take it as c: mut Tally to change it +counter.ludic:3: error: a module-level var is refused: a module's changing data is its state (state Name { ... }), passed to the functions that use it - or, if it never changes, a let +counter.ludic:5: error: this assignment: LIMITS is module-level and immutable all the way down; changing data belongs in a state, passed as a mut parameter +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: + +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. + +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. + ### Types are checked before anything is emitted Between the parse and the emitter a checker walks every function, the entry, the tests, the diff --git a/examples/rejected/module_let_write.ludic b/examples/rejected/module_let_write.ludic new file mode 100644 index 00000000..e98324e2 --- /dev/null +++ b/examples/rejected/module_let_write.ludic @@ -0,0 +1,8 @@ +# 0.S: a module-level let is immutable all the way down - a write through it is refused +program ModuleLetWrite { + let LIMITS: []int = [1, 2] + entry { + LIMITS[0] = 3 + print(LIMITS[0]) + } +} diff --git a/examples/rejected/module_var.ludic b/examples/rejected/module_var.ludic new file mode 100644 index 00000000..1ac09ad2 --- /dev/null +++ b/examples/rejected/module_var.ludic @@ -0,0 +1,7 @@ +# 0.S: a module-level var is refused - a module's changing data is its state +program ModuleVar { + var count: int = 0 + entry { + print(count) + } +} diff --git a/examples/rejected/mut_not_state.ludic b/examples/rejected/mut_not_state.ludic new file mode 100644 index 00000000..00f1d42a --- /dev/null +++ b/examples/rejected/mut_not_state.ludic @@ -0,0 +1,7 @@ +# 0.S: `mut` is for a state parameter +program MutNotState { + function f(x: mut int) -> int { return x } + entry { + print(f(1)) + } +} diff --git a/examples/rejected/state_mut_arg.ludic b/examples/rejected/state_mut_arg.ludic new file mode 100644 index 00000000..19cd7a84 --- /dev/null +++ b/examples/rejected/state_mut_arg.ludic @@ -0,0 +1,9 @@ +# 0.S: a read-only state is not handed on where a mut one is wanted +program StateMutArg { + state Tally { n: int = 0 } + function bump(c: mut Tally) -> void { c.n += 1 } + function look(c: Tally) -> void { bump(c) } + entry (c: Tally) { + look(c) + } +} diff --git a/examples/rejected/state_readonly.ludic b/examples/rejected/state_readonly.ludic new file mode 100644 index 00000000..692bb5dc --- /dev/null +++ b/examples/rejected/state_readonly.ludic @@ -0,0 +1,11 @@ +# 0.S: a state taken without `mut` is read-only - assigning through it is refused +program StateReadonly { + state Tally { n: int = 0 } + function peek(c: Tally) -> int { + c.n = 1 + return c.n + } + entry (c: Tally) { + print(peek(c)) + } +} diff --git a/examples/state/counter.ludic b/examples/state/counter.ludic new file mode 100644 index 00000000..8a08e9c9 --- /dev/null +++ b/examples/state/counter.ludic @@ -0,0 +1,29 @@ +# counter.ludic — 0.S: no function writes a global. The changing data is a `state`; it reaches a +# function only as a parameter - `c: mut Tally` to change it, `c: Tally` to read it - and the +# runtime supplies it where the program is entered: `entry (c: mut Tally)`, an `@On` listener's +# `(c: mut Tally)`, and any function value (`fn tick` is tick with its Tally supplied). +# +# Running it prints: 11 3 heard 5 +program Counter { + state Tally { + n: int = 0 + heard: int = 0 + runs: []int = null + } + event Ping { amount: int } + @On(Ping) handler Heard(c: mut Tally) { c.heard += amount } + function bump(c: mut Tally, by: int) -> void { + c.n = c.n + by + if c.runs == null { c.runs = new []int } + push(c.runs, by) + } + function total(c: Tally) -> int { return c.n } + function tick(c: mut Tally, by: int) -> void { bump(c, by) } + function run_later(f: fn(int) -> void, by: int) -> void { f(by) } + entry (c: mut Tally) { + bump(c, 3) + run_later(fn tick, 8) + emit Ping(amount: 5) + print(`{total(c)} {len(c.runs) + 1} heard {c.heard}`) + } +} diff --git a/examples/state/port_field.ludic b/examples/state/port_field.ludic new file mode 100644 index 00000000..7800867b --- /dev/null +++ b/examples/state/port_field.ludic @@ -0,0 +1,12 @@ +# port_field.ludic — 0.S: a port member bound to a state's field, `money: Wallet.cash` - read +# through the Wallet the runtime supplies, so a port needs no wrapper and no global. +# +# Running it prints: 40 +program PortField { + state Wallet { cash: int = 40 } + port Purse { money: fn() -> int } + bind Purse { money: Wallet.cash } + entry { + print(Purse.money()) + } +} diff --git a/examples/state/tested.ludic b/examples/state/tested.ludic new file mode 100644 index 00000000..ad96a05e --- /dev/null +++ b/examples/state/tested.ludic @@ -0,0 +1,14 @@ +# tested.ludic — 0.S: a test declares the states it uses, `test "name" (c: mut Tally)`, and gets a +# fresh one (each test is a process of its own). Its runner prints: +# == 2 passed, 0 failed == +program Tested { + state Tally { n: int = 0 } + function bump(c: mut Tally) -> void { c.n += 1 } + test "a bump counts one" (c: mut Tally) { + bump(c) + expect_eq(c.n, 1) + } + test "reading needs no mut" (c: Tally) { + expect_eq(c.n, 0) + } +} diff --git a/selfhost/backend/emit_call.ludic b/selfhost/backend/emit_call.ludic index deabd01e..b60a717b 100644 --- a/selfhost/backend/emit_call.ludic +++ b/selfhost/backend/emit_call.ludic @@ -951,6 +951,7 @@ function emit_call(e: Node) -> Val { let rtname = `rt_{name}` fn2 = find_fn(rtname) if (fn2 == null) { perr(`unknown function {name}`) } + state_inject(e, fn2) # 0.S: a runtime built-in's states, supplied cname = rtname } vis_check(fn2, name) diff --git a/selfhost/backend/emit_decl.ludic b/selfhost/backend/emit_decl.ludic index 4e9c131b..cf144922 100644 --- a/selfhost/backend/emit_decl.ludic +++ b/selfhost/backend/emit_decl.ludic @@ -166,6 +166,8 @@ function emit_test_runner() -> void { let rn1 = emit_bind(`add i32 {rn}, 1`) emit(` store i32 {rn1}, ptr %ran\n`) emit(" store i32 0, ptr @L_test_fail\n") + # 0.S: every state fresh for each test - the states are what changes, and they are made here + if i > 0 { emit(" call void @L_init_globals()\n") } emit(` call void @fn__test_{itoa(i)}()\n`) let f = emit_bind("load i32, ptr @L_test_fail") let isbad = emit_bind(`icmp ne i32 {f}, 0`) diff --git a/selfhost/backend/emit_fnval.ludic b/selfhost/backend/emit_fnval.ludic index 3d11f57e..fe549909 100644 --- a/selfhost/backend/emit_fnval.ludic +++ b/selfhost/backend/emit_fnval.ludic @@ -7,11 +7,14 @@ function is_fn_type(t: pointer) -> bool { return len(t) > 3 and t[0] == 'f' and t[1] == 'n' and t[2] == '(' } # the function type of a declared function, in ptype's own spelling +# 0.S: a function value is the function with its leading states supplied (state.ludic), so its +# type leaves them out function fn_sig_of(d: Node) -> pointer { var out = "fn(" - var i = 0 + var i = state_lead(d) + let first = i while i < len(d.kids) { - if i > 0 { out = out + "," } + if i > first { out = out + "," } out = out + d.kids[i].ty i += 1 } @@ -63,8 +66,53 @@ function emit_fnref(e: Node) -> Val { let d = find_fn(e.s) if d == null { perr(`fn {e.s}: no function called {e.s}`) } vis_check(d, e.s) + if state_lead(d) > 0 { return val(emit_inj_thunk(d), fn_sig_of(d)) } return val(`@fn_{e.s}`, fn_sig_of(d)) } +# how many of a function's parameters, from the first, are states +function state_lead(d: Node) -> int { + var n = 0 + while n < len(d.kids) and d.kids[n].kind == N_PARAM and is_state_ty(d.kids[n].ty) { n += 1 } + return n +} +# @fn_$inj: the function with its leading states loaded from their instances - what a +# function value of it calls, so whoever calls the value (a system runner, a port, a package) +# supplies none of them. Written once per function, beside the rest. +var g_inj_done: []pointer = new []pointer +function emit_inj_thunk(d: Node) -> pointer { + let name = `@fn_{d.s}$inj` + var i = 0 + while i < len(g_inj_done) { + if (g_inj_done[i] == d.s) { return name } + i += 1 + } + push(g_inj_done, d.s) + let k = state_lead(d) + var rt = "void" + if d.ty != null { rt = llty(d.ty) } + var params = "" + var args = "" + var body = "" + i = 0 + while i < len(d.kids) { + let p = d.kids[i] + if i < k { + body = body + ` %s{itoa(i)} = load ptr, ptr @g_state${p.ty}\n` + if len(args) > 0 { args = args + ", " } + args = args + `ptr %s{itoa(i)}` + } else { + if len(params) > 0 { params = params + ", " } + params = params + `{llty(p.ty)} %p{itoa(i)}` + if len(args) > 0 { args = args + ", " } + args = args + `{llty(p.ty)} %p{itoa(i)}` + } + i += 1 + } + var call = ` call {rt} @fn_{d.s}({args})\n ret void\n` + if not (rt == "void") { call = ` %r = call {rt} @fn_{d.s}({args})\n ret {rt} %r\n` } + emith(`define {rt} {name}({params}) {{\nentry:\n{body}{call}}}\n\n`) + return name +} # a call through a value of a function type function emit_indirect_call(fv: Val, e: Node) -> Val { let ptys = fn_ty_params(fv.ty) diff --git a/selfhost/check/check_call.ludic b/selfhost/check/check_call.ludic index 8a44bcdf..bbfced84 100644 --- a/selfhost/check/check_call.ludic +++ b/selfhost/check/check_call.ludic @@ -13,7 +13,9 @@ function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void { } } function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer { + mg_call(e, f) call_fill_defaults(e, f) + ck_state_args(e, f) let labels = new []pointer let tys = new []pointer ck_params(f, labels, tys) @@ -22,6 +24,7 @@ function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer { return f.ty } function ck_call_alias(e: Node, name: pointer, al: int, f: Node) -> pointer { + state_inject(e, f) if g_al_labels[al] == null { call_fill_defaults(e, f) } else { call_mixed_to_named(e, ns_alias_labels(al)) } let labels = new []pointer let tys = new []pointer @@ -141,6 +144,7 @@ function ck_builtin(e: Node, name: pointer) -> pointer { return "void" } if (name == "push") and len(e.kids) == 2 { + ck_write_check(e.kids[0], "push") let st = ck_expr(e.kids[0]) let vt = ck_expr(e.kids[1]) if not ck_unknown(st) and is_slice_ty(st) { ck_give(slice_elem(st), vt, e.kids[1], `push onto {ck_a(st)}`) } diff --git a/selfhost/check/check_env.ludic b/selfhost/check/check_env.ludic index a4da1c48..e9b5506a 100644 --- a/selfhost/check/check_env.ludic +++ b/selfhost/check/check_env.ludic @@ -18,6 +18,7 @@ function ck_bind(name: pointer, ty: pointer) -> void { push(ck_tys, t) } ck_top += 1 + ck_set_ro(false) } function ck_mark() -> int { return ck_top } function ck_pop(m: int) -> void { ck_top = m } diff --git a/selfhost/check/check_expr.ludic b/selfhost/check/check_expr.ludic index c7574b9a..3ca4de3d 100644 --- a/selfhost/check/check_expr.ludic +++ b/selfhost/check/check_expr.ludic @@ -70,6 +70,7 @@ function ck_id(e: Node) -> pointer { if li >= 0 { return ck_tys[li] } let g = ck_global(e.s) if g != null { + mg_ref(e, g) ck_vis(g, e.s, e) return g.ty } diff --git a/selfhost/check/check_gen.ludic b/selfhost/check/check_gen.ludic index 5f5288cc..648b6926 100644 --- a/selfhost/check/check_gen.ludic +++ b/selfhost/check/check_gen.ludic @@ -88,6 +88,9 @@ function gen_clone(n: Node, tps: []pointer, args: []pointer) -> Node { c.line = n.line c.file = n.file c.vis = n.vis + c.uns = n.uns + c.pos = n.pos + c.pos2 = n.pos2 c.a = gen_clone(n.a, tps, args) c.b = gen_clone(n.b, tps, args) c.c = gen_clone(n.c, tps, args) diff --git a/selfhost/check/check_gen_call.ludic b/selfhost/check/check_gen_call.ludic index 08aa67be..44a29c2a 100644 --- a/selfhost/check/check_gen_call.ludic +++ b/selfhost/check/check_gen_call.ludic @@ -17,6 +17,7 @@ function gen_fn_instance(t: Node, iname: pointer, binds: []pointer) -> Node { if have != null { return have } let f = gen_clone(t, gen_split(t.tps), binds) f.s = iname + if g_migrate { f.mg = mg_unit(t, 0) } # an instance's needs are its generic's push(g_gen_out, f) push(g_gen_work, f) ck_tab_put(ck_fn_k, ck_fn_v, iname, f) @@ -44,6 +45,7 @@ function gen_arg_nodes(e: Node, labels: []pointer) -> []Node { return out } function gen_call(e: Node, name: pointer, t: Node) -> pointer { + mg_call(e, t) let expect = ck_call_expect call_fill_defaults(e, t) let tps = gen_split(t.tps) diff --git a/selfhost/check/check_state.ludic b/selfhost/check/check_state.ludic new file mode 100644 index 00000000..ba381db1 --- /dev/null +++ b/selfhost/check/check_state.ludic @@ -0,0 +1,79 @@ +# check_state.ludic — 0.S: what a state parameter allows. `h: mut Hiker` may change the state; +# `h: Hiker` may only read it - an assignment whose target starts at it (`h.hips = 1`, +# `h.list[i] = x`), a push onto something in it, or passing it where a `mut Hiker` is wanted is +# refused. A module-level `let` (and a const, a registry, a state's own instance) is immutable all +# the way down: the same writes starting at one are refused. What this does not follow: a reference +# read out of a read-only state into a local (`let l = h.list`) and changed there. +var ck_ro: []int = new []int # parallel to ck_names: 1 for a read-only state + +function ck_set_ro(ro: bool) -> void { + let k = ck_top - 1 + while len(ck_ro) < len(ck_names) { push(ck_ro, 0) } + if ro { ck_ro[k] = 1 } else { ck_ro[k] = 0 } +} +function ck_is_ro(li: int) -> bool { + if li < 0 or li >= len(ck_ro) { return false } + return ck_ro[li] == 1 +} +# the name an assignment's target starts at: `a` in `a.b[c].d` +function ck_chain_root(t: Node) -> Node { + var n = t + while n != null and (n.kind == E_INDEX or n.kind == E_MEMBER) { n = n.a } + if n == null or n.kind != E_ID { return null } + return n +} +function ck_module_let(g: Node) -> bool { + if g == null { return false } + if g.kind == N_CONST { return true } + if g.kind != N_VAR { return false } + if g.uns == 1 { return true } + return reg_find(g.s) >= 0 +} +# a write into `t`: refused when it starts at a read-only state or at a module-level binding +function ck_write_check(t: Node, what: pointer) -> void { + let r = ck_chain_root(t) + if r == null { return } + let li = ck_local(r.s) + if li >= 0 { + if ck_is_ro(li) { + ck_err("state", t, `{what}: {r.s} is read-only here ({r.s}: {ck_tys[li]}); take it as {r.s}: mut {ck_tys[li]} to change it`) + } + return + } + let g = ck_global(r.s) + mg_write(t, g) + if ck_module_let(g) { + var name = r.s + if is_state_ty(g.ty) and g.uns == 1 { name = `the state {g.ty}` } + ck_err("state", t, `{what}: {name} is module-level and immutable all the way down; changing data belongs in a state, passed as a mut parameter`) + } +} +# a function's parameters: `mut` is for a state, and a state without it is read-only +function ck_bind_param(p: Node) -> void { + ck_bind(p.s, p.ty) + if p.uns == 1 and not is_state_ty(p.ty) { + ck_err("state", p, `{p.s}: mut {p.ty} - mut is for a state parameter, and {p.ty} is not a state`) + } + ck_set_ro(is_state_ty(p.ty) and p.uns == 0) +} +# a call's arguments against the function's state parameters: a read-only state is not a mut one +function ck_state_args(e: Node, f: Node) -> void { + var k = 0 + var i = 0 + while i < len(f.kids) { + let p = f.kids[i] + if p.kind == N_PARAM { + if p.uns == 1 and is_state_ty(p.ty) and k < len(e.kids) { + let a = e.kids[k] + if a.kind == E_ID { + let li = ck_local(a.s) + if li >= 0 and ck_is_ro(li) { + ck_err("state", a, `{f.s} changes {p.ty} ({p.s}: mut {p.ty}), and {a.s} is read-only here`) + } + } + } + k += 1 + } + i += 1 + } +} diff --git a/selfhost/check/check_stmt.ludic b/selfhost/check/check_stmt.ludic index 25f751b2..33a55f3c 100644 --- a/selfhost/check/check_stmt.ludic +++ b/selfhost/check/check_stmt.ludic @@ -28,11 +28,21 @@ function ck_let(s: Node) -> void { if s.ty != null { if s.a != null { ck_give(s.ty, t, s.a, s.s) } ck_bind(s.s, s.ty) + if s.tps != null and (s.tps == "state") { # an entry point's state (state.ludic) + if not is_state_ty(s.ty) { ck_err("state", s, `an entry point takes states, and {s.s}: {s.ty} is not one`) } + ck_set_ro(s.uns == 0) + } return } ck_bind(s.s, t) + # a local bound to a read-only state is read-only too + if s.a != null and s.a.kind == E_ID { + let li = ck_local(s.a.s) + if li >= 0 and li < ck_top - 1 and ck_is_ro(li) { ck_set_ro(true) } + } } function ck_assign(s: Node) -> void { + ck_write_check(s.a, "this assignment") let lt = ck_target(s.a) if (s.s == "=") { ck_expect = lt } let rt = ck_expr(s.b) @@ -116,10 +126,11 @@ function ck_stmt(s: Node) -> void { } } function ck_fn_body(d: Node) -> void { + mg_enter(d, 0) let m = ck_mark() var i = 0 while i < len(d.kids) { - if d.kids[i].kind == N_PARAM { ck_bind(d.kids[i].s, d.kids[i].ty) } + if d.kids[i].kind == N_PARAM { ck_bind_param(d.kids[i]) } i += 1 } ck_ret = d.ty @@ -132,8 +143,10 @@ function ck_fn_body(d: Node) -> void { if d.uns == 1 { ck_unsafe -= 1 } ck_ret = "void" ck_pop(m) + mg_leave() } function ck_listener(l: Node) -> void { + mg_enter(l, 4) let ev = find_event(l.s) ck_vis(ev, l.s, l) let m = ck_mark() @@ -147,6 +160,7 @@ function ck_listener(l: Node) -> void { ck_ret = "void" ck_block(l.a) ck_pop(m) + mg_leave() } # a program's function named like one of the runtime's takes every call the runtime makes to its # own - the runtime's ui_measure started answering to a package's - so the name is refused; one @@ -177,7 +191,16 @@ function check_program() -> void { while i < len(prog) { let d = prog[i] if d.kind == N_FN { ck_fn_body(d) } - if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) } + if d.kind == N_MAIN or d.kind == N_TEST { + mg_enter(d, 1) + ck_block(d.a) + mg_leave() + } + if d.kind == N_SYS and g_migrate { # a handler's body: only the migration reads it here + mg_enter(d, 2) + ck_block(d.a) + mg_leave() + } if (d.kind == N_VAR or d.kind == N_CONST) and d.a != null { ck_vis_file = d.file ck_vis_skip = d.kind == N_VAR and is_port_var(d) @@ -195,11 +218,13 @@ function check_program() -> void { # a test block is checked like `entry`: that is also what makes its generic calls real j = 0 while j < len(g_tests) { + mg_enter(g_tests[j], 3) ck_block(g_tests[j].a) + mg_leave() j += 1 } gen_finish() - if ck_errors > 0 and not ck_reporting() { + if ck_errors > 0 and not ck_reporting() and not g_migrate { let m = `{itoa(ck_errors)} type error(s)\n` file_write(file_stderr(), m, len(m)) exit(1) diff --git a/selfhost/frontend/aliases.ludic b/selfhost/frontend/aliases.ludic index 9744ae1c..859bd683 100644 --- a/selfhost/frontend/aliases.ludic +++ b/selfhost/frontend/aliases.ludic @@ -66,6 +66,7 @@ function ns_alias_labels(i: int) -> []pointer { return out } function emit_alias_call(i: int, e: Node) -> Val { + state_inject(e, find_fn(g_al_target[i])) # 0.S: the target's states, supplied call_mixed_to_named(e, ns_alias_labels(i)) reorder_named(e, ns_alias_labels(i)) let id = node(E_ID) diff --git a/selfhost/frontend/ast.ludic b/selfhost/frontend/ast.ludic index 5a647e5a..426dd9b1 100644 --- a/selfhost/frontend/ast.ludic +++ b/selfhost/frontend/ast.ludic @@ -89,6 +89,9 @@ property Node { vis: int = 0 # L3: 1 when the declaration is `export`ed from its module tps: pointer = null # L5: a generic declaration's type parameters, "T|U"; null when not generic uns: int = 0 # L7: 1 on an `unsafe function` + pos: int = -1 # 0.S: byte offset of its first token in its file (-1: generated) + 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) } # every node remembers where it was parsed (file + the line of the token the @@ -98,6 +101,9 @@ function node(kind: int) -> Node { n.kind = kind n.kids = new []Node n.file = g_parse_file - if g_parsing and pi < len(toks) { n.line = toks[pi].line } + if g_parsing and pi < len(toks) { + n.line = toks[pi].line + n.pos = toks[pi].pos + } return n } diff --git a/selfhost/frontend/lex.ludic b/selfhost/frontend/lex.ludic index c360aa40..9137cb63 100644 --- a/selfhost/frontend/lex.ludic +++ b/selfhost/frontend/lex.ludic @@ -11,13 +11,19 @@ const TK_EOF: int = 5 const TK_FLOAT: int = 6 const TK_INTERP: int = 7 # `text {expr} text` — raw content, split by the parser -property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0 } +property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0, pos: int = -1, end: int = -1 } + +# where the tokens are in their file (0.S's migration rewrites source by position): a file's own +# text starts at 0, an interpolation hole at its place in the file, and generated code has none (-1) +var g_lex_base: int = -1 +var lx_start: int = 0 var toks: []Tok function tok_push(kind: int, text: pointer, ival: int, line: int) -> void { let t = new Tok t.kind = kind; t.text = text; t.ival = ival; t.line = line + if g_lex_base >= 0 { t.pos = g_lex_base + lx_start } push(toks, t) } @@ -102,7 +108,12 @@ function lex_fail(line: int, msg: pointer) -> void { exit(1) } -function lex(src: pointer) -> void { lex_at(src, 1) } +function lex(src: pointer) -> void { + let saved = g_lex_base + g_lex_base = 0 + lex_at(src, 1) + g_lex_base = saved +} # lex `src` with its first line numbered `first_line` (an interpolation hole is # re-lexed on its own, and keeps the line of the string it sits in) @@ -111,7 +122,14 @@ function lex_at(src: pointer, first_line: int) -> void { var i = 0 var line = first_line let n = len(src) + var lx_n = 0 while i < n { + # the token the last turn pushed ends where this one starts looking + if len(toks) > lx_n { + if g_lex_base >= 0 { toks[len(toks) - 1].end = g_lex_base + i } + lx_n = len(toks) + } + lx_start = i let c = src[i] if c == '\n' { tok_push(TK_NL, null, 0, line); line += 1; i += 1; continue } if c == ' ' or c == '\t' or c == '\r' { i += 1; continue } @@ -236,5 +254,7 @@ function lex_at(src: pointer, first_line: int) -> void { if is_op1(c) { tok_push(TK_OP, src[i..i + 1], 0, line); i += 1; continue } lex_error(line, c) } + if len(toks) > lx_n and g_lex_base >= 0 { toks[len(toks) - 1].end = g_lex_base + i } + lx_start = i tok_push(TK_EOF, null, 0, line) } diff --git a/selfhost/frontend/migrate.ludic b/selfhost/frontend/migrate.ludic new file mode 100644 index 00000000..9325c2af --- /dev/null +++ b/selfhost/frontend/migrate.ludic @@ -0,0 +1,406 @@ +# migrate.ludic — 0.S2: `ludicc --migrate-state ` works out how a program's module-level +# vars become states and writes the edits as a plan (`ludic migrate state` applies it): +# 1. each module's vars are one state, `state State { ... }`, where the first of them was; +# 2. every reference to one - read or written - is `_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 a callee writes); +# 4. each call passes them on, and each entry point (entry, a handler, a listener, a test) +# declares them for the runtime to supply. +# It runs inside the checker (check_*.ludic call the mg_* hooks), which already knows which name is a +# local and which a global, and which function a call reaches. Plan lines, one edit each: +# VAR a declaration to move into +# STATE where is declared (the first var) +# REF a reference's name replaced +# INS text inserted (a parameter, an argument) +# ? : 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 +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_ms_name: []pointer = new []pointer +var g_ms_mod: []pointer = new []pointer +var g_ms_param: []pointer = new []pointer +var g_ms_export: []int = new []int +# units: the functions and entry points whose states are worked out +var g_mu_node: []Node = new []Node +var g_mu_kind: []int = new []int # 0 function, 1 entry block, 2 handler, 3 test, 4 listener +var g_mu_need: []pointer = new []pointer # ",0,3," state indices it needs +var g_mu_mut: []pointer = new []pointer # the ones it changes +var g_mg_cur: int = -1 +var g_mc_call: []Node = new []Node # every call to a function, and where it is +var g_mc_unit: []int = new []int +var g_mc_callee: []int = new []int +var g_mr_node: []Node = new []Node # every reference to a moving var +var g_mr_var: []int = new []int +var g_mg_report: []pointer = new []pointer + +function mg_camel(s: pointer) -> pointer { + var out = "" + var up = true + var i = 0 + while i < len(s) { + let c = s[i] + if c == '_' or c == '.' or c == '-' { up = true } else { + var ch = s[i .. i + 1] + if up and c >= 'a' and c <= 'z' { ch = reg_upper(ch) } + out = out + ch + up = false + } + i += 1 + } + return out +} +function mg_short(m: pointer) -> pointer { + if len(m) > 6 and (m[0 .. 6] == "ludic_") { return m[6 .. len(m)] } + return m +} +function mg_state_of_mod(m: pointer) -> int { + var i = 0 + while i < len(g_ms_mod) { + if (g_ms_mod[i] == m) { return i } + i += 1 + } + var base = mg_short(m) + if (m == "") { base = reg_lower(g_game_name) } + push(g_ms_mod, m) + push(g_ms_name, `{mg_camel(base)}State`) + push(g_ms_param, `{base}_st`) + push(g_ms_export, 0) + return len(g_ms_mod) - 1 +} +function mg_is_target(v: Node) -> bool { + if v.kind != N_VAR or v.uns != 0 or v.file == null or v.pos < 0 { return false } + if reg_find(v.s) >= 0 or is_port_var(v) { return false } + if not g_mg_runtime and has_sub(v.file, "runtime/") { return false } + return true +} +# which vars move, and the state each moves into +function mg_collect() -> void { + var i = 0 + while i < g_prog_user_end and i < len(prog) { + let d = prog[i] + if d.kind == N_VAR and mg_is_target(d) { + let k = mg_state_of_mod(module_for_uses(d.file)) + push(g_mg_vars, d) + push(g_mg_vstate, k) + d.mg = len(g_mg_vars) - 1 + if d.vis == 1 { g_ms_export[k] = 1 } + } + i += 1 + } +} +function mg_unit(n: Node, kind: int) -> int { + if n.mg >= 0 and n.kind != N_VAR { return n.mg } + push(g_mu_node, n) + push(g_mu_kind, kind) + push(g_mu_need, ",") + push(g_mu_mut, ",") + n.mg = len(g_mu_node) - 1 + # an already-migrated function: its declared states are needs + if kind == 0 { + var i = 0 + while i < len(n.kids) and n.kids[i].kind == N_PARAM and is_state_ty(n.kids[i].ty) { + let s = mg_state_named(n.kids[i].ty) + if s >= 0 { + mg_need(n.mg, s, n.kids[i].uns == 1) + } + i += 1 + } + } + return n.mg +} +function mg_state_named(name: pointer) -> int { + var i = 0 + while i < len(g_ms_name) { + if (g_ms_name[i] == name) { return i } + i += 1 + } + # a state declared by hand: known by its name, its param the name in snake case + push(g_ms_mod, `={name}`) + push(g_ms_name, name) + push(g_ms_param, `{reg_lower(name)}`) + push(g_ms_export, 0) + return len(g_ms_mod) - 1 +} +function mg_need(u: int, s: int, m: bool) -> bool { + var changed = false + let key = `,{itoa(s)},` + if not has_sub(g_mu_need[u], key) { + g_mu_need[u] = g_mu_need[u] + itoa(s) + "," + changed = true + } + if m and not has_sub(g_mu_mut[u], key) { + g_mu_mut[u] = g_mu_mut[u] + itoa(s) + "," + changed = true + } + return changed +} +# ---- the checker's hooks -------------------------------------------------------- +function mg_enter(n: Node, kind: int) -> void { + if not g_migrate { return } + g_mg_cur = mg_unit(n, kind) +} +function mg_leave() -> void { g_mg_cur = -1 } +function mg_ref(e: Node, g: Node) -> void { + if not g_migrate or g == null or g.kind != N_VAR or g.mg < 0 { return } + push(g_mr_node, e) + push(g_mr_var, g.mg) + if g_mg_cur >= 0 { mg_need(g_mg_cur, g_mg_vstate[g.mg], 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 { mg_say(e, `a reference to {g.s} in generated code (a component, a view, a registry's lookup)`) } +} +function mg_write(t: Node, g: Node) -> void { + if not g_migrate or g == null or g.kind != N_VAR or g.mg < 0 { return } + if g_mg_cur >= 0 { mg_need(g_mg_cur, g_mg_vstate[g.mg], true) } +} +function mg_call(e: Node, f: Node) -> void { + if not g_migrate or f == null or g_mg_cur < 0 { return } + push(g_mc_call, e) + push(g_mc_unit, g_mg_cur) + push(g_mc_callee, mg_unit(f, 0)) +} +function mg_say(n: Node, what: pointer) -> void { + var at = "?" + if n != null and n.file != null { at = `{n.file}:{itoa(n.line)}` } + push(g_mg_report, `? {at} {what}`) +} +# ---- the fixed point, and the plan --------------------------------------------------- +function mg_fixpoint() -> void { + var changed = true + while changed { + changed = false + var c = 0 + while c < len(g_mc_call) { + let from = g_mc_unit[c] + let to = g_mc_callee[c] + let need = mg_list(g_mu_need[to]) + var k = 0 + while k < len(need) { + let s = need[k] + if mg_need(from, s, has_sub(g_mu_mut[to], `,{itoa(s)},`)) { changed = true } + k += 1 + } + c += 1 + } + } +} +function mg_list(set: pointer) -> []int { + let out = new []int + var v = 0 + var any = false + var i = 1 + while i < len(set) { + let ch = set[i] + if ch == ',' { + if any { push(out, v) } + v = 0 + any = false + } else { + v = v * 10 + (ch - 48) + any = true + } + i += 1 + } + # in the order of the states' names, so every signature and every call agree + var a = 1 + while a < len(out) { + let x = out[a] + var b = a - 1 + while b >= 0 and reg_str_less(g_ms_name[x], g_ms_name[out[b]]) { + out[b + 1] = out[b] + b -= 1 + } + out[b + 1] = x + a += 1 + } + return out +} +# a function's states that it declares already (a migration run before this one) +function mg_declared(n: Node, s: int) -> bool { + if n.kind != N_FN { return false } + var i = 0 + while i < len(n.kids) and n.kids[i].kind == N_PARAM and is_state_ty(n.kids[i].ty) { + if (n.kids[i].ty == g_ms_name[s]) { return true } + i += 1 + } + return false +} +function mg_param_text(u: int, s: int) -> pointer { + if has_sub(g_mu_mut[u], `,{itoa(s)},`) { return `{g_ms_param[s]}: mut {g_ms_name[s]}` } + return `{g_ms_param[s]}: {g_ms_name[s]}` +} +var g_mg_lines: []pointer = new []pointer +var g_mg_keys: pointer = "," +function mg_emit(line: pointer, key: pointer) -> void { + if not (key == "") { + if has_sub(g_mg_keys, `,{key},`) { return } + g_mg_keys = g_mg_keys + key + "," + } + push(g_mg_lines, line) +} +# text for a plan line: spaces and newlines escaped +function mg_esc(s: pointer) -> pointer { + var out = "" + var i = 0 + while i < len(s) { + let c = s[i] + if c == ' ' { out = out + "\\s" } else if c == '\n' { out = out + "\\n" } else if c == '\\' { out = out + "\\\\" } else { out = out + s[i .. i + 1] } + i += 1 + } + return out +} +function mg_first_positional_is_state(e: Node) -> bool { + if len(e.kids) == 0 or e.kids[0].kind != E_ID { return false } + var i = 0 + while i < len(g_ms_param) { + if (g_ms_param[i] == e.kids[0].s) { return true } + i += 1 + } + return false +} +function mg_nparams(n: Node) -> int { + var k = 0 + var i = 0 + while i < len(n.kids) { + if n.kids[i].kind == N_PARAM { k += 1 } + i += 1 + } + return k +} +function mg_units() -> void { + var u = 0 + while u < len(g_mu_node) { + let n = g_mu_node[u] + let need = mg_list(g_mu_need[u]) + if len(need) > 0 { + var text = "" + var k = 0 + while k < len(need) { + if not mg_declared(n, need[k]) { + if len(text) > 0 { text = text + ", " } + text = text + mg_param_text(u, need[k]) + } + k += 1 + } + if len(text) > 0 { mg_unit_edit(u, n, text) } + } + u += 1 + } +} +function mg_unit_edit(u: int, n: Node, text: pointer) -> void { + let kind = g_mu_kind[u] + if kind == 0 { + if n.pos2 < 0 or n.file == null { + mg_say(n, `{n.s} needs {text} and is generated code: its caller cannot be rewritten`) + return + } + var t = text + if mg_nparams(n) > 0 { t = t + ", " } + mg_emit(`INS {n.file} {itoa(n.pos2 + 1)} {mg_esc(t)}`, `{n.file}@{itoa(n.pos2)}`) + return + } + # an entry point: a list of the states the runtime supplies + var body = n.a + if kind == 4 { body = n.a } + if body != null and len(body.kids) > 0 and body.kids[0].tps != null and (body.kids[0].tps == "state") { + mg_say(n, `this entry point declares states already; add ({text}) to them by hand`) + return + } + if kind == 1 { mg_emit(`INS {n.file} {itoa(n.pos)} {mg_esc(`({text}) `)}`, `{n.file}@{itoa(n.pos)}`) } + if kind == 2 { mg_emit(`INS {n.file} {itoa(n.pos2)} {mg_esc(`({text})`)}`, `{n.file}@{itoa(n.pos2)}`) } + if kind == 3 { mg_emit(`INS {n.file} {itoa(n.pos2)} {mg_esc(` ({text})`)}`, `{n.file}@{itoa(n.pos2)}`) } + if kind == 4 { + let b = n.a + if b == null or b.pos2 < 0 { + mg_say(n, `a listener that needs ({text}) whose place is not known`) + return + } + mg_emit(`INS {b.file} {itoa(b.pos2)} {mg_esc(`({text})`)}`, `{b.file}@{itoa(b.pos2)}`) + } +} +function mg_calls() -> void { + var c = 0 + while c < len(g_mc_call) { + let e = g_mc_call[c] + let to = g_mc_callee[c] + let callee = g_mu_node[to] + let need = mg_list(g_mu_need[to]) + if len(need) > 0 { + var text = "" + let passed = mg_first_positional_is_state(e) + var k = 0 + while k < len(need) { + if not (mg_declared(callee, need[k]) and passed) { + if len(text) > 0 { text = text + ", " } + text = text + g_ms_param[need[k]] + } + k += 1 + } + if len(text) > 0 { + if e.pos < 0 or e.file == null { mg_say(e, `a call to {callee.s} in generated code needs ({text})`) } else { + var t = text + if len(e.kids) > 0 { t = t + ", " } + mg_emit(`INS {e.file} {itoa(e.pos + 1)} {mg_esc(t)}`, `{e.file}@{itoa(e.pos)}`) + } + } + } + c += 1 + } +} +function mg_refs() -> void { + var r = 0 + while r < len(g_mr_node) { + let e = g_mr_node[r] + let v = g_mg_vars[g_mr_var[r]] + if e.pos >= 0 and e.file != null { + let s = g_mg_vstate[g_mr_var[r]] + mg_emit(`REF {e.file} {itoa(e.pos)} {itoa(len(v.s))} {mg_esc(`{g_ms_param[s]}.{v.s}`)}`, `{e.file}@{itoa(e.pos)}`) + } + r += 1 + } +} +function mg_finish() -> void { + mg_fixpoint() + var i = 0 + while i < len(g_mg_vars) { + let v = g_mg_vars[i] + let s = g_mg_vstate[i] + mg_emit(`VAR {v.file} {itoa(v.pos)} {itoa(v.pos2)} {g_ms_name[s]}`, "") + i += 1 + } + var s = 0 + while s < len(g_ms_name) { + # the first var of each new state is where the state is declared + var first = -1 + var j = 0 + while j < len(g_mg_vars) and first < 0 { + if g_mg_vstate[j] == s { first = j } + j += 1 + } + if first >= 0 { + let v = g_mg_vars[first] + mg_emit(`STATE {g_ms_name[s]} {v.file} {itoa(v.pos)} {itoa(g_ms_export[s])} {g_ms_param[s]}`, "") + } + s += 1 + } + mg_refs() + mg_units() + mg_calls() + let f = file_open(g_mg_plan, "wb") + if f == null { die("ludicc: cannot write the migration plan\n") } + i = 0 + while i < len(g_mg_lines) { + file_write(f, g_mg_lines[i], len(g_mg_lines[i])) + file_write(f, "\n", 1) + i += 1 + } + i = 0 + while i < len(g_mg_report) { + file_write(f, g_mg_report[i], len(g_mg_report[i])) + file_write(f, "\n", 1) + i += 1 + } + file_close(f) + exit(0) +} diff --git a/selfhost/frontend/parse.ludic b/selfhost/frontend/parse.ludic index ecec0be1..cc162f9a 100644 --- a/selfhost/frontend/parse.ludic +++ b/selfhost/frontend/parse.ludic @@ -179,14 +179,20 @@ function interp_str(e: Node) -> Node { # wrap a hole in string( let c = node(E_CALL); let id = node(E_ID); id.s = "string"; c.a = id; push(c.kids, e); return c } var g_interp_line: int = 1 # the line the interpolated string sits on +var g_interp_pos: int = -1 # the backtick's place in its file +var g_hole_base: int = -1 function parse_hole(inner: pointer) -> Node { # re-lex+parse an embedded expression let saved_toks = toks; let saved_pi = pi + let saved_base = g_lex_base + g_lex_base = g_hole_base # the hole's place in its file, or -1 lex_at(inner, g_interp_line); pi = 0; skipnl() + g_lex_base = saved_base let e = expr() toks = saved_toks; pi = saved_pi return e } function parse_interp(raw: pointer) -> Node { + let my_pos = g_interp_pos # a hole's own templates move g_interp_pos let n = len(raw) var acc: Node = null let lit = bytes(n + 1) @@ -202,6 +208,8 @@ function parse_interp(raw: pointer) -> Node { # find the hole's closing brace: braces nest, and a brace inside a "string", a 'char' or a # `template` in the hole is text, not structure (lex.ludic, hole_end) i = hole_end(raw, i, n) + g_hole_base = -1 + if my_pos >= 0 { g_hole_base = my_pos + 1 + hs } acc = interp_add(acc, interp_str(parse_hole(raw[hs..i]))) i += 1 # skip the closing '}' } else { @@ -246,7 +254,7 @@ function parse_list() -> Node { function p_primary() -> Node { let t = toks[pi] - if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; return parse_interp(t.text) } + if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; g_interp_pos = t.pos; return parse_interp(t.text) } if is_op("[") { return parse_list() } if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() } if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; n.s = t.text; pi += 1; return n } # s: a long literal's digits @@ -533,6 +541,7 @@ function stmt_body() -> Node { function parse_var() -> Node { pi += 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype() if is_op("=") { pi += 1; n.a = expr() } + n.pos2 = toks[pi - 1].end # 0.S: where the declaration ends return n } function parse_const() -> Node { @@ -558,10 +567,11 @@ function parse_fn() -> Node { pi += 1; let n = node(N_FN); n.s = eat_id() if is_op("<") { n.tps = gen_params() } # L5: function first(xs: []T) -> T gen_enter(n.tps) + n.pos2 = toks[pi].pos # 0.S: the parameter list's '(' eat_op("(") if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) } while not is_op(")") { - let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype() + let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); parse_param_mut(p); p.ty = ptype() if is_op("=") { pi += 1; p.a = expr() } # L11: a default, for a call that leaves it out push(n.kids, p) if is_op(",") { pi += 1 } @@ -573,7 +583,14 @@ function parse_fn() -> Node { gen_enter(null) return n } -function parse_main() -> Node { pi += 1; let n = node(N_MAIN); n.a = block(); return n } +function parse_main() -> Node { + pi += 1 + let n = node(N_MAIN) + let ps = parse_entry_params() # 0.S: entry (h: mut Hiker) - state.ludic + n.a = block() + entry_bind(n.a, ps) + return n +} # issue #76 — rewrite calls to a namespace-block sibling (a bare short-name call) # to the prefixed function name, so a body can call `pending()` where the function @@ -1008,7 +1025,21 @@ function parse_one_decl() -> void { 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 == "{") { 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("var") { push(prog, parse_var()); return } + 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 { + perr(`a module-level var is refused: a module's changing data is its state (state Name {{ ... }}), passed to the functions that use it - or, if it never changes, a let`) + } + push(prog, parse_var()) + return + } + if is_id("let") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == ":") { # 0.S: a module-level let - immutable all the way down + let v = parse_var() + v.uns = 1 + if v.a == null { perr(`a module-level let {v.s} needs its value`) } + push(prog, v) + return + } if is_id("const") { push(prog, parse_const()); return } var is_unsafe = false if is_id("unsafe") and (toks[pi + 1].text == "function") { # L7: an unsafe function @@ -1041,8 +1072,11 @@ function parse_test() -> Node { let n = node(N_TEST) n.s = toks[pi].text n.line = ln + n.pos2 = toks[pi].end # 0.S: where state parameters would go pi += 1 # past the name + let ps = parse_entry_params() # 0.S: test "name" (h: mut Hiker) n.a = block() + entry_bind(n.a, ps) return n } diff --git a/selfhost/frontend/parse_game.ludic b/selfhost/frontend/parse_game.ludic index b2719339..0a505986 100644 --- a/selfhost/frontend/parse_game.ludic +++ b/selfhost/frontend/parse_game.ludic @@ -38,6 +38,8 @@ function parse_event() -> Node { function parse_system() -> Node { pi += 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update" + n.pos2 = toks[pi - 1].end # 0.S: where state parameters would go + let ps = parse_entry_params() # 0.S: handler Draw(h: Hiker) phase Render # postfix clauses on `handler Name …`: @anno(...) (parsed and reserved, e.g. # @deterministic / @Reads(...) / @Writes(...)) and `phase X`. The handler's # query lives in a prefix `@Queries(...)` annotation (see parse_one_decl), not @@ -56,6 +58,8 @@ function parse_system() -> Node { } skipnl() n.a = block() + n.a.pos2 = n.pos2 # 0.S2: a listener is known by its body + entry_bind(n.a, ps) return n } diff --git a/selfhost/frontend/ports.ludic b/selfhost/frontend/ports.ludic index e4b22804..912f4e38 100644 --- a/selfhost/frontend/ports.ludic +++ b/selfhost/frontend/ports.ludic @@ -93,6 +93,7 @@ function port_bind_one(b: Node) -> void { while i < len(rec.kids) { let e = rec.kids[i].a if e != null and e.kind == E_ID { port_bind_var(b, c, rec.kids[i]) } + if e != null and e.kind == E_MEMBER and e.a != null and e.a.kind == E_ID and is_state_ty(e.a.s) { port_bind_var(b, c, rec.kids[i]) } i += 1 } # what the bind names is seen from the bind's file: the port itself, and every `fn name` @@ -135,9 +136,17 @@ function port_defaults_only(k: int) -> void { # `money: gm_money` for a `money: fn() -> int` member: the function `bind__` that # returns the variable, written in the bind's own file (so it sees what the bind sees) function port_bind_var(b: Node, c: Node, fi: Node) -> void { - let name = fi.a.s - let g = find_global(name) - if g == null { port_err(b, `bind {b.s}: {fi.s}: {name} is not a variable (a function is bound as fn {name})`) } + # 0.S: `money: Wallet.cash` - a field of a state, read through the state the runtime supplies + var sty: pointer = null + var name: pointer = null + if fi.a.kind == E_MEMBER { + sty = fi.a.a.s + name = fi.a.s + } else { + name = fi.a.s + let g = find_global(name) + if g == null { port_err(b, `bind {b.s}: {fi.s}: {name} is not a variable (a function is bound as fn {name})`) } + } let fty = field_type(c, fi.s) let n = len(fty) if n < 7 or not (fty[0 .. 6] == "fn()->") { @@ -145,7 +154,8 @@ function port_bind_var(b: Node, c: Node, fi: Node) -> void { } let ret = fty[6 .. n] let gen = `bind_{b.s}_{fi.s}` - let src = `function {gen}() -> {ret} {{\n return {name}\n}}\n` + var src = `function {gen}() -> {ret} {{\n return {name}\n}}\n` + if sty != null { src = `function {gen}(st: {sty}) -> {ret} {{\n return st.{name}\n}}\n` } let saved_toks = toks let saved_pi = pi let saved_file = g_parse_file diff --git a/selfhost/frontend/state.ludic b/selfhost/frontend/state.ludic new file mode 100644 index 00000000..6f36d407 --- /dev/null +++ b/selfhost/frontend/state.ludic @@ -0,0 +1,135 @@ +# state.ludic — 0.S: no function writes a global. A module's changing data is a `state` record: +# +# state Hiker { hips: int = -1, spine: int = -1 } +# +# The program holds exactly one instance of each, which no code can name - it is the hidden global +# `state$Hiker` (a name the lexer cannot produce). It reaches code only as a parameter, `h: mut +# Hiker` to change it or `h: Hiker` to read it, and the runtime supplies it at the entry points: +# - a body that declares it: `entry (h: mut Hiker)`, `handler Draw(h: Hiker) phase Render`, +# `@On(E) handler Heard(h: mut Hiker)`, `test "name" (h: mut Hiker)` - each is a `let` at the +# body's top bound to the instance; +# - a function value: `fn f` of a function whose leading parameters are states is that function +# with them supplied (emit_fnval.ludic writes the thunk), so a system, a port's bind and any +# callback a package calls are entry points without saying so; +# - a call the compiler writes: a namespace method's target, a runtime built-in. +# A module-level `var` is refused (a `state` holds it), unless the build says --globals (the +# toolchain's own programs, which are not part of this); a module-level `let` is deeply immutable. +var g_state_names: []pointer = new []pointer +var g_allow_globals: bool = false + +function state_global(name: pointer) -> pointer { return `state${name}` } +function is_state_ty(t: pointer) -> bool { + if t == null { return false } + var i = 0 + while i < len(g_state_names) { + if (g_state_names[i] == t) { return true } + i += 1 + } + return false +} +# state NAME { fields } - a record, and its one instance +function parse_state() -> void { + let c = parse_component() + c.uns = 2 + push(prog, c) + push(g_state_names, c.s) + let v = node(N_VAR) + v.s = state_global(c.s) + v.ty = c.s + v.line = c.line + v.uns = 1 + let nw = node(E_NEW) + nw.s = c.s + nw.file = c.file + nw.line = c.line + v.a = nw + push(prog, v) +} +# `name: mut T` in a parameter list: the `mut`, when it is there +function parse_param_mut(p: Node) -> void { + if is_id("mut") and toks[pi + 1].kind == TK_ID { + pi += 1 + p.uns = 1 + } +} +# `(h: mut Hiker, ...)` before an entry point's body: the states the runtime supplies, as the +# `let`s that open the body +function parse_entry_params() -> []Node { + let out = new []Node + if not is_op("(") { return out } + pi += 1 + while not is_op(")") { + skipnl() + let p = node(N_PARAM) + p.s = eat_id() + eat_op(":") + parse_param_mut(p) + p.ty = ptype() + push(out, p) + skipnl() + if is_op(",") { pi += 1 } + } + eat_op(")") + return out +} +# a call the compiler writes (a namespace method's target, a runtime built-in) to a function whose +# leading parameters are states: the instances, unless the call already gives every parameter +function state_inject(e: Node, f: Node) -> void { + if f == null or e == null { return } + var k = 0 + var total = 0 + var i = 0 + while i < len(f.kids) { + if f.kids[i].kind == N_PARAM { + if total == k and is_state_ty(f.kids[i].ty) { k += 1 } + total += 1 + } + i += 1 + } + if k == 0 or len(e.kids) + k > total { return } + let args = new []Node + i = 0 + while i < k { + let id = node(E_ID) + id.s = state_global(f.kids[i].ty) + id.file = e.file + id.line = e.line + push(args, id) + i += 1 + } + i = 0 + while i < len(e.kids) { + push(args, e.kids[i]) + i += 1 + } + e.kids = args +} +function entry_bind(body: Node, ps: []Node) -> void { + if len(ps) == 0 or body == null { return } + let lets = new []Node + var i = 0 + while i < len(ps) { + let p = ps[i] + let l = node(S_LET) + l.s = p.s + l.ty = p.ty + l.file = p.file + l.line = p.line + l.ival = 0 + l.uns = p.uns # 1: a `mut` state; 0: read-only + l.tps = "state" # an entry point's state: checked to be one + let id = node(E_ID) + id.s = state_global(p.ty) + id.file = p.file + id.line = p.line + l.a = id + push(lets, l) + i += 1 + } + var k = 0 + while k < len(body.kids) { + push(lets, body.kids[k]) + k += 1 + } + body.kids = lets +} diff --git a/selfhost/main.ludic b/selfhost/main.ludic index d63891d3..1f08247d 100644 --- a/selfhost/main.ludic +++ b/selfhost/main.ludic @@ -152,7 +152,15 @@ entry { else if a == "--fmt" { fmt = true } else if a == "--save-temps" { save = true } else if a == "--run" { run = true } - else if a == "--check" { g_check_only = true } # parse, type, module and port checks; nothing emitted + else if a == "--check" { g_check_only = true } + else if a == "--globals" { g_allow_globals = true } + else if a == "--migrate-state" { # 0.S2: write the plan ludic migrate state applies + ai += 1 + if ai < arg_count() { g_mg_plan = arg(ai) } + g_migrate = true + g_allow_globals = true + } + else if a == "--migrate-runtime" { g_mg_runtime = true } # 0.S: the toolchain's own programs keep module-level vars # parse, type, module and port checks; nothing emitted else if a == "--coverage" { g_coverage = true } else if a == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } } else if a == "--gui" { gui = true } @@ -207,7 +215,9 @@ entry { privates_rename() # L3: a module's private names are its own (privates.ludic) check_duplicate_fns() # two declarations of one name: the cause, before its symptoms check_duplicate_decls() + if g_migrate { mg_collect() } # 0.S2: the vars that move (migrate.ludic) check_program() # L4: the types agree before anything is emitted + if g_migrate { mg_finish() } if g_check_only { exit(0) } # --check: the checks are all there is emit_program() deps_flush() # LUDIC_DEPS=: the module graph (ludic deps) diff --git a/tools/ludic-cli/main.ludic b/tools/ludic-cli/main.ludic index 3646cb78..a357f288 100644 --- a/tools/ludic-cli/main.ludic +++ b/tools/ludic-cli/main.ludic @@ -31,6 +31,7 @@ program Ludic { import "scripts.ludic" import "deps.ludic" import "testpar.ludic" + import "migrate.ludic" function usage() -> void { print("ludic — the toolchain for the Ludic language") @@ -47,6 +48,7 @@ program Ludic { print(" compile and run the project's tests (-v: every test's line)") print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]") print(" the module graph as the compiler sees it, and how tangled it is") + print(" migrate state [file] [--runtime] module-level vars into states, passed as parameters (0.S)") print(" clean remove build/") print("") print("packages:") @@ -99,6 +101,7 @@ program Ludic { if (cmd == "run") { return cmd_run() } if (cmd == "test") { return cmd_test() } if (cmd == "deps") { return cmd_deps() } + if (cmd == "migrate") { return cmd_migrate() } if (cmd == "clean") { return cmd_clean() } if (cmd == "fmt") { return cmd_fmt() } if (cmd == "lsp") { return cmd_lsp() } diff --git a/tools/ludic-cli/migrate.ludic b/tools/ludic-cli/migrate.ludic new file mode 100644 index 00000000..c1ca250d --- /dev/null +++ b/tools/ludic-cli/migrate.ludic @@ -0,0 +1,202 @@ +# ---- ludic migrate state ------------------------------------------------------- +# 0.S2: module-level vars become states. The compiler works out the edits (selfhost/frontend/ +# migrate.ludic, `ludicc --migrate-state `); this applies them to the source, file by file, +# and prints what it could not decide. +# +# ludic migrate state [file] [--runtime] [--unsafe] [--dry-run] +# +# Run it once per program that uses what changes (a package's test programs, the game's entry, +# its lab): a later run finds the states an earlier one made and passes them on. +var mgx_file: []pointer = null # an edit: its file, where, how much it replaces, with what +var mgx_pos: []int = null +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 + +function mgx_unesc(s: pointer) -> pointer { + var out = "" + var i = 0 + let n = slen(s) + while i < n { + if s[i] == 92 and i + 1 < n { + let c = s[i + 1] + if c == 's' { out = out + " " } else if c == 'n' { out = out + "\n" } else { out = out + sslice(s, i + 1, i + 2) } + i += 2 + } else { + out = out + sslice(s, i, i + 1) + i += 1 + } + } + return out +} +function mgx_text_of(path: pointer) -> pointer { + for i in 0 .. len(mgx_names) { if mgx_names[i] == path { return mgx_texts[i] } } + let t = read_file(path) + push(mgx_names, path) + push(mgx_texts, t) + return t +} +function mgx_edit(file: pointer, pos: int, n: int, text: pointer) -> void { + push(mgx_file, file) + push(mgx_pos, pos) + push(mgx_len, n) + push(mgx_text, text) +} +function mgx_line_start(t: pointer, pos: int) -> int { + var i = pos + while i > 0 and t[i - 1] != 10 { i -= 1 } + return i +} +function mgx_line_end(t: pointer, pos: int) -> int { + var i = pos + while t[i] != 0 and t[i] != 10 { i += 1 } + if t[i] == 10 { i += 1 } + return i +} +function mgx_indent(t: pointer, start: int) -> pointer { + var i = start + while t[i] == 32 or t[i] == 9 { i += 1 } + return sslice(t, start, i) +} +function mgx_rtrim(s: pointer) -> pointer { + var n = slen(s) + while n > 0 and (s[n - 1] == 10 or s[n - 1] == 13 or s[n - 1] == 32) { n -= 1 } + return sslice(s, 0, n) +} +function cmd_migrate() -> int { + if arg_count() < 3 or not (arg(2) == "state") { + err("usage: ludic migrate state [file] [--runtime] [--unsafe] [--dry-run]\n") + return 2 + } + var src = "" + var runtime = "" + var dry = false + var ai = 3 + while ai < arg_count() { + let a = arg(ai) + if a == "--runtime" { runtime = " --migrate-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 { src = a } + ai += 1 + } + let entry = find_entry(src) + if entry == "" { return no_entry() } + ensure_ludicc() + let plan = tmp_path("migrate.plan") + shell(`rm -f {plan}`) + shq(`{ludicc()} --headless{unsafe_flag()}{runtime} --migrate-state {plan} {entry} > {tmp_path("migrate.err")} 2>&1`) + let text = read_file(plan) + if text == null { + err(`ludic migrate: the compiler wrote no plan for {entry}:\n`) + err(capture(`grep -i error {tmp_path("migrate.err")} | head -5`)) + return 1 + } + return mgx_apply(split_lines(text), dry) +} +function mgx_apply(lines: []pointer, dry: bool) -> int { + mgx_file = new []pointer; mgx_pos = new []int; mgx_len = new []int; mgx_text = new []pointer + mgx_texts = new []pointer; mgx_names = new []pointer + let vfile = new []pointer + let vpos = new []int + let vpos2 = new []int + let vstate = new []pointer + let sname = new []pointer + let sfile = new []pointer + let spos = new []int + let sexp = new []int + let reports = new []pointer + for i in 0 .. len(lines) { + let w = dp_words(lines[i]) + if len(w) == 0 { continue } + if w[0] == "VAR" and len(w) == 5 { + push(vfile, w[1]) + push(vpos, s_to_int(w[2])) + push(vpos2, s_to_int(w[3])) + push(vstate, w[4]) + } else if w[0] == "STATE" and len(w) >= 6 { + push(sname, w[1]) + push(sfile, w[2]) + push(spos, s_to_int(w[3])) + push(sexp, s_to_int(w[4])) + } else if (w[0] == "REF") and len(w) == 5 { + 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] == "?" { push(reports, lines[i]) } + } + # 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 + let del_b = new []int + for v in 0 .. len(vfile) { + let t = mgx_text_of(vfile[v]) + let a = mgx_line_start(t, vpos[v]) + let b = mgx_line_end(t, vpos2[v]) + push(del_file, vfile[v]) + push(del_a, a) + push(del_b, b) + } + for s in 0 .. len(sname) { + var block = "" + var at = -1 + var indent = "" + for v in 0 .. len(vfile) { + if vstate[v] == sname[s] { + let t = mgx_text_of(vfile[v]) + if vfile[v] == sfile[s] and vpos[v] == spos[s] { + at = v + indent = mgx_indent(t, del_a[v]) + } + block = block + `{indent} {mgx_rtrim(sslice(t, vpos[v], del_b[v]))}\n` + } + } + var head = "" + if sexp[s] == 1 { head = "export " } + if at >= 0 { mgx_edit(vfile[at], del_a[at], 0, `{indent}{head}state {sname[s]} {{\n{block}{indent}}}\n`) } + } + for d in 0 .. len(del_file) { mgx_edit(del_file[d], del_a[d], del_b[d] - del_a[d], "") } + # an edit inside a line that goes is dropped with it + var files = 0 + var edits = 0 + for f in 0 .. len(mgx_names) { + let path = mgx_names[f] + var t = mgx_texts[f] + let idx = new []int + for e in 0 .. len(mgx_file) { + if mgx_file[e] == path { + var inside = false + for d in 0 .. len(del_file) { + if del_file[d] == path and mgx_len[e] < del_b[d] - del_a[d] and mgx_pos[e] > del_a[d] and mgx_pos[e] < del_b[d] { inside = true } + } + if not inside { push(idx, e) } + } + } + # last first, so every earlier place stays where it was; at one place, the deletion last + var a = 1 + while a < len(idx) { + let x = idx[a] + var b = a - 1 + while b >= 0 and (mgx_pos[idx[b]] < mgx_pos[x] or (mgx_pos[idx[b]] == mgx_pos[x] and mgx_len[idx[b]] < mgx_len[x])) { + idx[b + 1] = idx[b] + b -= 1 + } + idx[b + 1] = x + a += 1 + } + for k in 0 .. len(idx) { + let e = idx[k] + t = sslice(t, 0, mgx_pos[e]) + mgx_text[e] + sslice(t, mgx_pos[e] + mgx_len[e], slen(t)) + edits += 1 + } + if len(idx) > 0 { + files += 1 + if not dry { write_file(path, t) } + } + } + print(`migrate: {string(len(vfile))} vars into {string(len(sname))} states; {string(edits)} edits in {string(files)} files`) + for r in 0 .. len(reports) { print(` by hand: {sslice(reports[r], 2, slen(reports[r]))}`) } + return 0 +} diff --git a/tools/ludic-cli/scripts.ludic b/tools/ludic-cli/scripts.ludic index 7d1aece9..0a29e7c2 100644 --- a/tools/ludic-cli/scripts.ludic +++ b/tools/ludic-cli/scripts.ludic @@ -12,7 +12,7 @@ # the built-in commands a hook can wrap (every user command that does work) function is_hookable(cmd: pointer) -> bool { - return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib") + return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib") } # the directory this `ludic` binary lives in, with no trailing slash diff --git a/tools/ludic-cli/selfhost.ludic b/tools/ludic-cli/selfhost.ludic index c88b51a3..6aad9fec 100644 --- a/tools/ludic-cli/selfhost.ludic +++ b/tools/ludic-cli/selfhost.ludic @@ -24,6 +24,8 @@ function selfhost_frags() -> []pointer { push(f, "selfhost/frontend/ports.ludic") push(f, "selfhost/frontend/privates.ludic") push(f, "selfhost/frontend/privates_types.ludic") + push(f, "selfhost/frontend/state.ludic") + push(f, "selfhost/frontend/migrate.ludic") push(f, "selfhost/frontend/registry.ludic") push(f, "selfhost/frontend/registry_finish.ludic") push(f, "selfhost/frontend/registry_open.ludic") @@ -75,6 +77,7 @@ function selfhost_frags() -> []pointer { push(f, "selfhost/backend/emit_call.ludic") push(f, "selfhost/backend/emit_fnval.ludic") push(f, "selfhost/backend/emit_vis.ludic") + push(f, "selfhost/check/check_state.ludic") push(f, "selfhost/check/check_index.ludic") push(f, "selfhost/check/check_types.ludic") push(f, "selfhost/check/check_env.ludic") @@ -135,7 +138,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int { if not write_selfhost_src(src) { err("ludic-dev: cannot write build/selfhost.ludic\n"); return 1 } let ll = `{outbin}.ll` # say why when it fails: it used to exit 1 with nothing on the screen - if not shq(`{lc} --unsafe {src} > {ll} 2>{tmp_dir()}/shb.err`) { + if not shq(`{lc} --unsafe --globals {src} > {ll} 2>{tmp_dir()}/shb.err`) { shell(`rm -f {ll}`) err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`)) return 1 @@ -162,7 +165,7 @@ function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int { # the quiet core, reused by the test suites; returns true on success. On failure # the self-host/link diagnostics are left in tmp_path("gb.err"). # an example that is a raw-memory program on purpose (a decoder's ABI, a Vulkan demo) is built -# with --unsafe (L7); the suite sets this around those cases only +# with --unsafe --globals (L7); the suite sets this around those cases only var g_case_unsafe: bool = false function case_unsafe_flag() -> pointer { if g_case_unsafe { return " --unsafe" } @@ -194,11 +197,11 @@ function cmd_bootstrap() -> int { if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 } print(" stage0: bin/ludicc -> gen1 (self-host compiler)") - if not shq("build/boot/gen1 --unsafe build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 } + if not shq("build/boot/gen1 --unsafe --globals build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 } if not shq(`{cc()} build/boot/gen2.ll -o build/boot/gen2 2>/dev/null`) { print("FAIL: gen2 assemble"); return 1 } print(` stage1: gen1 -> gen2.ll ({line_count("build/boot/gen2.ll")} lines) -> gen2`) - if not shq("build/boot/gen2 --unsafe build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 } + if not shq("build/boot/gen2 --unsafe --globals build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 } print(` stage2: gen2 -> gen3.ll ({line_count("build/boot/gen3.ll")} lines)`) if shq("cmp -s build/boot/gen2.ll build/boot/gen3.ll") { @@ -220,7 +223,7 @@ function cmd_bootstrap_cfree() -> int { if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 } print(" seed.ll --clang--> sh_seed (no C compiler used)") if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } - if not shq("build/cfree/sh_seed --unsafe build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 } + if not shq("build/cfree/sh_seed --unsafe --globals build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 } print(` sh_seed compiles selfhost.ludic -> out.ll ({line_count("build/cfree/out.ll")} lines)`) if shq("cmp -s build/cfree/out.ll selfhost/ludicc.seed.ll") { print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler") @@ -240,17 +243,17 @@ function cmd_reseed() -> int { shell("mkdir -p build/cfree") if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) { - if not shq("build/cfree/sh_old --unsafe build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 } + if not shq("build/cfree/sh_old --unsafe --globals build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 } if not shq(`{cc()} build/cfree/step1.ll -o build/cfree/sh_new`) { print("FAIL: assemble sh_new"); return 1 } - if not shq("build/cfree/sh_new --unsafe build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 } + if not shq("build/cfree/sh_new --unsafe --globals build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 } # and the Windows seed, from the same compiler: a Windows checkout bootstraps from it - if not shq("build/cfree/sh_new --unsafe --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 } + if not shq("build/cfree/sh_new --unsafe --globals --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 } } else { print("seed does not build; reseeding from bin/ludicc") ensure_ludicc() if cmd_selfhost_build("bin/ludicc", "build/cfree/sh_c") != 0 { print("FAIL: build from bin/ludicc"); return 1 } - if not shq("build/cfree/sh_c --unsafe build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 } - if not shq("build/cfree/sh_c --unsafe --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 } + if not shq("build/cfree/sh_c --unsafe --globals build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 } + if not shq("build/cfree/sh_c --unsafe --globals --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 } } print(`reseeded: {line_count("selfhost/ludicc.seed.ll")} lines`) return 0 diff --git a/tools/ludic-cli/selfhost_test.ludic b/tools/ludic-cli/selfhost_test.ludic index b4d6cf86..887afad0 100644 --- a/tools/ludic-cli/selfhost_test.ludic +++ b/tools/ludic-cli/selfhost_test.ludic @@ -7,7 +7,7 @@ function sh_case(name: pointer, exp: pointer) -> void { let ll = `{tmp_dir()}/sh_{name}.ll` let er = `{tmp_dir()}/sh_{name}.err` let bn = `{tmp_dir()}/sh_{name}` - if not shq(`bin/ludicc --unsafe selfhost/tests/{name}.ludic > {ll} 2>{er}`) { + if not shq(`bin/ludicc --unsafe --globals selfhost/tests/{name}.ludic > {ll} 2>{er}`) { bad2(`{name}: self-host errored`, capture_line(`head -1 {er}`)); return } if not shq(`{cc()} {ll} -o {bn} 2>/dev/null`) { bad(`{name}: IR did not assemble`); return } diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index d6364b06..3a2871c4 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -508,6 +508,29 @@ function check_build_case() -> void { ok(lbl) } +# ludic migrate state: a program with module-level vars comes out with a state, threaded, and runs +# the same +function migrate_case() -> void { + let lbl = "ludic migrate state: vars into a state, parameters threaded, the same output" + let work = `{tmp_dir()}/migrate` + shell(`rm -rf {work} && mkdir -p {work}`) + var src = "program Demo {\n var count: int = 0\n var names: []string = new []string # the names\n" + src = src + " function add(n: string) -> void {\n push(names, n)\n count += 1\n }\n" + src = src + " function total() -> int {\n return count\n }\n" + src = src + " function report() -> string {\n return `{{total()}} names, first {{names[0]}}`\n }\n" + src = src + " entry {\n add(\"a\")\n add(\"b\")\n print(report())\n }\n}\n" + write_file(`{work}/demo.ludic`, src) + if not shq(`bin/ludic migrate state {work}/demo.ludic > {work}/out.txt 2>&1`) { bad2(lbl, capture_line(`tail -1 {work}/out.txt`)); return } + let got = read_file(`{work}/demo.ludic`) + if not s_contains(got, "state DemoState {") or not s_contains(got, "function add(demo_st: mut DemoState, n: string)") or not s_contains(got, "function total(demo_st: DemoState)") or not s_contains(got, "entry (demo_st: mut DemoState)") { + bad2(lbl, `rewrote it as [{got}]`) + return + } + if not shq(`bin/ludicc {work}/demo.ludic -o {work}/demo > {work}/cc.txt 2>&1`) { bad2(lbl, capture_line(`grep -i error {work}/cc.txt | head -1`)); return } + let ran = capture_line(`{work}/demo`) + if ran == "2 names, first a" { ok(lbl) } else { bad2(lbl, `ran [{ran}]`) } +} + # 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" @@ -877,6 +900,15 @@ function cmd_dev_test() -> int { feat_case("modules/layers", "", "5", "layers.ludic (L3: `module menu in layer app uses items` - one layer's modules use each other and may go round)") reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer") reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused") + feat_case("state/counter", "", "11 3 heard 5", "counter.ludic (0.S: a state, mut and read-only parameters, entry, listener and fn-value injection)") + feat_case("state/port_field", "", "40", "port_field.ludic (0.S: a port member bound to a state's field)") + spec_case("state/tested", "== 2 passed, 0 failed ==") + reject_case("rejected/state_readonly", "c is read-only here (c: Tally)", "a read-only state cannot be assigned through") + reject_case("rejected/state_mut_arg", "bump changes Tally (c: mut Tally), and c is read-only here", "a read-only state is not passed as mut") + reject_case("rejected/module_var", "a module-level var is refused", "a module-level var is refused") + reject_case("rejected/module_let_write", "LIMITS is module-level and immutable all the way down", "a module-level let is immutable all the way down") + reject_case("rejected/mut_not_state", "mut is for a state parameter", "mut is for a state parameter") + 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") feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)") reject_case("rejected/wrong_arity", "this call to area leaves out h, which has no default", "a call with the wrong number of arguments is refused") diff --git a/tools/ludic-cli/toolchain.ludic b/tools/ludic-cli/toolchain.ludic index b3884c53..2252b4e8 100644 --- a/tools/ludic-cli/toolchain.ludic +++ b/tools/ludic-cli/toolchain.ludic @@ -6,7 +6,7 @@ function build_tool(name: pointer, src: pointer) -> bool { let ll = `build/{name}.ll` let out = `bin/{exe_name(name)}` - if not shq(`bin/{exe_name("ludicc")} --unsafe {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } + if not shq(`bin/{exe_name("ludicc")} --unsafe --globals {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } # write to a temp then move, so a running bin/ludic can rebuild itself in place # (Windows lets a running executable be renamed, never replaced, so the old one # steps aside first)