fix(migrate): 0.R4 - --prune keeps a state declared after a plain parameter; a parameter named twice is refused

On Maroon Lake the prune gave home_keep_records(base_app_st, home_st, r: RunRecords, save_app_st) a
second save_app_st at the front, and every call a second argument: a state declared after a plain
parameter was not counted as declared. It is now, and a call to such a function is never given the
state again. `ludic build --check` let the duplicate through and clang refused it; the checker now
refuses a function that names two parameters alike (`add names two parameters n`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-26 05:32:08 +03:00
parent 71735b10a2
commit c35481f344
8 changed files with 47291 additions and 46599 deletions

View file

@ -318,7 +318,8 @@ migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files
function nor anything it calls uses, and the argument that fills it at every call. An argument for a function nor anything it calls uses, and the argument that fills it at every call. An argument for a
parameter the callee no longer has goes too - a package's verb that dropped a state leaves its callers parameter the callee no longer has goes too - a package's verb that dropped a state leaves its callers
passing one too many, and `ludic migrate state --prune <program>` puts them right. A reducer keeps its passing one too many, and `ludic migrate state --prune <program>` puts them right. A reducer keeps its
state. When ludic.base's queues began keeping their own counts, every package verb lost its state, and a state declared after a plain parameter (`home_keep(r: Records, save_st: mut Save)`) is
kept where it is. When ludic.base's queues began keeping their own counts, every package verb lost its
`base_st` this way (`wallet_earn(wallet_st, n)`, not `wallet_earn(base_st, wallet_st, n)`): `base_st` this way (`wallet_earn(wallet_st, n)`, not `wallet_earn(base_st, wallet_st, n)`):
``` ```
@ -399,7 +400,8 @@ What it holds apart: `int`, `float`, `fixed` and `bool` (a float or a fixed into
`int(x)`; a float and a fixed never meet but by a literal, which takes whichever kind its slot is); `int(x)`; a float and a fixed never meet but by a literal, which takes whichever kind its slot is);
text and numbers (`string(n)` or a template); one record type and another; slices of different text and numbers (`string(n)` or a template); one record type and another; slices of different
elements; functions of different types. A call gives exactly as many arguments as there are elements; functions of different types. A call gives exactly as many arguments as there are
parameters, a `return` gives the declared result, and `push` gives the slice's own element. parameters, a `return` gives the declared result, and `push` gives the slice's own element. A
function names each parameter once (`add names two parameters n`).
`pointer` is untyped, as `void *` is in C: it goes wherever a reference is wanted and takes any `pointer` is untyped, as `void *` is in C: it goes wherever a reference is wanted and takes any
reference, and a `[]pointer` any slice of references. Restricting what a raw pointer may reach is reference, and a `[]pointer` any slice of references. Restricting what a raw pointer may reach is

View file

@ -0,0 +1,8 @@
bump: patch
type: fix
**`ludic migrate state --prune` keeps a state declared after a plain parameter, and a parameter named twice is refused.**
A function like `home_keep(r: Records, save_st: mut Save)` was taken to lack `save_st`, and the
prune gave it a second one at the front (`home_keep(save_st: mut Save, r: Records, save_st: mut
Save)`) - which `ludic build --check` let through and clang then refused. The migration now counts a
state anywhere in the list as declared, and the checker refuses a function that names two parameters
alike (`add names two parameters n`).

View file

@ -0,0 +1,5 @@
# a parameter named twice is refused where it is written
program ParamTwice {
function add(n: int, k: int, n: int) -> int { return n + k }
entry { print(`{add(1, 2, 3)}`) }
}

View file

