feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune
ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v), q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)). ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no longer uses, nor anything it calls, and the argument that fills it - including an argument for a parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8cf4b3fed6
commit
71735b10a2
151 changed files with 58668 additions and 54719 deletions
|
|
@ -48,7 +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(" migrate state [file] [--prune] [--runtime] module-level vars into states, passed as parameters (0.S); --prune drops states nothing uses")
|
||||
print(" clean remove build/")
|
||||
print("")
|
||||
print("packages:")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
# 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]
|
||||
# ludic migrate state [file|dir...] [--prune] [--runtime] [--unsafe] [--dry-run]
|
||||
# --prune also takes out every state parameter (and the argument that fills it) a function no
|
||||
# longer uses, nor anything it calls
|
||||
#
|
||||
# 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.
|
||||
|
|
@ -80,6 +82,30 @@ function mgx_rtrim(s: pointer) -> pointer {
|
|||
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 mgx_merge_dels(f: []pointer, a: []int, b: []int) -> void {
|
||||
let used = new []bool
|
||||
for i in 0 .. len(f) { push(used, false) }
|
||||
for i in 0 .. len(f) {
|
||||
if not used[i] {
|
||||
var lo = a[i]
|
||||
var hi = b[i]
|
||||
var grew = true
|
||||
while grew {
|
||||
grew = false
|
||||
for j in 0 .. len(f) {
|
||||
if not used[j] and j != i and f[j] == f[i] and a[j] <= hi and b[j] >= lo {
|
||||
if a[j] < lo { lo = a[j] }
|
||||
if b[j] > hi { hi = b[j] }
|
||||
used[j] = true
|
||||
grew = true
|
||||
}
|
||||
}
|
||||
}
|
||||
used[i] = true
|
||||
mgx_edit(f[i], lo, hi - lo, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
# the brace that closes the first block after `at` (strings skipped), or -1
|
||||
function mgx_close(t: pointer, at: int) -> int {
|
||||
var i = at
|
||||
|
|
@ -146,13 +172,14 @@ function mgx_mine(path: pointer) -> bool {
|
|||
}
|
||||
function cmd_migrate() -> int {
|
||||
if arg_count() < 3 or not (arg(2) == "state") {
|
||||
err("usage: ludic migrate state [file|dir...] [--runtime] [--unsafe] [--dry-run]\n")
|
||||
err("usage: ludic migrate state [file|dir...] [--prune] [--runtime] [--unsafe] [--dry-run]\n")
|
||||
return 2
|
||||
}
|
||||
let srcs = new []pointer
|
||||
mgx_roots = new []pointer
|
||||
var runtime = ""
|
||||
var dry = false
|
||||
var prune = ""
|
||||
var ai = 3
|
||||
while ai < arg_count() {
|
||||
let a = arg(ai)
|
||||
|
|
@ -162,6 +189,7 @@ function cmd_migrate() -> int {
|
|||
}
|
||||
else if a == "--unsafe" { g_unsafe_build = true }
|
||||
else if a == "--dry-run" { dry = true }
|
||||
else if a == "--prune" { prune = " --migrate-prune" }
|
||||
else if a[0] == '-' { err(`ludic migrate: unknown option {a}\n`); return 2 }
|
||||
else if shq(`test -d {sh_single(a)}`) {
|
||||
let found = test_files_in(a)
|
||||
|
|
@ -187,7 +215,7 @@ function cmd_migrate() -> int {
|
|||
for i in 0 .. len(srcs) {
|
||||
let plan = tmp_path(`migrate{string(i)}.plan`)
|
||||
shell(`rm -f {plan}`)
|
||||
shq(`{ludicc()} --headless{unsafe_flag()}{runtime} --migrate-state {plan} {sh_single(srcs[i])} > {tmp_path("migrate.err")} 2>&1`)
|
||||
shq(`{ludicc()} --headless{unsafe_flag()}{runtime}{prune} --migrate-state {plan} {sh_single(srcs[i])} > {tmp_path("migrate.err")} 2>&1`)
|
||||
let text = read_file(plan)
|
||||
if text == null {
|
||||
err(`ludic migrate: the compiler wrote no plan for {srcs[i]}:\n`)
|
||||
|
|
@ -246,12 +274,15 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
|
|||
let reports = new []pointer
|
||||
let foreign = new []pointer # files a plan would change that the migration may not
|
||||
let lfile = new []pointer
|
||||
let dfile = new []pointer # DELITEM: ranges to take out, merged where they touch
|
||||
let da = new []int
|
||||
let db = new []int
|
||||
let lpos = new []int
|
||||
for i in 0 .. len(lines) {
|
||||
let w = dp_words(lines[i])
|
||||
if len(w) == 0 { continue }
|
||||
var at = ""
|
||||
if (w[0] == "VAR") or (w[0] == "REF") or (w[0] == "INS") or (w[0] == "CLOSE") or (w[0] == "LET") { at = w[1] }
|
||||
if (w[0] == "VAR") or (w[0] == "REF") or (w[0] == "INS") or (w[0] == "CLOSE") or (w[0] == "LET") or (w[0] == "DELITEM") { at = w[1] }
|
||||
if (w[0] == "STATE") and len(w) > 2 { at = w[2] }
|
||||
if len(at) > 0 and mgx_roots != null and not mgx_mine(at) {
|
||||
var seen = false
|
||||
|
|
@ -292,6 +323,30 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
|
|||
mgx_edit(w[1], s_to_int(w[2]), s_to_int(w[3]), mgx_unesc(w[4]))
|
||||
} else if (w[0] == "INS") and len(w) == 4 {
|
||||
mgx_edit(w[1], s_to_int(w[2]), 0, mgx_unesc(w[3]))
|
||||
} else if (w[0] == "DELITEM") and len(w) == 3 {
|
||||
# a parameter or an argument taken out, with the comma that goes with it
|
||||
let t = mgx_text_of(w[1])
|
||||
let p = s_to_int(w[2])
|
||||
var e = p
|
||||
var depth = 0
|
||||
while t[e] != 0 and not (depth == 0 and (t[e] == ',' or t[e] == ')')) {
|
||||
if t[e] == '(' or t[e] == '[' { depth += 1 }
|
||||
if t[e] == ')' or t[e] == ']' { depth -= 1 }
|
||||
e += 1
|
||||
}
|
||||
var a = p
|
||||
var b = e
|
||||
if t[e] == ',' {
|
||||
b = e + 1
|
||||
while t[b] == ' ' { b += 1 }
|
||||
} else {
|
||||
var k = p - 1
|
||||
while k > 0 and t[k] == ' ' { k -= 1 }
|
||||
if t[k] == ',' { a = k }
|
||||
}
|
||||
push(dfile, w[1])
|
||||
push(da, a)
|
||||
push(db, b)
|
||||
} else if (w[0] == "CLOSE") and len(w) == 4 {
|
||||
let t = mgx_text_of(w[1])
|
||||
var k = s_to_int(w[2])
|
||||
|
|
@ -302,6 +357,8 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
|
|||
push(lpos, s_to_int(w[2]))
|
||||
} else if w[0] == "?" { push(reports, lines[i]) }
|
||||
}
|
||||
# the taken-out ranges: two neighbours in one list overlap (each with its comma), so they merge
|
||||
mgx_merge_dels(dfile, da, db)
|
||||
# a var nothing writes is a let - unless another program's plan saw it written
|
||||
var lets = 0
|
||||
for l in 0 .. len(lfile) {
|
||||
|
|
@ -425,7 +482,10 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
|
|||
}
|
||||
if len(idx) == 0 { continue }
|
||||
files += 1
|
||||
if dry { continue }
|
||||
if dry {
|
||||
edits += len(idx) # what would be made; nothing is written
|
||||
continue
|
||||
}
|
||||
let out = file_open(path, "wb")
|
||||
if out == null {
|
||||
err(`ludic migrate: cannot write {path}\n`)
|
||||
|
|
|
|||
|
|
@ -537,6 +537,33 @@ function migrate_case() -> void {
|
|||
if ran == "2 names, first a" { ok(lbl) } else { bad2(lbl, `ran [{ran}]`) }
|
||||
}
|
||||
|
||||
# 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
|
||||
# dropped a state), and the program then builds and runs
|
||||
function migrate_prune_case() -> void {
|
||||
let lbl = "ludic migrate state --prune: unused states and the arguments that fill them go, the same output"
|
||||
let work = `{tmp_dir()}/migprune`
|
||||
shell(`rm -rf {work} && mkdir -p {work}`)
|
||||
var src = "program Prune {\n state Log { lines: []string = new []string }\n state Bag { items: []int = new []int }\n"
|
||||
src = src + " function head<T>(xs: []T) -> T { return xs[0] }\n"
|
||||
src = src + " function put(b: mut Bag, v: int) -> void { push(b.items, 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 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 + " 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)
|
||||
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`)
|
||||
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)}") {
|
||||
bad2(lbl, `rewrote it as [{got}]`)
|
||||
return
|
||||
}
|
||||
if not shq(`bin/ludicc {work}/prune.ludic -o {work}/prune > {work}/cc.txt 2>&1`) { bad2(lbl, capture_line(`grep -i error {work}/cc.txt | head -1`)); return }
|
||||
let ran = capture_line(`{work}/prune`)
|
||||
if ran == "4 1" { ok(lbl) } else { bad2(lbl, `ran [{ran}]`) }
|
||||
}
|
||||
|
||||
# expect_eq on strings: the text is compared, and a failure prints both (the spec fails on purpose)
|
||||
function expect_str_case() -> void {
|
||||
let lbl = "testing_strings.ludic (expect_eq compares strings by content, a null only equal to a null, and prints both on a failure)"
|
||||
|
|
@ -995,6 +1022,7 @@ function cmd_dev_test() -> int {
|
|||
migrate_foreign_case()
|
||||
reject_case("rejected/runtime_type_clash", "PadButton is the runtime's enum", "a program's type named like one of the runtime's is refused")
|
||||
migrate_case()
|
||||
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")
|
||||
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue