ludic.base: StrBuf and StrTable - a line written into a kept buffer and interned, one string per distinct text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3915af34c2
commit
a26cc2eea7
5 changed files with 182 additions and 0 deletions
|
|
@ -178,6 +178,17 @@ program Game {
|
||||||
|
|
||||||
Both are compiled and run by `tests/route_test.ludic` (the mechanics are `tests/toys/`).
|
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
|
## Tests
|
||||||
|
|
||||||
Each piece has a program under `tests/`, and `ludic test` runs them all, every test block in a
|
Each piece has a program under `tests/`, and `ludic test` runs them all, every test block in a
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ module ludic_base uses
|
||||||
numbers float
|
numbers float
|
||||||
import "tick.ludic"
|
import "tick.ludic"
|
||||||
import "queue.ludic"
|
import "queue.ludic"
|
||||||
|
import "text_buf.ludic"
|
||||||
|
import "text_intern.ludic"
|
||||||
import "rng.ludic"
|
import "rng.ludic"
|
||||||
import "save.ludic"
|
import "save.ludic"
|
||||||
import "save_fields.ludic"
|
import "save_fields.ludic"
|
||||||
|
|
|
||||||
51
packages/ludic.base/tests/text_test.ludic
Normal file
51
packages/ludic.base/tests/text_test.ludic
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/ludic.base/text_buf.ludic
Normal file
66
packages/ludic.base/text_buf.ludic
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
packages/ludic.base/text_intern.ludic
Normal file
52
packages/ludic.base/text_intern.ludic
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue