ludic.ui: a hole's text found by its pieces in the memo (built only when it first reads that way), avals left for the attributes read as text, a class's [root] made once, and a dev build's reload lets go of every file it read only to compare

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 01:29:02 +03:00
parent 6c663cc87b
commit ea5eedb5a5
10 changed files with 76 additions and 18 deletions

View file

@ -43,6 +43,8 @@ export state UiState {
ev_depth: int = 0 # eval.ludic: a call's arguments, in lists kept one a depth
ev_args: []Val = new []Val
cv_texts: []string = new []string # units_pieces.ludic: a value's var() pieces, kept
tx_pieces: [][]string = new [][]string # eval_text.ludic: a text's pieces, a list a depth
tx_depth: int = 0
ca_pool: []CaVal = new []CaVal # calc.ludic: an evaluation's terms, used again by the next
ca_used: int = 0
ca_args: [][]CaVal = new [][]CaVal

View file

@ -25,8 +25,11 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
lc_note(ui_st, key, k.on_keys, k.on_acts, e)
let at = len(out)
let root = new []UiTpl
push(root, cls.root)
if cls.roots == null or len(cls.roots) != 1 or cls.roots[0] != cls.root {
cls.roots = new []UiTpl # its template as a list of one, made once (again after a reload)
push(cls.roots, cls.root)
}
let root = cls.roots
bd_kids(ui_st, root, ce, key, out)
if len(out) > at { bd_pass_down(ui_st, out[at], k, e) }
}

View file

