Merge commit 'a299117' into lang/foundations

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 12:22:42 +03:00
commit 15f1020fb6
24 changed files with 71233 additions and 69256 deletions

View file

@ -0,0 +1,16 @@
# value_list_regrow.ludic - a model's list filled in place that shrinks and grows back, frame after
# frame, makes nothing: the items cut off are the list's spares, handed out again as it grows (each
# regrowth once made a new Value per item, and the failing fence stopped it at the warm-up's cap):
# bad 0 xs 2
program ValueListRegrow {
entry {
let o = Value.object()
let long = ["a", "b", "c", "d", "e", "f"]
let short = ["x", "y"]
for f in 0 .. 3500 {
if f % 2 == 0 { value_set_strs(o, "xs", long) } else { value_set_strs(o, "xs", short) }
Mem.frame()
}
print(`bad {Mem.bad_frames()} xs {Value.count(Value.get(o, "xs"))}`)
}
}

View file

@ -43,7 +43,7 @@ export function hints_mutes_load(hints_st: mut HintsState, l: Val) -> void {
if Value.kind(e) != 4 { continue }
let k = Value.as_str(e)
let id = hints_find(k)
if id >= 0 { hints_st.hn_mute[id] = true } else { push(hints_st.hn_foreign, k) }
if id >= 0 { hints_st.hn_mute[id] = true } else { push(hints_st.hn_foreign, intern(k)) } # the tree is let go
}
}
function hn_mutes() -> []bool {

View file

@ -3,6 +3,7 @@ module ludic_i18n uses
numbers float
import "ports.ludic"
import "state.ludic"
import "tables.ludic"
import "bytes.ludic"
import "po.ludic"
import "langs.ludic"

View file

@ -6,6 +6,7 @@ export function i18n_init(i18n_st: mut I18nState) -> void {
i18n_st.names = new []string
i18n_st.paths = new []string
i18n_st.fonts = new []string
i18n_st.tabs = new []I18nTables
lang_add(i18n_st, "en", "English", "", "")
let idx = Fs.read_text(i18n_st.lang_dir + "/languages.txt")
if idx != null {
@ -37,6 +38,7 @@ function lang_add(i18n_st: mut I18nState, code: string, name: string, path: stri
i18n_st.names[i] = name
i18n_st.paths[i] = path
i18n_st.fonts[i] = font
if tables_have(i18n_st, i) { i18n_st.tabs[i] = null } # another file for it: read that one next time
return
}
}

View file

@ -44,6 +44,7 @@ export state I18nState {
plural: Dict = null # a plural msgid (the singular) -> index into forms / 3
forms: []string = null # three forms per entry, "" where the file has fewer
plural_rule: int = 0 # PL_*
tabs: []I18nTables = null # by language: its lookups once read, kept for the next switch
}
# where the shipped languages are (a directory with languages.txt and <code>.po), and the atlas a

View file

@ -0,0 +1,88 @@
# tables.ludic - a language's lookups, kept once read: switching back to it is a copy of references,
# not the .po parsed again (every change of language once dropped the old tables for good)
property I18nTables {
exact: Dict = null
out: []string = null
lits: []string = null
pat_out: []string = null
pat_l0: words = null
pat_nl: words = null
pat_sc: words = null
pat_ph: words = null
pat_n: int = 0
pfx: Dict = null
sfx: Dict = null
gen_head: int = 0
nx_p: words = null
nx_s: words = null
nx_g: words = null
cache: Dict = null
cache_out: []string = null
caps: []string = null
plural: Dict = null
forms: []string = null
plural_rule: int = 0
}
@alloc_ok("once per language: its lookups, kept")
function tables_keep(i18n_st: I18nState) -> I18nTables {
let t = new I18nTables
t.exact = i18n_st.exact
t.out = i18n_st.out
t.lits = i18n_st.lits
t.pat_out = i18n_st.pat_out
t.pat_l0 = i18n_st.pat_l0
t.pat_nl = i18n_st.pat_nl
t.pat_sc = i18n_st.pat_sc
t.pat_ph = i18n_st.pat_ph
t.pat_n = i18n_st.pat_n
t.pfx = i18n_st.pfx
t.sfx = i18n_st.sfx
t.gen_head = i18n_st.gen_head
t.nx_p = i18n_st.nx_p
t.nx_s = i18n_st.nx_s
t.nx_g = i18n_st.nx_g
t.cache = i18n_st.cache
t.cache_out = i18n_st.cache_out
t.caps = i18n_st.caps
t.plural = i18n_st.plural
t.forms = i18n_st.forms
t.plural_rule = i18n_st.plural_rule
return t
}
function tables_put(i18n_st: mut I18nState, t: I18nTables) -> void {
i18n_st.exact = t.exact
i18n_st.out = t.out
i18n_st.lits = t.lits
i18n_st.pat_out = t.pat_out
i18n_st.pat_l0 = t.pat_l0
i18n_st.pat_nl = t.pat_nl
i18n_st.pat_sc = t.pat_sc
i18n_st.pat_ph = t.pat_ph
i18n_st.pat_n = t.pat_n
i18n_st.pfx = t.pfx
i18n_st.sfx = t.sfx
i18n_st.gen_head = t.gen_head
i18n_st.nx_p = t.nx_p
i18n_st.nx_s = t.nx_s
i18n_st.nx_g = t.nx_g
i18n_st.cache = t.cache
i18n_st.cache_out = t.cache_out
i18n_st.caps = t.caps
i18n_st.plural = t.plural
i18n_st.forms = t.forms
i18n_st.plural_rule = t.plural_rule
}
function tables_have(i18n_st: I18nState, k: int) -> bool {
if i18n_st.tabs == null or k >= len(i18n_st.tabs) { return false }
return i18n_st.tabs[k] != null
}
@alloc_ok("once per language: the slot its lookups are kept in")
function tables_file(i18n_st: mut I18nState, k: int) -> void {
if i18n_st.tabs == null { i18n_st.tabs = new []I18nTables }
while len(i18n_st.tabs) <= k { push(i18n_st.tabs, null) }
i18n_st.tabs[k] = tables_keep(i18n_st)
}

View file

@ -114,6 +114,19 @@ program I18nTest {
expect(i18n_font_now(i18n_st) == "font_default")
}
test "a language switched back to is its kept tables, not its file read again; a new probe reads it" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st)
i18n_use(i18n_st, "tt")
let first = L(i18n_st, "Settings")
Fs.write_text(dir() + "/tt.po", header("Tongue", "") + po_line("Settings", "Changed"))
i18n_use(i18n_st, "en")
i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == first)
i18n_init(i18n_st)
i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == "Changed")
}
test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st)
Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("Settings", "Mettings"))

