diff --git a/examples/lang/json_free_edited.ludic b/examples/lang/json_free_edited.ludic new file mode 100644 index 00000000..98de2346 --- /dev/null +++ b/examples/lang/json_free_edited.ludic @@ -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()}`) + } +} diff --git a/packages/ludic.base/save_fields.ludic b/packages/ludic.base/save_fields.ludic index e9ab7811..4cad5534 100644 --- a/packages/ludic.base/save_fields.ludic +++ b/packages/ludic.base/save_fields.ludic @@ -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 diff --git a/runtime/native/value.ludic b/runtime/native/value.ludic index a697bf9d..3e15ffa2 100644 --- a/runtime/native/value.ludic +++ b/runtime/native/value.ludic @@ -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) diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index a27000c6..5dc9690c 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -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)")