Merge branch 'lang/memory-gaps' into lang/foundations

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 17:13:53 +03:00
commit 1315baf332
6 changed files with 62210 additions and 59807 deletions

View file

@ -0,0 +1,40 @@
# arena_pool.ludic - a pool that keeps its records across frames through a generic push (the shape of
# ludic.ui's nd_take and ui_kept). Built with --arena, a record the pool keeps must not come from the
# frame's scratch: run with R3D_ARENA_CHECK=1 (each reset filled with 0xDD) it prints what it prints
# with the arena off (R3D_ARENA=0) - `7 3498`.
program ArenaPool {
property Cell { v: int = 0, tag: string = "" }
property Pool { cells: []Cell = new []Cell, used: int = 0 }
function kept<T>(xs: []T, v: T) -> void { push(xs, v) }
function take(p: Pool, f: int) -> Cell {
if p.used < len(p.cells) {
let c = p.cells[p.used]
p.used += 1
return c
}
let c = new Cell { v: f }
kept(p.cells, c)
p.used += 1
return c
}
function step(p: Pool, f: int) -> int {
p.used = 0
var s = 0
for k in 0 .. (f % 8) {
let c = take(p, f)
c.v = f + k
s += c.v
}
return s
}
entry {
let p = new Pool
for f in 0 .. 500 {
step(p, f)
Mem.frame()
}
var sum = 0
for i in 0 .. len(p.cells) { sum += p.cells[i].v }
print(`{len(p.cells)} {sum}`)
}
}

View file

@ -224,6 +224,16 @@ function es_val(e: Node) -> int {
var i = 0
while i < len(e.a.kids) { es_store(es_ref(e.a.kids[i].a), s); i += 1 }
}
# a field's default is made with the record and held by it (`facts: Queue<F> = queue_new(...)`)
let lay = find_comp(e.s)
if lay != null and lay.kids != null {
var j = 0
while j < len(lay.kids) {
let fd = lay.kids[j]
if fd.a != null and not es_overridden(e, fd.s) { es_store(es_ref(fd.a), s) }
j += 1
}
}
return s
}
if k == E_LIST {
@ -242,6 +252,39 @@ function es_val(e: Node) -> int {
return g_es_heap
}
function es_overridden(e: Node, field: pointer) -> bool {
if e.a == null or e.a.kids == null { return false }
var i = 0
while i < len(e.a.kids) { if (e.a.kids[i].s == field) { return true }; i += 1 }
return false
}
# the function records a call to `nm` reaches: nm itself, or every instance of a generic nm
function es_fns_for(nm: pointer) -> []int {
let out = new []int
let f = es_fn(nm)
if f >= 0 {
push(out, f)
return out
}
let pre = `{nm}$`
var i = 0
while i < len(g_es_fnode) {
let d = g_es_fnode[i]
if d.s != null and str_starts(d.s, pre) { push(out, i) }
i += 1
}
return out
}
function es_keeps_nothing(nm: pointer) -> bool {
if (nm == "quit") or (nm == "exit") or (nm == "panic") or (nm == "assert") or (nm == "expect") or (nm == "expect_eq") or (nm == "expect_near") { return true }
if (nm == "min") or (nm == "max") or (nm == "abs") or (nm == "clamp") or (nm == "sqrt") or (nm == "floor") or (nm == "ceil") or (nm == "round") { return true }
if (nm == "int") or (nm == "float") or (nm == "long") or (nm == "double") or (nm == "fixed") or (nm == "as_int") or (nm == "as_fixed") { return true }
if (nm == "float_bits") or (nm == "float_from_bits") or (nm == "sin") or (nm == "cos") or (nm == "atan2") or (nm == "file_write") or (nm == "file_close") { return true }
return false
}
function es_args_escape(e: Node) -> void {
var i = 0
while i < len(e.kids) { es_escape(es_val(e.kids[i])); i += 1 }
@ -293,26 +336,47 @@ function es_call(e: Node) -> int {
return -1
}
if (nm == "len") { es_args_walk(e); return -1 }
let f = es_fn(nm)
if f >= 0 {
push(g_es_calls_f, g_es_cur)
push(g_es_calls_t, f)
let d = g_es_fnode[f]
if (nm == "view") and len(e.kids) >= 1 { # a view shares its list's storage
let base = es_val(e.kids[0])
var vi = 1
while vi < len(e.kids) { es_val(e.kids[vi]); vi += 1 }
return base
}
# a generic's call names the generic; its instances (ui_kept$UiNode) are what is analysed, and
# the call reaches every one of them
let fs = es_fns_for(nm)
if len(fs) > 0 {
let r = es_new(0)
var fi = 0
while fi < len(fs) {
let f = fs[fi]
push(g_es_calls_f, g_es_cur)
push(g_es_calls_t, f)
es_flow(g_es_fret[f], r)
fi += 1
}
var i = 0
while i < len(e.kids) {
let v = es_ref(e.kids[i])
if i < len(d.kids) and d.kids[i].kind == N_PARAM {
if not es_prim(d.kids[i].ty) { es_flow(v, g_es_fparam[f] + i) }
} else { es_escape(v) }
var fj = 0
while fj < len(fs) {
let d = g_es_fnode[fs[fj]]
if i < len(d.kids) and d.kids[i].kind == N_PARAM {
if not es_prim(d.kids[i].ty) { es_flow(v, g_es_fparam[fs[fj]] + i) }
} else { es_escape(v) }
fj += 1
}
i += 1
}
let r = es_new(0)
es_flow(g_es_fret[f], r)
return r
}
# an intrinsic or a runtime builtin of the emitter's own: it keeps nothing it is handed, and
# what it hands back is its own
es_args_walk(e)
# an intrinsic known to keep nothing it is handed; anything else unknown keeps it all - the
# arena is only as safe as this default
if es_keeps_nothing(nm) {
es_args_walk(e)
return g_es_heap
}
es_args_escape(e)
return g_es_heap
}
if es_is_mem_frame(e) {
@ -541,6 +605,18 @@ function escape_analyse() -> void {
f += 1
}
g_es_cur = -1
# a global's initializer - a state's instance, a module's table - is made to be kept
var gi = 0
while gi < len(prog) {
let gd = prog[gi]
if gd.kind == N_VAR and gd.a != null {
g_es_ln = new []pointer
g_es_lc = new []int
g_es_lt = new []pointer
es_escape(es_ref(gd.a))
}
gi += 1
}
# an @On body keeps nothing it makes unless it stores it; it has no function record of its own
var li = 0
while li < len(g_onlisten) {
@ -557,7 +633,9 @@ function escape_analyse() -> void {
let c = g_es_site_cls[s]
let fi = g_es_site_fn[s]
let spans = fi < 0 or g_es_fspans[fi]
if c >= 0 and (g_es_flag[c] & ES_ESC) == 0 and not spans {
# LOCAL only when it can neither be kept (ESC) nor share storage with what is (HEAP): a push's
# growth into a parameter's list is the list's own buffer, and the list may be a state's
if c >= 0 and (g_es_flag[c] & (ES_ESC | ES_HEAP)) == 0 and not spans {
n.uns = ES_SCRATCH
g_es_local_n += 1
} else {

View file

@ -151,6 +151,7 @@ function deps_frame(f: pointer) -> void {
let d = prog[i]
if d.kind == N_SYS and d.s != null and fr_phase_frame(d.ty) { fr_mark_root(d.s) }
if d.kind == N_FN and d.s != null and str_starts(d.s, "ludic_reduce__") { fr_mark_root(d.s) }
if d.kind == N_FN and d.s != null and d.cm >= 0 { fr_mark_root(d.s) } # a component's own: it runs while its page is open
fr_find_roots(d)
i += 1
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -817,6 +817,17 @@ function alloc_fence_auto_case() -> void {
else { bad2(lbl, `rc={string(rc)} head=[{head}] count=[{through}]`) }
}
# 25.3: a pool that keeps records across frames through a generic push must not take them from the
# frame's scratch - poisoned on every reset it prints what the heap does
function arena_pool_case() -> void {
let lbl = "arena_pool.ludic (--arena: a generic push into a kept pool keeps its records and its buffer on the heap; 0xDD poison changes nothing)"
let b = `{tmp_dir()}/e_arena_pool`
if not shq(`bin/ludicc --arena examples/lang/arena_pool.ludic -o {b} 2>/dev/null`) { bad2(lbl, "did not build"); return }
let poisoned = capture_line(`R3D_ARENA_CHECK=1 R3D_ALLOC_FENCE=off {b} < /dev/null`)
let heap = capture_line(`R3D_ARENA=0 R3D_ALLOC_FENCE=off {b} < /dev/null`)
if (poisoned == "7 3498") and (heap == "7 3498") { ok(lbl) } else { bad2(lbl, `poisoned [{poisoned}] heap [{heap}]`) }
}
# issue #45: `bin/ludic-dev test --coverage`. Compile each test-spec with `--coverage`,
# run it with LUDIC_COVERAGE pointed at a per-file dump, then aggregate the dumps
# into a clean per-file line-coverage report. The instrumentation is flag-gated,
@ -1115,6 +1126,7 @@ function cmd_dev_test() -> int {
feat_case("lang/alloc_fence", "", "frames 300 kept 0 bad 0", "alloc_fence.ludic (25.1: a frame that makes only what it frees or reuses keeps nothing, and the fence passes it)")
alloc_fence_leak_case()
alloc_fence_auto_case()
arena_pool_case()
feat_case("lang/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)")
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")