View file

@ -7,10 +7,16 @@ export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
let forced = I18nWorld.forced()
if len(forced) > 0 { k = i18n_index(i18n_st, forced) }
var font = ""
if k > 0 {
if k > 0 and tables_have(i18n_st, k) {
tables_put(i18n_st, i18n_st.tabs[k])
i18n_st.on = true
i18n_st.cur = i18n_st.codes[k]
font = i18n_st.fonts[k]
} else if k > 0 {
let text = Fs.read_text(i18n_st.paths[k])
if text != null {
i18n_load_text(i18n_st, text)
tables_file(i18n_st, k)
i18n_st.on = true
i18n_st.cur = i18n_st.codes[k]
font = i18n_st.fonts[k]

View file

@ -20,6 +20,22 @@ export function settings_put(settings_st: SettingsState, v: Val, id: int) -> voi
}
}
# the store written into a kept object in place, in the file's own form: a Value is made only the
# first time its key is seen (a screen reading the settings made a new tree on every change)
export function settings_fill_json(settings_st: SettingsState, v: Val) -> void {
for i in 0 .. len(settings_st.sg_defs) {
let d = settings_st.sg_defs[i]
if not d.saved { continue }
if d.kind == SETTING_TEXT {
value_set_str(v, d.key, settings_st.sg_text[i])
} else if d.kind == SETTING_FLOAT {
value_set_int(v, d.key, int(Math.floor(settings_st.sg_num[i] * 1000.0 + 0.5)))
} else {
value_set_int(v, d.key, int(settings_st.sg_num[i]))
}
}
}
# every key `v` names, held to its range; quiet, since a load is applied whole by its caller
export function settings_from_json(settings_st: mut SettingsState, v: Val) -> void {
if v == null { return }
@ -30,7 +46,7 @@ function sg_read(settings_st: mut SettingsState, v: Val, id: int) -> void {
let d = settings_st.sg_defs[id]
if not d.saved or Value.kind(v) != 6 or Value.has(v, d.key) == 0 { return }
if d.kind == SETTING_TEXT {
settings_st.sg_text[id] = sv_str(v, d.key, settings_st.sg_text[id])
settings_st.sg_text[id] = intern(sv_str(v, d.key, settings_st.sg_text[id])) # the file's tree is let go
} else if d.kind == SETTING_FLOAT {
settings_st.sg_num[id] = settings_held(settings_st, id, sv_float(v, d.key, settings_st.sg_num[id]))
} else {

View file

@ -35,6 +35,21 @@ program SettingsTest {
expect_eq(fs[1].id, TOY_LANG)
}
test "the store filled into a kept object changes its Values in place" (settings_st: mut SettingsState) {
declare(settings_st)
let o = Value.object()
settings_fill_json(settings_st, o)
let q = Value.get(o, "shadow_q")
let l = Value.get(o, "lang")
settings_set_int(settings_st, TOY_SHADOW_Q, 3)
settings_set_text(settings_st, TOY_LANG, "tr")
settings_fill_json(settings_st, o)
expect(Value.get(o, "shadow_q") == q)
expect(Value.get(o, "lang") == l)
expect_eq(Value.as_int(q), 3)
expect(Value.as_str(l) == "tr")
}
test "a value out of range is the default" (settings_st: mut SettingsState) {
declare(settings_st)
settings_set_int(settings_st, TOY_SHADOW_Q, 9)

View file

@ -51,6 +51,8 @@ export state UiState {
ev_ring_at: int = 0
nt_evs: []Val = nt_evs_new() # native.ludic: fired events' objects, NT_EVS of them, made with the state
nt_ev_at: int = 0
nt_nums: []Val = nt_nums_new() # fired.ludic: the numbers a scroll or a slider reports, a ring
nt_num_at: int = 0
hd_ev: Val = hd_ev_new() # hold.ludic: the held control's event, filled in place
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

View file

@ -46,7 +46,7 @@ function ct_range_set(ui_st: mut UiState, n: UiNode, v0: float) -> void {
let lo = Value.as_float(ui_attr(n, "min"))
let hi = Value.as_float(ui_attr(n, "max"))
let v = Math.clamp(v0, lo, Math.max(hi, lo))
if v != Value.as_float(ui_attr(n, "value")) { ui_fire(ui_st, n, "change", Value.float(v)) }
if v != Value.as_float(ui_attr(n, "value")) { ui_fire_float(ui_st, n, "change", v) }
}
# a range's track pressed or dragged: the value where the pointer is
function ct_press(ui_st: mut UiState, n: UiNode, x: float, y: float) -> void {

View file

@ -0,0 +1,16 @@
# fired.ludic - the number a control reports on each frame it moves (a box scrolled, a slider dragged),
# from a ring of Values kept with the state: a fresh Value.float a frame was kept by nothing. The
# queue runs within the frame, long before NT_NUMS more come round to one
const NT_NUMS: int = 64
function nt_nums_new() -> []Val {
let l = new []Val
for i in 0 .. NT_NUMS { push(l, Value.float(0.0)) }
return l
}
export function ui_fire_float(ui_st: mut UiState, n: UiNode, name: string, x: float) -> void {
let at = ui_st.nt_num_at % NT_NUMS
ui_st.nt_num_at = at + 1
let v = ui_st.nt_nums[at]
Value.be_float(v, x)
ui_fire(ui_st, n, name, v)
}

View file

@ -24,6 +24,7 @@ import "env.ludic"
import "eval.ludic"
import "act.ludic"
import "native.ludic"
import "fired.ludic"
import "native_room.ludic"
import "controls.ludic"
import "controls_build.ludic"

View file

@ -13,7 +13,8 @@ function ly_ratio(n: UiNode, fixed_w: float, avail: float) -> void {
}
# x, y, w, h: a `nat_w` x `nat_h` picture in the node's content box by its object-fit
export function ui_object_fit(n: UiNode, nat_w: float, nat_h: float) -> []float { return ui_object_fit_into(new []float, n, nat_w, nat_h) }
function ui_object_fit_into(out: []float, n: UiNode, nat_w: float, nat_h: float) -> []float {
# ... into the caller's kept list (a draw every frame should: ui_object_fit makes one a call)
export function ui_object_fit_into(out: []float, n: UiNode, nat_w: float, nat_h: float) -> []float {
let bx = n.x + n.pl + n.bl
let by = n.y + n.pt + n.bt
let bw = n.cw - ly_hin(n)

View file

@ -32,7 +32,7 @@ function sc_walk(ui_st: mut UiState, n: UiNode, live: bool) -> void {
var off = held
if live { off = sc_input(ui_st, n, off, room, most) }
off = Math.clamp(off, 0.0, most)
if live and off != Math.clamp(held, 0.0, most) { ui_fire(ui_st, n, "scroll", Value.float(off / ui_px(ui_st, 1.0))) }
if live and off != Math.clamp(held, 0.0, most) { ui_fire_float(ui_st, n, "scroll", off / ui_px(ui_st, 1.0)) }
sc_put(ui_st, n.key, off)
n.scroll_y = off
if off != 0.0 {

View file

@ -491,6 +491,7 @@ namespace Json {
alias encode(value) = json_encode
alias write_file(value, path) = json_write_file # encode straight to a file, keeping nothing per call
alias parse(text) = json_parse
alias read_file(path) = json_read_file # parsed, and the file's text given back
alias free(value) = json_free
alias free_all(value) = json_free_all
}

View file

@ -22,6 +22,7 @@ property Val {
txt: pointer = null
keys: []pointer
kids: []Val
spare: []Val = null # a list's items cut off when it last shrank, handed out again as it grows
}
# a number, a string or a null is one record; only a list or an object carries lists (made when first
@ -112,20 +113,30 @@ function value_set_bool(o: Val, key: pointer, b: bool) -> void {
function value_list_fit(o: Val, key: pointer, n: int) -> Val {
let l = value_slot(o, key, 5)
value_lists(l)
while len(l.kids) > n { List.pop(l.kids) }
while len(l.kids) > n { value_spare_put(l, List.pop(l.kids)) }
return l
}
# a list's item i as kind `tag`: its own Value turned when it is another kind, a spare from when the
# list was last longer, and made only past the most it has held (a list that shrank and grew back
# made a new Value per item every time)
function value_item(l: Val, i: int, tag: int) -> Val {
if i < len(l.kids) {
if l.kids[i].tag == tag { return l.kids[i] }
let v = value_new(tag)
l.kids[i] = v
return v
return value_become(l.kids[i], tag)
}
if l.spare != null and len(l.spare) > 0 {
let s = value_become(List.pop(l.spare), tag)
push(l.kids, s)
return s
}
let v = value_new(tag)
push(l.kids, v)
return v
}
function value_spare_put(l: Val, v: Val) -> void {
if l.spare == null { l.spare = new []Val }
push(l.spare, v)
}
function value_set_ints(o: Val, key: pointer, xs: []int) -> void {
let l = value_list_fit(o, key, len(xs))
for i in 0 .. len(xs) {
@ -488,6 +499,16 @@ function jp_skip_ws(p: JP) -> void {
}
}
# a JSON file read and parsed, and its text given back: the tree's strings are the parser's own, so
# a reader of Json.parse(Fs.read_text(path)) kept the whole file for good, on every read
function json_read_file(path: string) -> Val {
let s = Fs.read_text(path)
if s == null { return null }
let v = json_parse(s)
free(s)
return v
}
function json_parse(s: pointer) -> Val {
let p = new JP
p.s = s; p.i = 0; p.n = len(s)

View file

@ -62,7 +62,12 @@ function cm_set(c: int, ty: pointer, k: pointer, x: pointer, head: Node) -> poin
if (ty == "[]float") { return ` value_set_floats(o, "{k}", {x})\n` }
if (ty == "[]string") { return ` value_set_strs(o, "{k}", {x})\n` }
if (ty == "[]bool") { return ` value_set_bools(o, "{k}", {x})\n` }
return ` value_put(o, "{k}", {cm_to(c, ty, x, head)})\n`
let line = vw_set(0, ty, "o", k, x, head) # a record or a list of them: filled in place (view_fill)
if line == null {
cm_err(c, `a {ty} is something a template cannot read`)
return ""
}
return line
}
function cm_to(c: int, ty: pointer, x: pointer, head: Node) -> pointer {
if (ty == "Val") { return x }

View file

@ -0,0 +1,54 @@
# view_fill.ludic - a component's field as a Value FILLED IN PLACE: a record is the object under its
# key with each field set, a list of records is the list under its key with each item filled, so a
# model built every frame makes Values only the first time (value_put of a fresh conversion kept a
# whole new tree each frame - the HUD's notifications were one). What cannot be filled converts.
function vw_set(v: int, ty: pointer, o: pointer, k: pointer, x: pointer, head: Node) -> pointer {
if (ty == "int") { return ` value_set_int({o}, "{k}", {x})\n` }
if (ty == "float") { return ` value_set_float({o}, "{k}", {x})\n` }
if (ty == "string") { return ` value_set_str({o}, "{k}", {x})\n` }
if (ty == "bool") { return ` value_set_bool({o}, "{k}", {x})\n` }
if (ty == "[]int") { return ` value_set_ints({o}, "{k}", {x})\n` }
if (ty == "[]float") { return ` value_set_floats({o}, "{k}", {x})\n` }
if (ty == "[]string") { return ` value_set_strs({o}, "{k}", {x})\n` }
if (ty == "[]bool") { return ` value_set_bools({o}, "{k}", {x})\n` }
if (ty == "Val") { return ` value_put({o}, "{k}", {x})\n` }
if len(ty) > 2 and ty[0] == '[' and ty[1] == ']' {
let et: pointer = ty[2..len(ty)]
if find_comp(et) != null {
vw_fill_fn(v, et, head)
let name = `view_set_list_{vw_mangle(et)}`
vw_helper(`function {name}(o: Val, k: string, xs: {ty}) -> void {{\n let l = value_list_fit(o, k, len(xs))\n var i = 0\n while i < len(xs) {{\n let d = value_item(l, i, 6)\n if xs[i] == null {{ value_become(d, 0) }} else {{ view_fill_{vw_mangle(et)}(d, xs[i]) }}\n i += 1\n }}\n}}\n`, name, head)
return ` {name}({o}, "{k}", {x})\n`
}
}
if find_comp(ty) != null {
vw_fill_fn(v, ty, head)
let m = vw_mangle(ty)
let name = `view_set_{m}`
vw_helper(`function {name}(o: Val, k: string, r: {ty}) -> void {{\n if r == null {{\n value_slot(o, k, 0)\n return\n }}\n view_fill_{m}(value_slot(o, k, 6), r)\n}}\n`, name, head)
return ` {name}({o}, "{k}", {x})\n`
}
let c = vw_conv(v, ty, x, head)
if c == null { return null }
return ` value_put({o}, "{k}", {c})\n`
}
# view_fill_T(d, r): every field of r a template could read, set into the object d in place. Its name
# is taken before its body is written, so a record that holds its own kind fills through itself
function vw_fill_fn(v: int, ty: pointer, head: Node) -> void {
let name = `view_fill_{vw_mangle(ty)}`
if vw_have(name) { return }
push(g_vw_helpers, name)
let comp = find_comp(ty)
var src = `function {name}(d: Val, r: {ty}) -> void {{\n`
var i = 0
while i < len(comp.kids) {
let f = comp.kids[i]
if f.ty != null {
let line = vw_set(v, f.ty, "d", f.s, `r.{f.s}`, head)
if line != null { src = src + line }
}
i += 1
}
vw_parse(src + "}\n", head.file, head.line, 1)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/defaults.ludic")
push(f, "selfhost/frontend/view.ludic")
push(f, "selfhost/frontend/view_conv.ludic")
push(f, "selfhost/frontend/view_fill.ludic")
push(f, "selfhost/frontend/view_finish.ludic")
push(f, "selfhost/frontend/component.ludic")
push(f, "selfhost/frontend/component_rw.ludic")

View file

@ -1182,6 +1182,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/long_into_int", "4294967295 does not fit one; it is a long", "a long literal given to an int is refused, not wrapped")
feat_case("lang/runtime_temps", "", "1 2 3 4 5 grew 0 files grew 0", "runtime_temps.ludic (Log, DateTime.format, Path, Mime, Text, Os and Fs keep nothing a call does not hand back)")
feat_case("lang/string_temps", "", "1 2 3 4 5 6 7 grew 0", "string_temps.ludic (a concatenation's pieces, a template's holes and a compared side are freed once used; a kept one is not)")
feat_case("lang/value_list_regrow", "", "bad 0 xs 2", "value_list_regrow.ludic (a model list filled in place that shrinks and grows back keeps its cut-off items as spares: nothing kept, the fence lets it through)")
feat_case("lang/json_saves", "", "1 2 3 saves grew 0", "json_saves.ludic (Json.write_file saves through a kept buffer: the text Json.encode gives, 1000 saves keep nothing)")
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)")
feat_case("lang/alloc_fence_declared", "", "bad 0 kept 0", "alloc_fence_declared.ludic (25.1: @alloc_ok on a function, a statement and a generic's statement is declared at run time; the failing fence lets it through)")