Merge branch 'lang/ecs' into lang/uifree

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 11:22:25 +03:00
commit 8fbd7c7e60
5 changed files with 182 additions and 0 deletions

View file

@ -178,6 +178,17 @@ program Game {
Both are compiled and run by `tests/route_test.ludic` (the mechanics are `tests/toys/`).
## Text without allocating: `StrBuf` and `StrTable`
Ludic frees nothing, so a line built every frame - a clock, a countdown, a prompt - grows the
program for as long as it runs. Write it into a kept `StrBuf` instead (`sb_clear`, `sb_add`,
`sb_int`, `sb_int2`, `sb_byte`, and `sb_pat(sb, "{1} of {2}", a, b, c, d)`, whose pattern stays a
literal at the call so a translator still finds it), then `sb_intern(sb, table)`: the `StrTable`
hands back ONE string per distinct text, looked up by content and copied out only the first time
those bytes are seen. What is handed on is the table's and never changes, so anything that keeps
a string as a key stays right. Growth stops once each text has been seen (1440 clock minutes, the
countdown's values); past `strs_new(most)` entries it falls back to a plain copy.
## Tests
Each piece has a program under `tests/`, and `ludic test` runs them all, every test block in a

View file

@ -4,6 +4,8 @@ module ludic_base uses
numbers float
import "tick.ludic"
import "queue.ludic"
import "text_buf.ludic"
import "text_intern.ludic"
import "rng.ludic"
import "save.ludic"
import "save_fields.ludic"

View file

@ -0,0 +1,51 @@
# text_test.ludic - text written into a kept buffer, and interned: one string per distinct text
import "ludic.base"
program TextTest {
numbers float
test "a buffer takes text, numbers and a pattern, and cuts at its end rather than past it" {
let sb = sb_new(64)
sb_add(sb, "day ")
sb_int(sb, 42)
sb_add(sb, " at ")
sb_int2(sb, 7)
sb_byte(sb, 58)
sb_int2(sb, 5)
let tb = strs_new(16)
expect_eq(sb_intern(sb, tb), "day 42 at 07:05")
sb_clear(sb)
sb_int(sb, -310)
sb_add(sb, " ")
sb_int(sb, 0)
expect_eq(sb_intern(sb, tb), "-310 0")
sb_clear(sb)
sb_pat(sb, "[{1}] {2}: {3} minutes - room for {4} more", "E", "Trout", "4", "2")
expect_eq(sb_intern(sb, tb), "[E] Trout: 4 minutes - room for 2 more")
let small = sb_new(16)
sb_add(small, "a line longer than its buffer")
expect_eq(sb_len(small), 16)
}
test "the same bytes intern to the one string, made once; past the cap a plain copy" {
let sb = sb_new(32)
let tb = strs_new(3)
sb_add(sb, "08:03")
let a = sb_intern(sb, tb)
sb_clear(sb)
sb_add(sb, "08:0")
sb_int(sb, 3)
let b = sb_intern(sb, tb)
expect_eq(strs_count(tb), 1)
let ap: pointer = a
let bp: pointer = b
expect(ap == bp)
for i in 0 .. 5 {
sb_clear(sb)
sb_int(sb, i)
expect_eq(sb_intern(sb, tb), string(i))
}
expect_eq(strs_count(tb), 3)
sb_clear(sb)
sb_add(sb, "08:03")
let c: pointer = sb_intern(sb, tb)
expect(c == ap)
}
}

View file

@ -0,0 +1,66 @@
# text_buf.ludic - text written into a kept buffer: Ludic frees nothing, so a line built every
# frame (a clock, a countdown) is written here and interned (text_intern.ludic), never allocated
export property StrBuf {
b: []byte = null
n: int = 0
}
export function sb_new(cap: int) -> StrBuf {
let sb = new StrBuf
sb.b = buffer(Math.max(cap, 16))
return sb
}
export function sb_clear(sb: StrBuf) -> void { sb.n = 0 }
export function sb_len(sb: StrBuf) -> int { return sb.n }
# a byte, dropped past the buffer's end: a line cut short, never a write out of it
export function sb_byte(sb: StrBuf, c: int) -> void {
if sb.n >= len(sb.b) { return }
sb.b[sb.n] = c
sb.n += 1
}
export function sb_add(sb: StrBuf, s: string) -> void {
if s == null { return }
let sp: string = s
for i in 0 .. len(sp) { sb_byte(sb, sp[i]) }
}
# a whole number in decimal, a minus sign first
export function sb_int(sb: StrBuf, v: int) -> void {
var x = v
if x < 0 {
sb_byte(sb, 45)
x = 0 - x
}
var d = 1
while d <= x / 10 { d = d * 10 }
while d >= 1 {
sb_byte(sb, 48 + (x / d) % 10)
d = d / 10
}
}
# a two-digit number, a leading zero below ten (a clock's hour and minute)
export function sb_int2(sb: StrBuf, v: int) -> void {
if v >= 0 and v < 10 { sb_byte(sb, 48) }
sb_int(sb, v)
}
# a pattern with numbered holes, "{1} of {2}", its holes filled from a..d in order. The pattern is
# the message a translator sees, as a template literal's is, so it stays a literal at the call
export function sb_pat(sb: StrBuf, pat: string, a: string, b: string, c: string, d: string) -> void {
let p: string = pat
var i = 0
while i < len(p) {
if p[i] == 123 and i + 2 < len(p) and p[i + 2] == 125 and p[i + 1] >= 49 and p[i + 1] <= 52 {
let k = p[i + 1] - 48
if k == 1 { sb_add(sb, a) } else if k == 2 { sb_add(sb, b) } else if k == 3 { sb_add(sb, c) } else { sb_add(sb, d) }
i += 3
} else {
sb_byte(sb, p[i])
i += 1
}
}
}

View file

@ -0,0 +1,52 @@
# text_intern.ludic - a kept buffer's bytes as ONE string per distinct text: looked up by content
# (FNV-1a, compared in place), copied out only the first time. Every string handed on is the
# table's and never changes, so a memo keyed on it stays right; past `most` texts a plain copy
export property StrTable {
hs: words = null # open addressing: each slot's hash, and its entry + 1
at: words = null
ss: []string = new []string
most: int = 8192
}
export function strs_new(most: int) -> StrTable {
let tb = new StrTable
tb.most = Math.max(most, 1)
var cap = 16
while cap < tb.most * 2 { cap = cap * 2 }
tb.hs = words(cap)
tb.at = words(cap)
return tb
}
export function strs_count(tb: StrTable) -> int { return len(tb.ss) }
function sb_hash(sb: StrBuf) -> int {
var h = -2128831035
for i in 0 .. sb.n { h = (h ^ sb.b[i]) * 16777619 }
return h
}
function sb_same(sb: StrBuf, s: string) -> bool {
let sp: string = s
if len(sp) != sb.n { return false }
for i in 0 .. sb.n { if sp[i] != sb.b[i] { return false } }
return true
}
# the table's string for the buffer's bytes
export function sb_intern(sb: StrBuf, tb: StrTable) -> string {
let h = sb_hash(sb)
let mask = len(tb.hs) - 1
var slot = h & mask
while tb.at[slot] != 0 {
let e = tb.at[slot] - 1
if tb.hs[slot] == h and sb_same(sb, tb.ss[e]) { return tb.ss[e] }
slot = (slot + 1) & mask
}
let s = text_of(sb.b, sb.n)
if len(tb.ss) >= tb.most { return s }
push(tb.ss, s)
tb.hs[slot] = h
tb.at[slot] = len(tb.ss)
return s
}