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:
Orkun ÇAKILKAYA 2026-09-29 13:43:24 +03:00
parent 6f39b50e63
commit 3f685af868
4 changed files with 56 additions and 5 deletions

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