wip(0.S1, 0.S2): state records, mut and read-only state parameters, entry injection, module var refused; ludic migrate state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
63a1fa1378
commit
1e8b5b0523
35 changed files with 1227 additions and 30 deletions
68
LANGUAGE.md
68
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 <Module>State { ... }`, where the first of them was;
|
||||
2. every reference to one is rewritten to `<module>_st.<name>`;
|
||||
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
|
||||
|
|
|
|||
8
examples/rejected/module_let_write.ludic
Normal file
8
examples/rejected/module_let_write.ludic
Normal file
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
7
examples/rejected/module_var.ludic
Normal file
7
examples/rejected/module_var.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
7
examples/rejected/mut_not_state.ludic
Normal file
7
examples/rejected/mut_not_state.ludic
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
9
examples/rejected/state_mut_arg.ludic
Normal file
9
examples/rejected/state_mut_arg.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
11
examples/rejected/state_readonly.ludic
Normal file
11
examples/rejected/state_readonly.ludic
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
29
examples/state/counter.ludic
Normal file
29
examples/state/counter.ludic
Normal file
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
12
examples/state/port_field.ludic
Normal file
12
examples/state/port_field.ludic
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
14
examples/state/tested.ludic
Normal file
14
examples/state/tested.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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_<name>$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)
|
||||
|
|
|
|||
|
|
@ -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)}`) }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
79
selfhost/check/check_state.ludic
Normal file
79
selfhost/check/check_state.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
406
selfhost/frontend/migrate.ludic
Normal file
406
selfhost/frontend/migrate.ludic
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
# migrate.ludic — 0.S2: `ludicc --migrate-state <plan>` 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 <Module>State { ... }`, where the first of them was;
|
||||
# 2. every reference to one - read or written - is `<module>_st.<var>`;
|
||||
# 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 <file> <pos> <pos2> <state> a declaration to move into <state>
|
||||
# STATE <state> <file> <pos> <export 0|1> <param> where <state> is declared (the first var)
|
||||
# REF <file> <pos> <len> <text> a reference's name replaced
|
||||
# INS <file> <pos> <text> text inserted (a parameter, an argument)
|
||||
# ? <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
|
||||
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)
|
||||
}
|
||||
|
|
@ -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<T>(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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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_<Port>_<member>` 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
|
||||
|
|
|
|||
135
selfhost/frontend/state.ludic
Normal file
135
selfhost/frontend/state.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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=<file>: the module graph (ludic deps)
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
202
tools/ludic-cli/migrate.ludic
Normal file
202
tools/ludic-cli/migrate.ludic
Normal file
|
|
@ -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 <plan>`); 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue