From 46888b67164f94bb3cc62a92e6dabc642e887b31 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 23:43:25 +0300 Subject: [PATCH] runtime, ludic.ui: a scalar Value is one allocation, not three (lists only for a list or an object); layout and cascade lists kept a depth; small numbers' text made once - benchmark 63 -> 36 KB a frame Co-Authored-By: Claude Opus 5.5 --- packages/ludic.ui/attr.ludic | 6 ++-- packages/ludic.ui/attr_box.ludic | 4 +-- packages/ludic.ui/attr_edge.ludic | 2 +- packages/ludic.ui/attr_inset.ludic | 2 +- packages/ludic.ui/attr_more.ludic | 4 +-- packages/ludic.ui/attr_place.ludic | 2 +- packages/ludic.ui/backend.ludic | 6 ++++ packages/ludic.ui/cascade_merge.ludic | 9 ++++-- packages/ludic.ui/elem.ludic | 6 ++-- packages/ludic.ui/frame.ludic | 2 +- packages/ludic.ui/layout_run.ludic | 17 +++++++++-- packages/ludic.ui/layout_shrink.ludic | 5 ++-- packages/ludic.ui/values.ludic | 11 +++++++ runtime/native/value.ludic | 41 +++++++++++++++++++-------- 14 files changed, 84 insertions(+), 33 deletions(-) diff --git a/packages/ludic.ui/attr.ludic b/packages/ludic.ui/attr.ludic index a5e13b1d..7abacd78 100644 --- a/packages/ludic.ui/attr.ludic +++ b/packages/ludic.ui/attr.ludic @@ -12,7 +12,7 @@ function el_apply(ui_st: mut UiState, n: UiNode, key: string, v0: Val) -> bool { ui_st.el_cur = n ui_st.el_key = k if len(k) > 2 and sat(k, 0, "--") { - cs_var_set(n, k, vl_text(v0)) + cs_var_set(n, k, vl_text_u(ui_st, v0)) return true } let v = cs_vars_in(ui_st, n, v0) @@ -48,7 +48,7 @@ function el_size(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { return true } function el_flex(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) if key == "dir" { n.dir = UI_COL if s == "row" { n.dir = UI_ROW } @@ -56,7 +56,7 @@ function el_flex(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { return true } function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) if key == "size" { n.size = el_len(ui_st, v) } else if key == "color" { n.fg = ui_color(ui_st, s) n.fg_a = ui_st.cl_alpha diff --git a/packages/ludic.ui/attr_box.ludic b/packages/ludic.ui/attr_box.ludic index edc37eb1..16f22735 100644 --- a/packages/ludic.ui/attr_box.ludic +++ b/packages/ludic.ui/attr_box.ludic @@ -23,7 +23,7 @@ function el_sides(ui_st: mut UiState, v: Val) -> void { ui_st.el_s0 = el_px(ui_st, Value.as_float(v)) n = 1 } else { - let words = mm_words(ui_st, vl_text(v)) + let words = mm_words(ui_st, vl_text_u(ui_st, v)) for i in 0 .. len(words) { if len(words[i]) > 0 and n < 4 { el_side_set(ui_st, n, el_word_len(ui_st, words[i])) @@ -43,7 +43,7 @@ function el_side_set(ui_st: mut UiState, i: int, v: float) -> void { # a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's) function el_len(ui_st: mut UiState, v: Val) -> float { if vl_num(v) { return el_px(ui_st, Value.as_float(v)) } - let s = vl_text(v) + let s = vl_text_u(ui_st, v) if s == "fill" { return UI_FILL } if ca_is(s) { return el_calc_len(ui_st, s) } if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 } diff --git a/packages/ludic.ui/attr_edge.ludic b/packages/ludic.ui/attr_edge.ludic index 51e4e09b..62df9758 100644 --- a/packages/ludic.ui/attr_edge.ludic +++ b/packages/ludic.ui/attr_edge.ludic @@ -1,6 +1,6 @@ # attr_edge.ludic - border-radius, outline (and its width, colour and offset), scrollbar-color function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) if key == "border-radius" { n.radius = el_len(ui_st, v) } else if key == "outline" { el_outline(ui_st, n, s) } else if key == "outline-width" { n.outline_w = el_len(ui_st, v) } else if key == "outline-offset" { n.outline_off = el_len(ui_st, v) } else if key == "outline-color" { n.outline_c = ui_color(ui_st, s) n.outline_a = ui_st.cl_alpha diff --git a/packages/ludic.ui/attr_inset.ludic b/packages/ludic.ui/attr_inset.ludic index b33af5a1..bb6eb47f 100644 --- a/packages/ludic.ui/attr_inset.ludic +++ b/packages/ludic.ui/attr_inset.ludic @@ -2,7 +2,7 @@ # percentage is kept apart and taken of the containing block's width (left, right) or height (top, # bottom) when the element is placed function el_inset(ui_st: mut UiState, n: UiNode, side: int, v: Val) -> float { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) var pct = 0.0 var px = UI_AUTO if s != "auto" { diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic index df55e8bb..bf00a328 100644 --- a/packages/ludic.ui/attr_more.ludic +++ b/packages/ludic.ui/attr_more.ludic @@ -1,13 +1,13 @@ # attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex # shorthand, flex-shrink, order, text-shadow, pointer-events, text-fit and line-height (text_fit.ludic) function el_more(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(ui_st, n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(ui_st, n, s) } else if key == "line-height" { el_line_h(ui_st, n, s) } else { return el_fit_props(ui_st, n, key, s) } return true } # width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none) function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) var fixed = UI_AUTO var pct = 0.0 var off = 0.0 diff --git a/packages/ludic.ui/attr_place.ludic b/packages/ludic.ui/attr_place.ludic index 31263c8e..8abefc97 100644 --- a/packages/ludic.ui/attr_place.ludic +++ b/packages/ludic.ui/attr_place.ludic @@ -1,6 +1,6 @@ # attr_place.ludic - position and its insets, z-index, overflow, white-space and text-overflow function el_place_prop(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { - let s = vl_text(v) + let s = vl_text_u(ui_st, v) if key == "position" { n.pos = 0 if s == "relative" { n.pos = 1 } diff --git a/packages/ludic.ui/backend.ludic b/packages/ludic.ui/backend.ludic index 74fe587d..1fc7bc7f 100644 --- a/packages/ludic.ui/backend.ludic +++ b/packages/ludic.ui/backend.ludic @@ -36,6 +36,12 @@ export state UiState { pl_cur: UiPool = null mm: UiMemo = null # memo.ludic: splits, gradients and keys by their text mm_ints: []string = null + ly_depth: int = 0 # layout_run.ludic: the lists a line lays out with, one a depth + ly_sizes: [][]float = new [][]float + ly_frozen: [][]bool = new [][]bool + cs_rules: []UiRule = new []UiRule # cascade_merge.ludic: a component root's rules, gathered + cs_envs: []UiEnv = new []UiEnv + cs_seen: []UiSheet = new []UiSheet an_frames: []UiKeyframes = new []UiKeyframes an_keys: []string = new []string an_start: []float = new []float diff --git a/packages/ludic.ui/cascade_merge.ludic b/packages/ludic.ui/cascade_merge.ludic index 1210b945..0054543f 100644 --- a/packages/ludic.ui/cascade_merge.ludic +++ b/packages/ludic.ui/cascade_merge.ludic @@ -3,9 +3,12 @@ # specificity as one sheet's are. On a tie the user's rule wins (the outermost user's last), as a # later rule does. Then `order`, which rearranges a flex box's children without touching the tree. function cs_merged(ui_st: mut UiState, n: UiNode) -> void { - let rules = new []UiRule - let envs = new []UiEnv - let seen = new []UiSheet + let rules = ui_st.cs_rules # kept and emptied: a merge runs once an element, never inside another + let envs = ui_st.cs_envs + let seen = ui_st.cs_seen + List.clear(rules) + List.clear(envs) + List.clear(seen) if n.sheet != null { cs_gather(ui_st, n, n.sheet, n.env, rules, envs, seen) } for i in 0 .. len(n.ups) { if n.ups[i].sheet != null { cs_gather(ui_st, n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) } diff --git a/packages/ludic.ui/elem.ludic b/packages/ludic.ui/elem.ludic index e8bb9d3c..fb1b32c0 100644 --- a/packages/ludic.ui/elem.ludic +++ b/packages/ludic.ui/elem.ludic @@ -11,7 +11,7 @@ function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode n.sheet = k.sheet n.style = k.style el_kind(ui_st, n, k.tag) - for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) } + for i in 0 .. len(k.keys) { el_keep(ui_st, n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) } n.hovered = fr_was_hovered(ui_st, key) n.on_names = k.on_keys n.on_acts = k.on_acts @@ -52,8 +52,8 @@ 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 -function el_keep(n: UiNode, key: string, v: Val) -> void { - let s = vl_text(v) +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 = lss_split(s, 32) for i in 0 .. len(parts) { diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic index 4ff3e98b..152e8bad 100644 --- a/packages/ludic.ui/frame.ludic +++ b/packages/ludic.ui/frame.ludic @@ -51,7 +51,7 @@ export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> Ui root.sheet = t.sheet root.tag = "screen" for i in 0 .. len(t.keys) { - if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) } + if t.keys[i] != "name" { el_keep(ui_st, root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) } } bd_kids(ui_st, t.kids, e, screen, root.children) cs_tree(ui_st, root) diff --git a/packages/ludic.ui/layout_run.ludic b/packages/ludic.ui/layout_run.ludic index 7bcc2b51..5bc9fb62 100644 --- a/packages/ludic.ui/layout_run.ludic +++ b/packages/ludic.ui/layout_run.ludic @@ -41,7 +41,9 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f var used = 0.0 var grows = 0.0 var count = 0 - let sizes = new []float + let depth = ui_st.ly_depth + ui_st.ly_depth += 1 + let sizes = ly_scratch(ui_st, depth) for i in from .. to { push(sizes, ly_basis(n.children[i], n.dir, main_room)) if n.children[i].pos < 2 { @@ -52,7 +54,7 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f } used = used + n.gap * float(Math.max(count - 1, 0)) let free = main_room - used - if free < 0.0 { ly_shrink_line(n, from, sizes, 0.0 - free) } + if free < 0.0 { ly_shrink_line(ui_st, depth, n, from, sizes, 0.0 - free) } ly_justify(ui_st, n, free, count, grows) let extra = ui_st.ly_extra var at = ui_st.ly_lead @@ -65,6 +67,17 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f if c.pos == 1 { ly_nudge(n, c) } at = at + m + n.gap + extra } + ui_st.ly_depth = depth +} +# a line's sizes, and its shrink's frozen flags, in lists kept one a depth (a line's children lay out +# their own lines while it is still placing them), emptied rather than made anew +function ly_scratch(ui_st: mut UiState, depth: int) -> []float { + while len(ui_st.ly_sizes) <= depth { + push(ui_st.ly_sizes, new []float) + push(ui_st.ly_frozen, new []bool) + } + List.clear(ui_st.ly_sizes[depth]) + return ui_st.ly_sizes[depth] } # one child: `m` along the run and its own size (or the whole room, stretched) across, less margins function ly_place_child(ui_st: mut UiState, n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void { diff --git a/packages/ludic.ui/layout_shrink.ludic b/packages/ludic.ui/layout_shrink.ludic index 884ea78b..b006cd36 100644 --- a/packages/ludic.ui/layout_shrink.ludic +++ b/packages/ludic.ui/layout_shrink.ludic @@ -23,9 +23,10 @@ function ly_min_h(c: UiNode) -> float { return c.mh } # `sizes` (the children from..to) shrunk by `over` in all -function ly_shrink_line(n: UiNode, from: int, sizes: []float, over0: float) -> void { +function ly_shrink_line(ui_st: mut UiState, depth: int, n: UiNode, from: int, sizes: []float, over0: float) -> void { var over = over0 - let frozen = new []bool + let frozen = ui_st.ly_frozen[depth] + List.clear(frozen) for k in 0 .. len(sizes) { push(frozen, n.children[from + k].pos >= 2) } for pass in 0 .. 8 { var wsum = 0.0 diff --git a/packages/ludic.ui/values.ludic b/packages/ludic.ui/values.ludic index 67e4ffd9..32127bbb 100644 --- a/packages/ludic.ui/values.ludic +++ b/packages/ludic.ui/values.ludic @@ -71,3 +71,14 @@ function vl_binary(op: string, a: Val, b: Val) -> Val { # a value as text, whatever kind it is: a component's string prop given a number (label="{3}") reads # "3", not nothing export function ui_val_text(v: Val) -> string { return vl_text(v) } +# vl_text for what is asked per element per frame: a small whole number's text comes from the table +# made once (mm_int) rather than being written out again +function vl_text_u(ui_st: mut UiState, v: Val) -> string { + let k = Value.kind(v) + if k == 1 { return mm_int(ui_st, Value.as_int(v)) } + if k == 2 or k == 7 { + let f = Value.as_float(v) + if f == float(int(f)) { return mm_int(ui_st, int(f)) } + } + return vl_text(v) +} diff --git a/runtime/native/value.ludic b/runtime/native/value.ludic index cc3e4c0b..db160c12 100644 --- a/runtime/native/value.ludic +++ b/runtime/native/value.ludic @@ -24,12 +24,22 @@ property Val { kids: []Val } +# a number, a string or a null is one record; only a list or an object carries lists (made when first +# filled) - three allocations for every scalar was most of what a UI frame made and never gave back function value_new(tag: int) -> Val { let v = new Val v.tag = tag + if tag == 5 or tag == 6 { value_lists(v) } + return v +} +function value_lists(v: Val) -> void { + if v.kids != null { return } v.keys = new []pointer v.kids = new []Val - return v +} +function value_n(v: Val) -> int { + if v.kids == null { return 0 } + return len(v.kids) } # --- constructors ----------------------------------------------------------- @@ -43,8 +53,13 @@ function value_float(x: float) -> Val { let v = value_new(7); v.num = float_bits function value_object() -> Val { return value_new(6) } # --- builders (return the container, so calls chain) ------------------------ -function value_add(list: Val, item: Val) -> Val { push(list.kids, item); return list } +function value_add(list: Val, item: Val) -> Val { + value_lists(list) + push(list.kids, item) + return list +} function value_put(obj: Val, key: pointer, item: Val) -> Val { + value_lists(obj) var i = 0 while i < len(obj.keys) { if obj.keys[i] == key { obj.kids[i] = item; return obj }; i += 1 } push(obj.keys, key); push(obj.kids, item) @@ -53,7 +68,7 @@ function value_put(obj: Val, key: pointer, item: Val) -> Val { # --- accessors -------------------------------------------------------------- function value_kind(v: Val) -> int { return v.tag } -function value_count(v: Val) -> int { return len(v.kids) } +function value_count(v: Val) -> int { return value_n(v) } function value_as_int(v: Val) -> int { if v.tag == 7 { return int(float_from_bits(v.num)) } return v.num @@ -66,21 +81,21 @@ function value_as_float(v: Val) -> float { } function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt } function value_at(list: Val, i: int) -> Val { - if i < 0 or i >= len(list.kids) { return value_null() } + if i < 0 or i >= value_n(list) { return value_null() } return list.kids[i] } function value_key_at(obj: Val, i: int) -> string { - if i < 0 or i >= len(obj.keys) { return "" } + if i < 0 or i >= value_n(obj) { return "" } return obj.keys[i] } function value_get(obj: Val, key: pointer) -> Val { var i = 0 - while i < len(obj.keys) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 } + while i < value_n(obj) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 } return value_null() } function value_has(obj: Val, key: pointer) -> int { var i = 0 - while i < len(obj.keys) { if obj.keys[i] == key { return 1 }; i += 1 } + while i < value_n(obj) { if obj.keys[i] == key { return 1 }; i += 1 } return 0 } @@ -132,7 +147,7 @@ function json_encode(v: Val) -> string { if v.tag == 5 { var out = "[" var i = 0 - while i < len(v.kids) { + while i < value_n(v) { if i > 0 { out += "," } out += json_encode(v.kids[i]) i += 1 @@ -141,7 +156,7 @@ function json_encode(v: Val) -> string { } var out = "{" var i = 0 - while i < len(v.keys) { + while i < value_n(v) { if i > 0 { out += "," } out = out + json_quote(v.keys[i]) + ":" + json_encode(v.kids[i]) i += 1 @@ -176,9 +191,11 @@ function json_parse(s: pointer) -> Val { # a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake). function value_free(v: Val) -> void { if v == null { return } - for i in 0 .. len(v.kids) { value_free(v.kids[i]) } - free(v.kids) - free(v.keys) + if v.kids != null { + for i in 0 .. len(v.kids) { value_free(v.kids[i]) } + free(v.kids) + free(v.keys) + } free(v) } function json_free(v: Val) -> void { value_free(v) }