Merge commit '3f685af' into lang/foundations

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 13:43:43 +03:00
commit 90b23e11e9
4 changed files with 56 additions and 5 deletions

View file

@ -0,0 +1,24 @@
# json_free_edited.ludic - a parsed file let go whole (Json.free_all) after it was migrated and read:
# a literal key and a literal string put in, a string set in place, a copy kept - only what the
# parser made is freed, so nothing crashes, the copy reads on, and a thousand loads keep nothing:
# names Sam|Ada grew 32
program JsonFreeEdited {
function read_one(text: string, keep: []string) -> void {
let v = Json.parse(text)
Value.put(v, "sver", Value.str("2"))
value_set_str(v, "name", "Ada")
if len(keep) < 2 { push(keep, intern(Value.as_str(Value.get(v, "who")))) }
Json.free_all(v)
}
entry {
let keep = new []string
let text = "{\"name\": \"Sam\", \"who\": \"Sam\", \"pins\": [\"a\", \"b\"], \"n\": 3}"
read_one(text, keep)
push(keep, "Ada")
for f in 0 .. 1100 {
read_one(text, keep)
Mem.frame()
}
print(`names {keep[0]}|{keep[1]} grew {Mem.kept()}`)
}
}

View file

@ -27,7 +27,7 @@ export function sv_str(v: Val, key: string, fallback: string) -> string {
if not sv_has(v, key) { return fallback }
let s = Value.get(v, key)
if Value.kind(s) != 4 { return fallback }
return Value.as_str(s)
return intern(Value.as_str(s)) # a copy of its own: the tree it came from is let go whole once read
}
# a list of ints; a missing key or a non-list is the empty list

View file

@ -101,6 +101,7 @@ function value_set_float(o: Val, key: pointer, x: float) -> void {
# every frame) is given back with its frame rather than kept by the model
function value_set_str(o: Val, key: pointer, s: pointer) -> void {
let v = value_slot(o, key, 4)
jp_let_go(v) # the parser's text it held is freed (JP_OWNED)
v.txt = intern(s)
}
function value_set_bool(o: Val, key: pointer, b: bool) -> void {
@ -156,6 +157,7 @@ function value_set_strs(o: Val, key: pointer, xs: []string) -> void {
let l = value_list_fit(o, key, len(xs))
for i in 0 .. len(xs) {
let v = value_item(l, i, 4)
jp_let_go(v)
v.txt = intern(xs[i])
}
}
@ -170,6 +172,7 @@ function value_set_bools(o: Val, key: pointer, xs: []bool) -> void {
# a Value turned into a blank one of kind `tag`, its own lists (if it has them) kept and emptied - for
# a pool that hands the same records out again (ludic.ui's, frame after frame)
function value_become(v: Val, tag: int) -> Val {
jp_let_go(v)
v.tag = tag
value_num_set(v, 0)
v.txt = null
@ -211,6 +214,7 @@ function value_into_float(into: Val, x: float) -> Val {
}
function value_into_str(into: Val, s: pointer) -> Val {
let v = value_into(into, 4)
jp_let_go(v)
v.txt = intern(s)
return v
}
@ -249,6 +253,7 @@ function value_into_strs(into: Val, xs: []string) -> Val {
let l = value_into_list(into, len(xs))
for i in 0 .. len(xs) {
let v = value_item(l, i, 4)
jp_let_go(v)
v.txt = intern(xs[i])
}
return l
@ -293,12 +298,14 @@ function value_clear(v: Val) -> void {
}
function value_as_int(v: Val) -> int {
if v.tag == 7 { return int(float_from_bits(v.num)) }
if v.tag == 4 { return 0 } # a string's num is its owner mark, not a number
return v.num
}
# a number as a float, whichever kind it was written as
function value_as_float(v: Val) -> float {
if v.tag == 7 { return float_from_bits(v.num) }
if v.tag == 2 { return float(v.num) / 65536.0 }
if v.tag == 4 { return 0.0 }
return float(v.num)
}
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
@ -534,15 +541,32 @@ function json_free(v: Val) -> void { value_free(v) }
# ... and its strings too - every string value and object key the parser made - for a tree whose
# reader copied what it keeps (render3d's glTF documents): a parse left otherwise gives back the
# nodes and keeps every name, ~640 KB a model
# A parsed tree let go whole once read, edited or not: a string the parser made (JP_OWNED) is
# freed; one a migration or a loader put in - a literal, an interned text - is theirs; a key is
# interned by the parser and never freed. A node must be reachable once (no loader shares one).
const JP_OWNED: int = 1246974528
# a string node about to be given other text: the parser's own is freed, and the mark goes with it
function jp_let_go(v: Val) -> void {
if v.tag == 4 and v.num == JP_OWNED and v.txt != null { free(v.txt) }
if v.tag == 4 { v.num = 0 }
}
function jp_owned_str(s: string) -> Val {
let v = value_str(s)
v.num = JP_OWNED
return v
}
function value_free_all(v: Val) -> void {
if v == null { return }
if v.tag == 4 and v.txt != null { free(v.txt) }
if v.tag == 4 and v.txt != null and v.num == JP_OWNED { free(v.txt) }
if v.kids != null {
for i in 0 .. len(v.kids) { value_free_all(v.kids[i]) }
if v.keys != null { for i in 0 .. len(v.keys) { if v.keys[i] != null { free(v.keys[i]) } } }
free(v.kids)
free(v.keys)
}
if v.spare != null {
for i in 0 .. len(v.spare) { value_free_all(v.spare[i]) }
free(v.spare)
}
free(v)
}
function json_free_all(v: Val) -> void { value_free_all(v) }
@ -553,7 +577,7 @@ function jp_value(p: JP) -> Val {
let c = p.s[p.i]
if c == '{' { return jp_object(p) } # '{'
if c == '[' { return jp_list(p) } # '['
if c == '"' { return value_str(jp_string(p)) } # '"'
if c == '"' { return jp_owned_str(jp_string(p)) } # '"'
if c == 't' { p.i += 4; return value_bool(1) } # true
if c == 'f' { p.i += 5; return value_bool(0) } # false
if c == 'n' { p.i += 4; return value_null() } # null
@ -689,7 +713,9 @@ function jp_object(p: JP) -> Val {
if p.i < p.n and p.s[p.i] == '}' { p.i += 1; return out } # empty '}'
while p.i < p.n {
jp_skip_ws(p)
let key = jp_string(p)
let raw = jp_string(p)
let key = intern(raw) # a key is one of a few names: interned, never freed
free(raw)
jp_skip_ws(p)
if p.i < p.n and p.s[p.i] == ':' { p.i += 1 } # ':'
push(out.keys, key)

View file

@ -1183,6 +1183,7 @@ function cmd_dev_test() -> int {
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_free_edited", "", "names Sam|Ada grew 32", "json_free_edited.ludic (Json.free_all after a migration put literals in and a loader set a string: only the parser's strings are freed, a kept copy reads on, a thousand loads keep nothing)")
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)")