runtime: Json.write_file, and Json.encode through a kept buffer
The encoder appends into RtJsonState's buffer (grown only past the biggest document yet): ints and Q16.16 fixeds written as digits in place, floats through string() and freed. Json.encode copies the answer out once; Json.write_file hands the buffer to Fs.write_text (.tmp + rename) and keeps nothing. json_saves.ludic: exact text, the file equals encode, parse round-trips, 1000 saves grow 0; clean under MallocScribble. json_quote (the + builder) is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc0f3d3790
commit
4499cedcb8
6 changed files with 222 additions and 39 deletions
7
changes/json-write-file.md
Normal file
7
changes/json-write-file.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`Json.write_file(value, path)` saves without keeping anything.** It writes the text
|
||||||
|
`Json.encode` would give straight to the file (through `path.tmp` and a rename), built in a buffer
|
||||||
|
the runtime keeps, so a game that saves often holds nothing more for it. `Json.encode` itself is
|
||||||
|
built the same way and makes only the string it returns: it used to join its text with `+`, keeping
|
||||||
|
every piece and every number's text. `examples/lang/json_saves.ludic`: 1000 saves, 0 bytes.
|
||||||
26
docs/language/json/json-write_file.md
Normal file
26
docs/language/json/json-write_file.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
id: json-write_file
|
||||||
|
name: Json.write_file
|
||||||
|
category: json
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Json.write_file
|
||||||
|
sig: Json.write_file(value, path) -> bool
|
||||||
|
tip: Write a value tree to a file as JSON, keeping nothing.
|
||||||
|
order: 5
|
||||||
|
ns: Json
|
||||||
|
member: write_file
|
||||||
|
---
|
||||||
|
|
||||||
|
Writes the tree as the same text <a href="json-encode"><code>Json.encode</code></a> would give, straight to <code>path</code>: to <code>path.tmp</code> first and then renamed over it, as <code>Fs.write_text</code> does, so a crash mid-write leaves the previous file whole. The text is built in a buffer the runtime keeps and grows only for a document bigger than any before, so a game can save as often as it likes and hold nothing more for it - where <code>Json.encode</code> hands back a string of its own every time. False when the file cannot be written.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Save {
|
||||||
|
entry {
|
||||||
|
let o = Value.object()
|
||||||
|
Value.put(o, "day", Value.int(3))
|
||||||
|
let path = Os.temp_dir() + "/save_doc.json"
|
||||||
|
print(Json.write_file(o, path))
|
||||||
|
Fs.remove(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
48
examples/lang/json_saves.ludic
Normal file
48
examples/lang/json_saves.ludic
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# json_saves.ludic - Json.write_file writes a tree as JSON through a buffer the runtime keeps (the
|
||||||
|
# .tmp and the rename as Fs.write_text), so a game can save as often as it likes and keep nothing:
|
||||||
|
# the same text Json.encode gives, read back whole. Prints 1 2 3 and `saves grew 0` (the heap is
|
||||||
|
# read on the Mac; 0 elsewhere).
|
||||||
|
program JsonSaves {
|
||||||
|
function tree(n: int) -> Val {
|
||||||
|
let o = Value.object()
|
||||||
|
Value.put(o, "name", Value.str("camp \"north\"\nshore\\"))
|
||||||
|
Value.put(o, "day", Value.int(n))
|
||||||
|
Value.put(o, "cold", Value.int(-2147483647))
|
||||||
|
Value.put(o, "rep", Value.fixed(49152))
|
||||||
|
Value.put(o, "pace", Value.float(float(7) / float(2)))
|
||||||
|
Value.put(o, "wet", Value.bool(0))
|
||||||
|
Value.put(o, "none", Value.null())
|
||||||
|
let l = Value.list()
|
||||||
|
for i in 0 .. 40 { Value.add(l, Value.int(i * 37 - 500)) }
|
||||||
|
Value.put(o, "pack", l)
|
||||||
|
let inner = Value.object()
|
||||||
|
Value.put(inner, "x", Value.fixed(-8192))
|
||||||
|
Value.put(o, "at", inner)
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
entry {
|
||||||
|
let small = Value.object()
|
||||||
|
Value.put(small, "a", Value.int(-7))
|
||||||
|
Value.put(small, "b", Value.fixed(32768))
|
||||||
|
Value.put(small, "c", Value.str("q\"\\\n"))
|
||||||
|
let sl = Value.list()
|
||||||
|
Value.add(sl, Value.bool(1))
|
||||||
|
Value.add(sl, Value.null())
|
||||||
|
Value.put(small, "d", sl)
|
||||||
|
if Json.encode(small) == "{\"a\":-7,\"b\":0.5,\"c\":\"q\\\"\\\\\\n\",\"d\":[true,null]}" { print(1) }
|
||||||
|
let path = Os.temp_dir() + "/json_saves_" + string(Os.pid()) + ".json"
|
||||||
|
let t = tree(3)
|
||||||
|
let wrote = Json.write_file(t, path)
|
||||||
|
let back = Fs.read_text(path)
|
||||||
|
let enc = Json.encode(t)
|
||||||
|
if wrote and back == enc { print(2) }
|
||||||
|
let again = Json.parse(back)
|
||||||
|
if Json.encode(again) == enc { print(3) }
|
||||||
|
for i in 0 .. 20 { Json.write_file(t, path) }
|
||||||
|
let before = Os.heap_bytes()
|
||||||
|
for i in 0 .. 1000 { Json.write_file(t, path) }
|
||||||
|
let grew = Os.heap_bytes() - before
|
||||||
|
Fs.remove(path)
|
||||||
|
print(`saves grew {grew}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -488,6 +488,7 @@ namespace Value {
|
||||||
}
|
}
|
||||||
namespace Json {
|
namespace Json {
|
||||||
alias encode(value) = json_encode
|
alias encode(value) = json_encode
|
||||||
|
alias write_file(value, path) = json_write_file # encode straight to a file, keeping nothing per call
|
||||||
alias parse(text) = json_parse
|
alias parse(text) = json_parse
|
||||||
alias free(value) = json_free
|
alias free(value) = json_free
|
||||||
alias free_all(value) = json_free_all
|
alias free_all(value) = json_free_all
|
||||||
|
|
|
||||||
|
|
@ -304,22 +304,6 @@ function value_has(obj: Val, key: pointer) -> int {
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- JSON encode ------------------------------------------------------------
|
# --- JSON encode ------------------------------------------------------------
|
||||||
# escape a string's `"` `\` and newline, wrapping it in quotes.
|
|
||||||
function json_quote(s: pointer) -> string {
|
|
||||||
var out = "\""
|
|
||||||
var i = 0
|
|
||||||
let n = len(s)
|
|
||||||
while i < n {
|
|
||||||
let c = s[i]
|
|
||||||
if c == '"' { out += "\\\"" }
|
|
||||||
else { if c == '\\' { out += "\\\\" }
|
|
||||||
else { if c == '\n' { out += "\\n" }
|
|
||||||
else { out += s[i..i + 1] } } }
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
return out + "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
# a raw Q16.16 fixed -> its EXACT decimal string. The denominator is 2^16, so the
|
# a raw Q16.16 fixed -> its EXACT decimal string. The denominator is 2^16, so the
|
||||||
# decimal always terminates (in <=16 fractional digits): emit digits until the
|
# decimal always terminates (in <=16 fractional digits): emit digits until the
|
||||||
# remainder hits zero. This is the exact inverse of jp_number's fixed decode, so
|
# remainder hits zero. This is the exact inverse of jp_number's fixed decode, so
|
||||||
|
|
@ -341,31 +325,147 @@ function json_fixed_str(raw: int) -> string {
|
||||||
return sign + string(ip) + "." + fs
|
return sign + string(ip) + "." + fs
|
||||||
}
|
}
|
||||||
|
|
||||||
function json_encode(v: Val) -> string {
|
# The encoder writes into one buffer the runtime keeps, grown only when a document is bigger than
|
||||||
if v.tag == 0 { return "null" }
|
# any before: a save once built its text by `+`, keeping every piece and every number's text, and
|
||||||
if v.tag == 1 { return string(v.num) }
|
# the answer was one more string a game could never free. `Json.encode` copies the answer out once;
|
||||||
if v.tag == 2 { return json_fixed_str(v.num) }
|
# `Json.write_file` writes it through Fs.write_text (the .tmp and the rename) and keeps nothing.
|
||||||
if v.tag == 3 { if v.num != 0 { return "true" }; return "false" }
|
export state RtJsonState {
|
||||||
if v.tag == 4 { return json_quote(v.txt) }
|
jb: pointer = null, # the text so far, NUL-terminated when handed on
|
||||||
if v.tag == 7 { return string(float_from_bits(v.num)) }
|
jcap: int = 0,
|
||||||
if v.tag == 5 {
|
jn: int = 0
|
||||||
var out = "["
|
}
|
||||||
var i = 0
|
|
||||||
while i < value_n(v) {
|
function jo_room(st: mut RtJsonState, more: int) -> void {
|
||||||
if i > 0 { out += "," }
|
if st.jn + more + 1 <= st.jcap { return }
|
||||||
out += json_encode(v.kids[i])
|
var cap = st.jcap
|
||||||
i += 1
|
if cap < 4096 { cap = 4096 }
|
||||||
|
while cap < st.jn + more + 1 { cap = cap * 2 }
|
||||||
|
let nb = bytes(cap)
|
||||||
|
if st.jb != null {
|
||||||
|
let ob: bytes = st.jb
|
||||||
|
for i in 0 .. st.jn { nb[i] = ob[i] }
|
||||||
|
free(st.jb)
|
||||||
|
}
|
||||||
|
st.jb = nb
|
||||||
|
st.jcap = cap
|
||||||
|
}
|
||||||
|
function jo_put(st: mut RtJsonState, c: int) -> void {
|
||||||
|
jo_room(st, 1)
|
||||||
|
let b: bytes = st.jb
|
||||||
|
b[st.jn] = c
|
||||||
|
st.jn += 1
|
||||||
|
}
|
||||||
|
function jo_str(st: mut RtJsonState, s: pointer) -> void {
|
||||||
|
let n = len(s)
|
||||||
|
jo_room(st, n)
|
||||||
|
let b: bytes = st.jb
|
||||||
|
for i in 0 .. n { b[st.jn + i] = s[i] }
|
||||||
|
st.jn += n
|
||||||
|
}
|
||||||
|
# an int's digits, written in place (no text made for it)
|
||||||
|
function jo_int(st: mut RtJsonState, v: int) -> void {
|
||||||
|
if v == 0 {
|
||||||
|
jo_put(st, '0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var x: long = v
|
||||||
|
if x < 0 {
|
||||||
|
jo_put(st, '-')
|
||||||
|
x = -x
|
||||||
|
}
|
||||||
|
var div: long = 1
|
||||||
|
while div * 10 <= x { div = div * 10 }
|
||||||
|
while div > 0 {
|
||||||
|
jo_put(st, 48 + int(x / div))
|
||||||
|
x = x % div
|
||||||
|
div = div / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# a raw Q16.16 fixed, exactly (json_fixed_str's digits, written in place)
|
||||||
|
function jo_fixed(st: mut RtJsonState, raw: int) -> void {
|
||||||
|
var a: long = raw
|
||||||
|
if a < 0 {
|
||||||
|
jo_put(st, '-')
|
||||||
|
a = -a
|
||||||
|
}
|
||||||
|
jo_int(st, int(a / 65536))
|
||||||
|
jo_put(st, '.')
|
||||||
|
var frac = int(a % 65536)
|
||||||
|
if frac == 0 {
|
||||||
|
jo_put(st, '0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
while frac != 0 {
|
||||||
|
frac *= 10
|
||||||
|
jo_put(st, 48 + frac / 65536)
|
||||||
|
frac = frac % 65536
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function jo_quote(st: mut RtJsonState, s: pointer) -> void {
|
||||||
|
jo_put(st, '"')
|
||||||
|
let n = len(s)
|
||||||
|
for i in 0 .. n {
|
||||||
|
let c = s[i]
|
||||||
|
if c == '"' or c == '\\' {
|
||||||
|
jo_put(st, '\\')
|
||||||
|
jo_put(st, c)
|
||||||
|
} else if c == '\n' {
|
||||||
|
jo_put(st, '\\')
|
||||||
|
jo_put(st, 'n')
|
||||||
|
} else { jo_put(st, c) }
|
||||||
|
}
|
||||||
|
jo_put(st, '"')
|
||||||
|
}
|
||||||
|
function jo_val(st: mut RtJsonState, v: Val) -> void {
|
||||||
|
if v.tag == 0 { jo_str(st, "null") }
|
||||||
|
else if v.tag == 1 { jo_int(st, v.num) }
|
||||||
|
else if v.tag == 2 { jo_fixed(st, v.num) }
|
||||||
|
else if v.tag == 3 { if v.num != 0 { jo_str(st, "true") } else { jo_str(st, "false") } }
|
||||||
|
else if v.tag == 4 { jo_quote(st, value_as_str(v)) }
|
||||||
|
else if v.tag == 7 {
|
||||||
|
let t = string(float_from_bits(v.num))
|
||||||
|
jo_str(st, t)
|
||||||
|
free(t)
|
||||||
|
} else if v.tag == 5 {
|
||||||
|
jo_put(st, '[')
|
||||||
|
for i in 0 .. value_n(v) {
|
||||||
|
if i > 0 { jo_put(st, ',') }
|
||||||
|
jo_val(st, v.kids[i])
|
||||||
}
|
}
|
||||||
return out + "]"
|
jo_put(st, ']')
|
||||||
|
} else {
|
||||||
|
jo_put(st, '{')
|
||||||
|
for i in 0 .. value_n(v) {
|
||||||
|
if i > 0 { jo_put(st, ',') }
|
||||||
|
jo_quote(st, v.keys[i])
|
||||||
|
jo_put(st, ':')
|
||||||
|
jo_val(st, v.kids[i])
|
||||||
|
}
|
||||||
|
jo_put(st, '}')
|
||||||
}
|
}
|
||||||
var out = "{"
|
}
|
||||||
var i = 0
|
# the whole document into the kept buffer, NUL-terminated
|
||||||
while i < value_n(v) {
|
function jo_doc(st: mut RtJsonState, v: Val) -> void {
|
||||||
if i > 0 { out += "," }
|
st.jn = 0
|
||||||
out = out + json_quote(v.keys[i]) + ":" + json_encode(v.kids[i])
|
jo_val(st, v)
|
||||||
i += 1
|
jo_room(st, 0)
|
||||||
}
|
let b: bytes = st.jb
|
||||||
return out + "}"
|
b[st.jn] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function json_encode(rt_json_st: mut RtJsonState, v: Val) -> string {
|
||||||
|
jo_doc(rt_json_st, v)
|
||||||
|
let out = bytes(rt_json_st.jn + 1)
|
||||||
|
let b: bytes = rt_json_st.jb
|
||||||
|
for i in 0 .. rt_json_st.jn + 1 { out[i] = b[i] }
|
||||||
|
let s: string = out
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
# v written to path as JSON, replacing it whole (the .tmp and the rename); nothing kept per call
|
||||||
|
function json_write_file(rt_json_st: mut RtJsonState, v: Val, path: string) -> bool {
|
||||||
|
jo_doc(rt_json_st, v)
|
||||||
|
let s: string = rt_json_st.jb
|
||||||
|
return Fs.write_text(path, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- JSON parse -------------------------------------------------------------
|
# --- JSON parse -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1081,6 +1081,7 @@ function cmd_dev_test() -> int {
|
||||||
reject_case("rejected/long_into_int", "4294967295 does not fit one; it is a long", "a long literal given to an int is refused, not wrapped")
|
reject_case("rejected/long_into_int", "4294967295 does not fit one; it is a long", "a long literal given to an int is refused, not wrapped")
|
||||||
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/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/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)")
|
feat_case("lang/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)")
|
||||||
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
|
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
|
||||||
reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")
|
reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue