Json.free_all is safe on a parsed tree a migration and a loader have edited; sv_str keeps a copy
A load freed only the parsed trip's nodes (Json.free) and kept every string the parser made, because
a loader might keep one; free_all freed every string and every key, so on a tree a migration had
added a literal to (`Value.put(v, "sver", Value.str("2"))`) it freed the literal and aborted.
- The parser marks the string values it makes (JP_OWNED, in the node's otherwise unused num) and
interns object keys (a few names, never freed); free_all frees only marked strings, never keys,
and a list's spares too. A setter that gives a marked node other text (value_set_str/_strs,
value_into_str/_strs, value_become) frees the parser's text first. value_as_int / _as_float read a
string as 0 as before.
- ludic.base sv_str returns intern(...): every package load that keeps a text read from a section
(minimap labels, a Thing's look, photo tags and files, an effect's label, ...) holds its own copy.
Golden json_free_edited: parse, put a literal key and string in, set a string, keep an interned copy,
free_all - 1100 loads: the copy reads on and nothing grows (the toolchain before this aborts, 134).
Tests: ludic.save, base, settings, minimap, things, photo, effects; json_saves, value_list_regrow; the
36 ui examples.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6f39b50e63
commit
3f685af868
4 changed files with 56 additions and 5 deletions
24
examples/lang/json_free_edited.ludic
Normal file
24
examples/lang/json_free_edited.ludic
Normal 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()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ export function sv_str(v: Val, key: string, fallback: string) -> string {
|
||||||
if not sv_has(v, key) { return fallback }
|
if not sv_has(v, key) { return fallback }
|
||||||
let s = Value.get(v, key)
|
let s = Value.get(v, key)
|
||||||
if Value.kind(s) != 4 { return fallback }
|
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
|
# a list of ints; a missing key or a non-list is the empty list
|
||||||
|
|
|
||||||
|
|
@ -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
|
# 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 {
|
function value_set_str(o: Val, key: pointer, s: pointer) -> void {
|
||||||
let v = value_slot(o, key, 4)
|
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)
|
v.txt = intern(s)
|
||||||
}
|
}
|
||||||
function value_set_bool(o: Val, key: pointer, b: bool) -> void {
|
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))
|
let l = value_list_fit(o, key, len(xs))
|
||||||
for i in 0 .. len(xs) {
|
for i in 0 .. len(xs) {
|
||||||
let v = value_item(l, i, 4)
|
let v = value_item(l, i, 4)
|
||||||
|
jp_let_go(v)
|
||||||
v.txt = intern(xs[i])
|
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 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)
|
# a pool that hands the same records out again (ludic.ui's, frame after frame)
|
||||||
function value_become(v: Val, tag: int) -> Val {
|
function value_become(v: Val, tag: int) -> Val {
|
||||||
|
jp_let_go(v)
|
||||||
v.tag = tag
|
v.tag = tag
|
||||||
value_num_set(v, 0)
|
value_num_set(v, 0)
|
||||||
v.txt = null
|
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 {
|
function value_into_str(into: Val, s: pointer) -> Val {
|
||||||
let v = value_into(into, 4)
|
let v = value_into(into, 4)
|
||||||
|
jp_let_go(v)
|
||||||
v.txt = intern(s)
|
v.txt = intern(s)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +253,7 @@ function value_into_strs(into: Val, xs: []string) -> Val {
|
||||||
let l = value_into_list(into, len(xs))
|
let l = value_into_list(into, len(xs))
|
||||||
for i in 0 .. len(xs) {
|
for i in 0 .. len(xs) {
|
||||||
let v = value_item(l, i, 4)
|
let v = value_item(l, i, 4)
|
||||||
|
jp_let_go(v)
|
||||||
v.txt = intern(xs[i])
|
v.txt = intern(xs[i])
|
||||||
}
|
}
|
||||||
return l
|
return l
|
||||||
|
|
@ -293,12 +298,14 @@ function value_clear(v: Val) -> void {
|
||||||
}
|
}
|
||||||
function value_as_int(v: Val) -> int {
|
function value_as_int(v: Val) -> int {
|
||||||
if v.tag == 7 { return int(float_from_bits(v.num)) }
|
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
|
return v.num
|
||||||
}
|
}
|
||||||
# a number as a float, whichever kind it was written as
|
# a number as a float, whichever kind it was written as
|
||||||
function value_as_float(v: Val) -> float {
|
function value_as_float(v: Val) -> float {
|
||||||
if v.tag == 7 { return float_from_bits(v.num) }
|
if v.tag == 7 { return float_from_bits(v.num) }
|
||||||
if v.tag == 2 { return float(v.num) / 65536.0 }
|
if v.tag == 2 { return float(v.num) / 65536.0 }
|
||||||
|
if v.tag == 4 { return 0.0 }
|
||||||
return float(v.num)
|
return float(v.num)
|
||||||
}
|
}
|
||||||
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
|
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
|
# ... 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
|
# 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
|
# 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 {
|
function value_free_all(v: Val) -> void {
|
||||||
if v == null { return }
|
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 {
|
if v.kids != null {
|
||||||
for i in 0 .. len(v.kids) { value_free_all(v.kids[i]) }
|
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.kids)
|
||||||
free(v.keys)
|
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)
|
free(v)
|
||||||
}
|
}
|
||||||
function json_free_all(v: Val) -> void { value_free_all(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]
|
let c = p.s[p.i]
|
||||||
if c == '{' { return jp_object(p) } # '{'
|
if c == '{' { return jp_object(p) } # '{'
|
||||||
if c == '[' { return jp_list(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 == 't' { p.i += 4; return value_bool(1) } # true
|
||||||
if c == 'f' { p.i += 5; return value_bool(0) } # false
|
if c == 'f' { p.i += 5; return value_bool(0) } # false
|
||||||
if c == 'n' { p.i += 4; return value_null() } # null
|
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 '}'
|
if p.i < p.n and p.s[p.i] == '}' { p.i += 1; return out } # empty '}'
|
||||||
while p.i < p.n {
|
while p.i < p.n {
|
||||||
jp_skip_ws(p)
|
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)
|
jp_skip_ws(p)
|
||||||
if p.i < p.n and p.s[p.i] == ':' { p.i += 1 } # ':'
|
if p.i < p.n and p.s[p.i] == ':' { p.i += 1 } # ':'
|
||||||
push(out.keys, key)
|
push(out.keys, key)
|
||||||
|
|
|
||||||
|
|
@ -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/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/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/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/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", "", "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)")
|
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)")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue