From ea5eedb5a5c845dcb1a53bb9ce9fd7339a40d2f4 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 01:29:02 +0300 Subject: [PATCH] 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 --- packages/ludic.ui/backend.ludic | 2 ++ packages/ludic.ui/build_class.ludic | 7 +++-- packages/ludic.ui/class.ludic | 11 ++++++-- packages/ludic.ui/elem.ludic | 13 +++++----- packages/ludic.ui/eval.ludic | 6 +---- packages/ludic.ui/eval_text.ludic | 38 ++++++++++++++++++++++++++++ packages/ludic.ui/index.ludic | 1 + packages/ludic.ui/memo.ludic | 3 ++- packages/ludic.ui/reload.ludic | 11 +++++++- packages/ludic.ui/select_match.ludic | 2 +- 10 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 packages/ludic.ui/eval_text.ludic diff --git a/packages/ludic.ui/backend.ludic b/packages/ludic.ui/backend.ludic index ec09fc3f..72a562e7 100644 --- a/packages/ludic.ui/backend.ludic +++ b/packages/ludic.ui/backend.ludic @@ -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 diff --git a/packages/ludic.ui/build_class.ludic b/packages/ludic.ui/build_class.ludic index 3106f8f6..ea012ade 100644 --- a/packages/ludic.ui/build_class.ludic +++ b/packages/ludic.ui/build_class.ludic @@ -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) } } diff --git a/packages/ludic.ui/class.ludic b/packages/ludic.ui/class.ludic index 2fb6e314..9eb9bc99 100644 --- a/packages/ludic.ui/class.ludic +++ b/packages/ludic.ui/class.ludic @@ -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 diff --git a/packages/ludic.ui/elem.ludic b/packages/ludic.ui/elem.ludic index cbaf014c..20a403dd 100644 --- a/packages/ludic.ui/elem.ludic +++ b/packages/ludic.ui/elem.ludic @@ -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) } } diff --git a/packages/ludic.ui/eval.ludic b/packages/ludic.ui/eval.ludic index e932ecc3..62c8ac58 100644 --- a/packages/ludic.ui/eval.ludic +++ b/packages/ludic.ui/eval.ludic @@ -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 diff --git a/packages/ludic.ui/eval_text.ludic b/packages/ludic.ui/eval_text.ludic new file mode 100644 index 00000000..3431ccb3 --- /dev/null +++ b/packages/ludic.ui/eval_text.ludic @@ -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) +} diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 07401b9c..76ee8c46 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -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" diff --git a/packages/ludic.ui/memo.ludic b/packages/ludic.ui/memo.ludic index 081c74ff..02343f57 100644 --- a/packages/ludic.ui/memo.ludic +++ b/packages/ludic.ui/memo.ludic @@ -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) diff --git a/packages/ludic.ui/reload.ludic b/packages/ludic.ui/reload.ludic index 436474b8..8efe0d19 100644 --- a/packages/ludic.ui/reload.ludic +++ b/packages/ludic.ui/reload.ludic @@ -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) } +} diff --git a/packages/ludic.ui/select_match.ludic b/packages/ludic.ui/select_match.ludic index 629f08e9..7bc007fc 100644 --- a/packages/ludic.ui/select_match.ludic +++ b/packages/ludic.ui/select_match.ludic @@ -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 }