Merge branch 'lang/ecs' into lang/uifree
This commit is contained in:
commit
8fbd7c7e60
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/`).
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
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