@ -16,6 +16,7 @@ export property UiClass {
seen_xml: string = "" # the files as a developer's build last found them on disk
seen_lss: string = ""
use: UiTpl = null # screen_class.ludic: the element that shows it as a screen, made once
roots: []UiTpl = null # build_class.ludic: [root], for bd_kids
none: Val = null # and the empty model it is shown with
}
export function ui_register(ui_st: mut UiState, k: UiClass) -> int {
@ -52,8 +53,14 @@ function uc_reload(ui_st: mut UiState) -> bool {
if k.xml_path != "" and Fs.exists(k.xml_path) {
let x = lib_text(ui_st, k.xml_path)
var l = ""
if k.lss_path != "" and Fs.exists(k.lss_path) { l = lib_text(ui_st, k.lss_path) }
if k.seen_xml != "" and (x != k.seen_xml or l != k.seen_lss) {
let has_l = k.lss_path != "" and Fs.exists(k.lss_path)
if has_l { l = lib_text(ui_st, k.lss_path) }
if k.seen_xml != "" and x == k.seen_xml and l == k.seen_lss {
rl_let_go(x) # read only to compare, and the same: nothing keeps it (a dev build
if has_l { rl_let_go(l) } # asks every second, and each read was kept for good)
continue
}
if k.seen_xml != "" {
k.xml = x
k.lss = l
k.root = null

View file

@ -52,10 +52,11 @@ function el_text(ui_st: mut UiState, n: UiNode, s: string) -> void {
push(n.children, t)
}
# id, class, hidden, disabled and src say what the element is; the rest are applied after the rules
# an attribute's text is worked out only where it is read as text (the rest keep their Value; a selector
# that asks for one reads it then) - a float's text written per element per frame was a leak
function el_keep(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
let s = vl_text_u(ui_st, v)
if key == "id" { n.id = s } else if key == "class" {
let parts = mm_split(ui_st, s, 32)
if key == "id" { n.id = vl_text_u(ui_st, v) } else if key == "class" {
let parts = mm_split(ui_st, vl_text_u(ui_st, v), 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
}
@ -63,12 +64,12 @@ function el_keep(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
n.popover = true
n.pos = 2
push(n.akeys, key)
push(n.avals, s)
push(n.avals, "")
push(n.vals, v)
} else if key == "tabindex" { n.focusable = s != "-1" } else if key == "hidden" { n.hidden = el_flag(v) } else if key == "disabled" { n.enabled = not el_flag(v) } else if key == "src" { n.src = s } else {
} else if key == "tabindex" { n.focusable = vl_text_u(ui_st, v) != "-1" } else if key == "hidden" { n.hidden = el_flag(v) } else if key == "disabled" { n.enabled = not el_flag(v) } else if key == "src" { n.src = vl_text_u(ui_st, v) } else {
if key == "enabled" { n.enabled = el_flag(v) }
push(n.akeys, key)
push(n.avals, s)
push(n.avals, "")
push(n.vals, v)
}
}

View file

@ -25,11 +25,7 @@ function ui_eval(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
if vl_truthy(ui_eval(ui_st, x.a, e)) { return ui_eval(ui_st, x.b, e) }
return ui_eval(ui_st, x.c, e)
}
if x.op == X_TEXT {
var out = ""
for i in 0 .. len(x.args) { out = out + vl_text_u(ui_st, ui_eval(ui_st, x.args[i], e)) }
return uv_str(ui_st, out)
}
if x.op == X_TEXT { return ev_text(ui_st, x, e) }
return vl_binary(ui_st, x.s, ui_eval(ui_st, x.a, e), ui_eval(ui_st, x.b, e))
}
# a call: len() and range() are the template's own, anything else is the view's

View file

@ -0,0 +1,38 @@
# eval_text.ludic - a text with holes ("{n} left") worked out: its pieces are found in the memo by
# comparing in place, so the text it was last frame (and the frame before) is its kept Value, and a
# text is built only the first time it reads that way. The memo is bounded; past it a text is built as ever
function ev_text(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
let depth = ui_st.tx_depth # a hole may hold a text of its own: a kept list of pieces a depth
while len(ui_st.tx_pieces) <= depth { push(ui_st.tx_pieces, new []string) }
let ps = ui_st.tx_pieces[depth]
ui_st.tx_depth = depth + 1
List.clear(ps)
for i in 0 .. len(x.args) { push(ps, vl_text_u(ui_st, ui_eval(ui_st, x.args[i], e))) }
ui_st.tx_depth = depth
let m = mm_of(ui_st)
var h = 216613626 ^ (MM_TEXT & 255)
for p in 0 .. len(ps) {
let t = ps[p]
for i in 0 .. len(t) { h = ((h ^ t[i]) * 16777619) & 2147483647 }
}
var at = h & (MM_CAP - 1)
while m.slots[at] != 0 {
let i = m.slots[at] - 1
if m.tags[i] == MM_TEXT and tx_same(ps, m.keys[i]) { return m.vals[i] }
at = (at + 1) & (MM_CAP - 1)
}
var out = ""
for p in 0 .. len(ps) { out = out + ps[p] }
let j = mm_add(m, MM_TEXT, out)
if j < 0 { return uv_str(ui_st, out) }
m.vals[j] = Value.str(out)
return m.vals[j]
}
function tx_same(ps: []string, k: string) -> bool {
var at = 0
for p in 0 .. len(ps) {
if not sat(k, at, ps[p]) { return false }
at += len(ps[p])
}
return at == len(k)
}

View file

@ -12,6 +12,7 @@ import "pool_reset.ludic"
import "pool_reset_more.ludic"
import "memo.ludic"
import "memo_join.ludic"
import "eval_text.ludic"
import "units_pieces.ludic"
import "expr.ludic"
import "expr_parse.ludic"

View file

@ -2,7 +2,7 @@
# split into its words, a gradient, a key joined from its parent's. Ludic frees nothing, so parsing
# "2px 6px" per element per frame was a leak; the table is bounded, and past it a result is made
# (and lost) as before rather than the table growing without end.
const MM_CAP: int = 8192 # slots; at most three quarters are used
const MM_CAP: int = 32768 # slots; at most three quarters are used
export property UiMemo {
slots: words = null # index + 1 into keys, 0 empty
keys: []string = null # the text a result was made from (or the joined text itself)
@ -24,6 +24,7 @@ const MM_VARS: int = -6 # cs_vars_in: keys[i] is the text with every
const MM_VTPL: int = -7 # a value's var()s parsed (units_pieces.ludic)
const MM_COLOR: int = -8 # ui_color: n1 the colour, n2 its alpha
const MM_BIMG: int = -9 # border-image: texts the picture, lists what follows its url()
const MM_TEXT: int = -10 # a text with holes, keys[i] the text itself (eval_text.ludic)
function mm_new() -> UiMemo {
let m = new UiMemo
m.slots = words(MM_CAP)

View file

@ -5,7 +5,11 @@ export function ui_reload(ui_st: mut UiState) -> bool {
var changed = uc_reload(ui_st)
for i in 0 .. len(ui_st.lib_files) {
let f = ui_st.lib_files[i]
if f.disk and lib_text(ui_st, f.path) != f.src { changed = true }
if f.disk {
let now = lib_text(ui_st, f.path)
if now != f.src { changed = true }
rl_let_go(now) # read only to compare; a change is read again below
}
}
if not changed { return false }
ui_st.ui_errs = new []string
@ -31,3 +35,8 @@ export function ui_reload(ui_st: mut UiState) -> bool {
print(`ui: templates reloaded`)
return true
}
# a text read from disk only to be compared, given back (never a literal: lib_text's "" is not freed)
function rl_let_go(s: string) -> void {
if s == null or len(s) == 0 { return }
unsafe { free(s) }
}

View file

@ -58,7 +58,7 @@ function sel_has(xs: []string, x: string) -> bool {
function sel_attr_of(n: UiNode, key: string) -> string {
if n.akeys == null { return null }
for i in 0 .. len(n.akeys) {
if n.akeys[i] == key { return n.avals[i] }
if n.akeys[i] == key { return vl_text(n.vals[i]) }
}
return null
}