@ -154,12 +154,26 @@ function ck_stmt(s: Node) -> void {
} }
ck_walk(s) ck_walk(s)
} }
# a name given to two parameters: the second would shadow the first, and nothing could reach it
function ck_param_twice(d: Node, i: int) -> void {
var j = 0
while j < i {
if d.kids[j].kind == N_PARAM and (d.kids[j].s == d.kids[i].s) {
ck_err("arity", d.kids[i], `{d.s} names two parameters {d.kids[i].s}`)
return
}
j += 1
}
}
function ck_fn_body(d: Node) -> void { function ck_fn_body(d: Node) -> void {
mg_enter(d, 0) mg_enter(d, 0)
let m = ck_mark() let m = ck_mark()
var i = 0 var i = 0
while i < len(d.kids) { while i < len(d.kids) {
if d.kids[i].kind == N_PARAM { ck_bind_param(d.kids[i]) } if d.kids[i].kind == N_PARAM {
ck_param_twice(d, i)
ck_bind_param(d.kids[i])
}
i += 1 i += 1
} }
ck_ret = d.ty ck_ret = d.ty

View file

@ -559,8 +559,22 @@ function mg_list(set: pointer) -> []int {
function mg_declared(n: Node, s: int) -> bool { function mg_declared(n: Node, s: int) -> bool {
if n.kind != N_FN { return false } if n.kind != N_FN { return false }
var i = 0 var i = 0
while i < len(n.kids) and n.kids[i].kind == N_PARAM and is_state_ty(n.kids[i].ty) { while i < len(n.kids) { # anywhere in the list: a state after a plain parameter is declared too
if (n.kids[i].ty == g_ms_name[s]) { return true } if n.kids[i].kind == N_PARAM and (n.kids[i].ty == g_ms_name[s]) { return true }
i += 1
}
return false
}
# a state declared after a plain parameter: every call already names it where it stands
function mg_declared_late(n: Node, s: int) -> bool {
if n.kind != N_FN { return false }
var plain = false
var i = 0
while i < len(n.kids) {
if n.kids[i].kind == N_PARAM {
if not is_state_ty(n.kids[i].ty) { plain = true }
else if plain and (n.kids[i].ty == g_ms_name[s]) { return true }
}
i += 1 i += 1
} }
return false return false
@ -972,7 +986,7 @@ function mg_calls() -> void {
let passed = g_mc_passed[c] == 1 let passed = g_mc_passed[c] == 1
var k = 0 var k = 0
while k < len(need) { while k < len(need) {
if not (mg_declared(callee, need[k]) and passed) { if not (mg_declared(callee, need[k]) and (passed or mg_declared_late(callee, need[k]))) {
if len(text) > 0 { text = text + ", " } if len(text) > 0 { text = text + ", " }
text = text + g_ms_param[need[k]] text = text + g_ms_param[need[k]]
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -539,7 +539,8 @@ function migrate_case() -> void {
# ludic migrate state --prune: a state nothing uses leaves the functions that take it and the calls # ludic migrate state --prune: a state nothing uses leaves the functions that take it and the calls
# that pass it; an argument for a parameter the callee no longer has goes too (a package's verb that # that pass it; an argument for a parameter the callee no longer has goes too (a package's verb that
# dropped a state), and the program then builds and runs # dropped a state), a state declared after a plain parameter is kept where it is, and the program
# then builds and runs
function migrate_prune_case() -> void { function migrate_prune_case() -> void {
let lbl = "ludic migrate state --prune: unused states and the arguments that fill them go, the same output" let lbl = "ludic migrate state --prune: unused states and the arguments that fill them go, the same output"
let work = `{tmp_dir()}/migprune` let work = `{tmp_dir()}/migprune`
@ -550,12 +551,13 @@ function migrate_prune_case() -> void {
src = src + " function add(log_st: mut Log, b: mut Bag, v: int) -> void { put(log_st, b, v) }\n" src = src + " function add(log_st: mut Log, b: mut Bag, v: int) -> void { put(log_st, b, v) }\n"
src = src + " function outer(log_st: mut Log, b: mut Bag) -> void { add(log_st, b, 4) }\n" src = src + " function outer(log_st: mut Log, b: mut Bag) -> void { add(log_st, b, 4) }\n"
src = src + " function top(log_st: Log, b: Bag) -> int { return head(log_st, b.items) }\n" src = src + " function top(log_st: Log, b: Bag) -> int { return head(log_st, b.items) }\n"
src = src + " function said(log_st: mut Log, b: Bag) -> int {\n push(log_st.lines, \"x\")\n return len(log_st.lines)\n }\n" src = src + " function said(log_st: mut Log, b: Bag) -> int {\n push(log_st.lines, \"x\")\n return late(3, log_st)\n }\n"
src = src + " function late(k: int, log_st: Log) -> int { return len(log_st.lines) + k - 3 }\n"
src = src + " entry (log_st: mut Log, b: mut Bag) {\n outer(log_st, b)\n print(`{top(log_st, b)} {said(log_st, b)}`)\n }\n}\n" src = src + " entry (log_st: mut Log, b: mut Bag) {\n outer(log_st, b)\n print(`{top(log_st, b)} {said(log_st, b)}`)\n }\n}\n"
write_file(`{work}/prune.ludic`, src) write_file(`{work}/prune.ludic`, src)
if not shq(`bin/ludic migrate state --prune {work}/prune.ludic > {work}/out.txt 2>&1`) { bad2(lbl, capture_line(`tail -1 {work}/out.txt`)); return } if not shq(`bin/ludic migrate state --prune {work}/prune.ludic > {work}/out.txt 2>&1`) { bad2(lbl, capture_line(`tail -1 {work}/out.txt`)); return }
let got = read_file(`{work}/prune.ludic`) let got = read_file(`{work}/prune.ludic`)
if not s_contains(got, "function add(b: mut Bag, v: int) -> void { put(b, v) }") or not s_contains(got, "function outer(b: mut Bag) -> void { add(b, 4) }") or not s_contains(got, "function top(b: Bag) -> int { return head(b.items) }") or not s_contains(got, "function said(log_st: mut Log) -> int") or not s_contains(got, "entry (log_st: mut Log, b: mut Bag)") or not s_contains(got, "{top(b)} {said(log_st)}") { if not s_contains(got, "function add(b: mut Bag, v: int) -> void { put(b, v) }") or not s_contains(got, "function outer(b: mut Bag) -> void { add(b, 4) }") or not s_contains(got, "function top(b: Bag) -> int { return head(b.items) }") or not s_contains(got, "function said(log_st: mut Log) -> int") or not s_contains(got, "entry (log_st: mut Log, b: mut Bag)") or not s_contains(got, "{top(b)} {said(log_st)}") or not s_contains(got, "function late(k: int, log_st: Log) -> int") or not s_contains(got, "return late(3, log_st)") {
bad2(lbl, `rewrote it as [{got}]`) bad2(lbl, `rewrote it as [{got}]`)
return return
} }
@ -1025,6 +1027,7 @@ function cmd_dev_test() -> int {
migrate_prune_case() migrate_prune_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") 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)") 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/param_twice", "add names two parameters n", "a parameter named twice is refused")
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") 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")
reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused") reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused")
reject_case("rejected/record_mixup", "d wants a Door and this is a Key", "one record type is not another") reject_case("rejected/record_mixup", "d wants a Door and this is a Key", "one record type is not another")