wip(0.S3): ui blocks, scene on enter/exit, hooks, machines over a state's field, namespace and engine-system injection; a reference out of a read-only state or a module let is read-only; deps counts writes into another module's state; the rejected examples hold states

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 14:49:03 +03:00
parent 4919c9c2fc
commit e7f8ee6b91
31 changed files with 100137 additions and 72476 deletions

View file

@ -79,6 +79,26 @@ 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)
}
# the brace that closes the first block after `at` (strings skipped), or -1
function mgx_close(t: pointer, at: int) -> int {
var i = at
while t[i] != 0 and t[i] != '{' { i += 1 }
var depth = 0
var instr = false
while t[i] != 0 {
let c = t[i]
if instr {
if c == 92 { i += 1 } else if c == 34 { instr = false }
} else if c == 34 { instr = true }
else if c == '{' { depth += 1 }
else if c == '}' {
depth -= 1
if depth == 0 { return i }
}
i += 1
}
return -1
}
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")
@ -167,6 +187,7 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
let sfile = new []pointer
let spos = new []int
let sexp = new []int
let smore = new []int # 1: the state is already declared, and gains fields
let reports = new []pointer
for i in 0 .. len(lines) {
let w = dp_words(lines[i])
@ -188,6 +209,7 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
push(sfile, w[2])
push(spos, s_to_int(w[3]))
push(sexp, s_to_int(w[4]))
if len(w) >= 7 { push(smore, 1) } else { push(smore, 0) }
} 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 {
@ -224,6 +246,19 @@ function mgx_apply(lines: []pointer, dry: bool) -> int {
var block = ""
var at = -1
var indent = ""
if smore[s] == 1 {
# the fields go in before the declaration's closing brace
let t = mgx_text_of(sfile[s])
indent = mgx_indent(t, mgx_line_start(t, spos[s]))
var ff = ""
for ov in 0 .. len(order) {
let v = order[ov]
if vstate[v] == sname[s] { ff = ff + `{indent} {mgx_field(mgx_text_of(vfile[v]), vpos[v], vpos2[v])}\n` }
}
let close = mgx_close(t, spos[s])
if close >= 0 { mgx_edit(sfile[s], mgx_line_start(t, close), 0, ff) }
continue
}
for ov in 0 .. len(order) {
let v = order[ov]
if vstate[v] == sname[s] {

View file

@ -181,7 +181,7 @@ function cmd_test_pkg() -> int {
pt_commit_tag(cp, "v1.0.0")
let proj4 = `{work}/proj4`
pt_write(`{proj4}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/coachpack" "1.0.0"` + nl())
pt_write(`{proj4}/game.ludic`, "program Game {\n import \"example.test/coachpack/coach.ludic\"\n model Player { Score }\n @OnSpawn(Player) handler Init { }\n var frames: int = 0\n handler Boot phase Start { let e = world_spawn(world_model_id(\"Player\")) }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 5 {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n print(world_get(e, P, 0))\n print(Coach.bonus())\n quit()\n }\n }\n}\n")
pt_write(`{proj4}/game.ludic`, "program Game {\n import \"example.test/coachpack/coach.ludic\"\n model Player { Score }\n @OnSpawn(Player) handler Init { }\n state Clock { frames: int = 0 }\n handler Boot phase Start { let e = world_spawn(world_model_id(\"Player\")) }\n handler Run(c: mut Clock) phase Update {\n c.frames = c.frames + 1\n if c.frames >= 5 {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n print(world_get(e, P, 0))\n print(Coach.bonus())\n quit()\n }\n }\n}\n")
if shq(`( cd {proj4} && {envp} {root}/bin/ludic get > {work}/get4.out 2>&1 )`) {
shell(`LUDIC_HOME={root} LUDIC_MODULES={proj4}/ludic_modules {root}/bin/ludicc --headless {proj4}/game.ludic -o {proj4}/game 2>{work}/gc4.err`)
if file_exists(`{proj4}/game`) {
@ -213,7 +213,7 @@ function cmd_test_pkg() -> int {
let proj3 = `{work}/proj3`
pt_write(`{proj3}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/manapack" "1.0.0"` + nl())
pt_write(`{proj3}/game.ludic`, "program Game {\n property Dummy { x: int = 0 }\n model Ent { Dummy }\n @OnSpawn(Ent) handler Init { }\n extern function mana_bonus() -> int = \"fn_mana_bonus\"\n var frames: int = 0\n handler Boot phase Start {\n let e = world_spawn(world_model_id(\"Ent\"))\n let M = world_prop_id(\"Mana\")\n world_attach_dyn(e, M)\n world_set(e, M, 0, 0)\n }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 4 {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n print(world_get(e, M, 0))\n print(mana_bonus())\n quit()\n }\n }\n}\n")
pt_write(`{proj3}/game.ludic`, "program Game {\n property Dummy { x: int = 0 }\n model Ent { Dummy }\n @OnSpawn(Ent) handler Init { }\n extern function mana_bonus() -> int = \"fn_mana_bonus\"\n state Clock { frames: int = 0 }\n handler Boot phase Start {\n let e = world_spawn(world_model_id(\"Ent\"))\n let M = world_prop_id(\"Mana\")\n world_attach_dyn(e, M)\n world_set(e, M, 0, 0)\n }\n handler Run(c: mut Clock) phase Update {\n c.frames = c.frames + 1\n if c.frames >= 4 {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n print(world_get(e, M, 0))\n print(mana_bonus())\n quit()\n }\n }\n}\n")
if shq(`( cd {proj3} && {envp} {root}/bin/ludic get > {work}/get3.out 2>&1 )`) and file_exists(`{proj3}/ludic_modules/example.test/manapack/lib/{tgt}/libmanapack.dylib`) {
ok("ludic get fetches + links a prebuilt (kind prebuilt) package for the target")
} else { bad2("prebuilt ludic get failed", capture_line(`tail -1 {work}/get3.out`)) }

View file

@ -517,7 +517,7 @@ function migrate_case() -> void {
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 + " 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 }
@ -545,9 +545,9 @@ function test_dir_case() -> void {
let work = `{tmp_dir()}/testdir`
let root = capture_line("pwd")
shell(`rm -rf {work} && mkdir -p {work}/pk/tests {work}/pk/src/deep`)
var a = "program State {\n var counter: int = 0\n var seen: []int = new []int\n"
a = a + " test \"first\" {\n counter += 1\n push(seen, 1)\n expect_eq(counter, 1)\n expect_eq(len(seen), 1)\n }\n"
a = a + " test \"second\" {\n counter += 1\n push(seen, 2)\n expect_eq(counter, 1)\n expect_eq(len(seen), 1)\n }\n}\n"
var a = "program State {\n state Tally {\n counter: int = 0\n seen: []int = new []int\n }\n"
a = a + " test \"first\" (t: mut Tally) {\n t.counter += 1\n push(t.seen, 1)\n expect_eq(t.counter, 1)\n expect_eq(len(t.seen), 1)\n }\n"
a = a + " test \"second\" (t: mut Tally) {\n t.counter += 1\n push(t.seen, 2)\n expect_eq(t.counter, 1)\n expect_eq(len(t.seen), 1)\n }\n}\n"
write_file(`{work}/pk/tests/state.ludic`, a)
write_file(`{work}/pk/src/deep/sums_test.ludic`, "program Sums {\n test \"adds\" {\n expect_eq(2 + 2, 4)\n }\n test \"wrong\" {\n expect_eq(2 + 2, 5)\n }\n}\n")
write_file(`{work}/pk/src/helper.ludic`, "program NotATest {\n entry { exit(3) }\n}\n")
@ -889,7 +889,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/port_var_args", "only a member that takes nothing can be bound to a variable", "a member with parameters binds a function, not a variable")
reject_case("rejected/port_new", "Clock is a port: it is filled once with 'bind Clock", "a port is bound, not made with new")
reject_case("rejected/port_private", "Clock is private to module clock", "whoever binds a port has to see it")
reject_case("rejected/uses_package", "mech uses ludic_inventory.inv_count", "a package's module counts under `uses` like any other")
reject_case("rejected/uses_package", "mech uses ludic_inventory.inv_limit", "a package's module counts under `uses` like any other")
reject_case("rejected/uses_render3d", "mech uses ludic_render3d.PI", "a package with no module line is still named under `uses`")
feat_case("modules/private_twins", "", "105 41 7", "private_twins.ludic (L3: two modules' private seed, dice and LIMIT, and the program's seed, do not collide)")
feat_case("modules/private_kinds", "", "7 hare", "private_kinds.ludic (L3: two modules' private record Catch and event Landed do not collide)")