feat(lang): 0.S - a reference out of a read-only state is read-only (named so), a program's type named like the runtime's is refused (PadButton), tests for both, the migrate case covers lets; changeset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 16:17:03 +03:00
parent 02448e176c
commit c167ecc714
10 changed files with 13858 additions and 13042 deletions

View file

@ -412,7 +412,11 @@ methods would take that method's calls - `Random.range` is `rng_range`, so a pac
`rng_range(a, b, c)` would receive every `Random.range(1, 6)`. Where the program calls that method,
the function is refused (`rng_range is the engine's Random.range, which this program calls (...),
and every such call would reach this function instead; choose another name`), as a function named
like a compiler built-in (`run`, `exit`) or like one of the engine runtime's own functions is.
like a compiler built-in (`run`, `exit`) or like one of the engine runtime's own functions is. So
is a type - a property, record, enum, event or state - named like one the runtime declares in a file
the program uses: `property PadButton` beside the runtime's `enum PadButton` used to compile and
then fail in clang, where the two layouts met (`PadButton is the runtime's enum
(runtime/native/input.ludic); choose another name for this property`).
### Registries (`registry`, `def`)

27
changes/state.md Normal file
View file

@ -0,0 +1,27 @@
bump: major
type: feature
**No function writes a global: a module's changing data is a `state`.** `state Hiker { hips: int =
-1 }` is a record with exactly one instance, which no code can name; it reaches code as a parameter,
`h: mut Hiker` to change it or `h: Hiker` to read it, and the runtime supplies it at the entry points
(`entry (h: mut Hiker)`, a handler's, a listener's or a lifecycle hook's parameters, a scene's `on
enter (h: mut Hiker)`, a test's, a `ui` block's `Hiker.hips`, a function value - a system's, a port
bind's, a package's callback - an engine system, a namespace method's target and every call into the
runtime). A module-level `var` is refused; a module-level `let` is immutable all the way down. A
read-only state is checked where it is written - an assignment or a `push` through it or through a
local holding part of it, passing it where `mut` is wanted, a `machine` over its field - and so is a
module-level `let`. Each test block gets fresh states. A program's type named like one the runtime
declares (`property PadButton` beside the runtime's `enum PadButton`) is refused instead of failing
in clang.
`ludic migrate state [file|dir...] [--runtime] [--dry-run]` moves programs there: a var nothing
writes becomes a `let`, each module's other vars one state (`FishingState`, `Render3dState`,
`SceneDemoState`), every reference `fishing_st.name`, each function's states - to a fixed point over
what it calls - its leading parameters, `mut` where anything below it writes, and each entry point
declares them; the plans of every program given are merged before a file is edited. gpp's 35
mechanic packages, ludic.ui, ludic.render3d, every example, the docs' fences and the runtime were
moved with it. The toolchain's own programs build with `--globals` until they move too.
Outside the runtime, the drawable's size and scale, the screen's framebuffer and the tilemap's tile
size are read and set through its functions (`gl_width()`, `gl_set_drawable(w, h)`,
`gl_screen_fbo()`, `gl_pixel_scale()`, `rt_map_tile_size()`), and ludic.ui's render3d backend
installs itself through the `UiRenderers` registry.

View file

@ -0,0 +1,9 @@
# 0.S: a module-level let is immutable all the way down, through a local that holds it too
program ModuleLetAlias {
let LIMITS: []int = [1, 2]
entry {
let l = LIMITS
push(l, 3)
print(len(LIMITS))
}
}

View file

@ -0,0 +1,10 @@
# a program's type named like one the runtime declares is refused (it compiled, and the two layouts
# met in the IR): the runtime's input has `enum PadButton`
program RuntimeTypeClash {
property PadButton { code: int = 0 }
entry {
let b = new PadButton
print(b.code)
print(Input.pad_button(0, 1))
}
}

View file

@ -0,0 +1,13 @@
# 0.S: a reference read out of a read-only state is read-only too
program StateAliasReadonly {
property Row { n: int = 0 }
state Table { rows: []Row = new []Row }
function bump(t: Table) -> void {
let r = t.rows[0]
r.n = 1
}
entry (t: mut Table) {
push(t.rows, new Row)
bump(t)
}
}

View file

@ -81,6 +81,10 @@ function ck_write_check(t: Node, what: pointer) -> void {
mg_need(g_mg_cur, mg_state_named(ck_ro_st[li]), true) # 0.S2: it becomes `mut`
return
}
if ck_is_ro(li) and li < len(ck_ro_st) and ck_ro_st[li] != null and not (ck_ro_st[li] == ck_tys[li]) {
ck_err("state", t, `{what}: {r.s} holds part of a read-only {ck_ro_st[li]}; take the state as mut {ck_ro_st[li]} to change it`)
return
}
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`)
}

View file

@ -208,9 +208,29 @@ function ck_runtime_shadows() -> void {
let u = ck_fn(d.s)
if u != null and u != d { ck_err("shadow", u, `{d.s} is also a function of the engine runtime ({d.file}); choose another name`) }
}
# a type the runtime declares - `property PadButton` beside its `enum PadButton` compiled,
# and the two layouts met in the IR
if ck_type_kind(d) != "" {
var k = 0
while k < g_prog_user_end and k < len(prog) {
let o = prog[k]
if ck_type_kind(o) != "" and (o.s == d.s) {
ck_err("shadow", o, `{d.s} is the runtime's {ck_type_kind(d)} ({d.file}); choose another name for this {ck_type_kind(o)}`)
}
k += 1
}
}
r += 1
}
}
function ck_type_kind(d: Node) -> pointer {
if d.kind == N_ENUM { return "enum" }
if d.kind == N_COMP and d.uns == 2 { return "state" }
if d.kind == N_COMP { return "property" }
if d.kind == N_STRUCT { return "record" }
if d.kind == N_EVENT { return "event" }
return ""
}
function ck_hooks(hs: []Node) -> void {
var i = 0
while i < len(hs) {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -514,15 +514,15 @@ 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"
var src = "program Demo {\n var count: int = 0\n var names: []string = new []string # the names\n var title: string = \"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()} {title}, 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)") {
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)") or not s_contains(got, "let title: string") {
bad2(lbl, `rewrote it as [{got}]`)
return
}
@ -908,6 +908,9 @@ function cmd_dev_test() -> int {
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")
reject_case("rejected/state_alias_readonly", "r holds part of a read-only Table; take the state as mut Table", "a reference out of a read-only state is read-only too")
reject_case("rejected/module_let_alias", "l holds part of LIMITS, which is module-level and immutable all the way down", "a module-level let is immutable through a local that holds it")
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()
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)")