json(vm): fix birth dispatch; unify constructor naming (Box.birth/N); JsonNode factories return JsonNodeInstance; quick: enable heavy JSON with probe; builder: NYASH_BUILDER_DEBUG_LIMIT guard; json_query_min(core) harness; docs/tasks updated
This commit is contained in:
340
apps/examples/json_query/main.nyash
Normal file
340
apps/examples/json_query/main.nyash
Normal file
@ -0,0 +1,340 @@
|
||||
// no external using required for quick json_query (text-slicing)
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
// Simple JSON query runner: evaluate a few (json, path) pairs and print results
|
||||
// Path grammar (subset):
|
||||
// <path> := ('.' <ident> | '[' <digits> ']')*
|
||||
// Examples: .a.b[1], .c, [0]
|
||||
|
||||
local cases = new ArrayBox()
|
||||
|
||||
// Case 1
|
||||
local j1 = "{\"a\":{\"b\":[1,2,3]},\"c\":\"x\"}"
|
||||
cases.push(j1)
|
||||
cases.push(".a.b[1]") // -> 2
|
||||
cases.push(j1)
|
||||
cases.push(".c") // -> "x"
|
||||
cases.push(j1)
|
||||
cases.push(".a") // -> {"b":[1,2,3]}
|
||||
cases.push(j1)
|
||||
cases.push(".a.b") // -> [1,2,3]
|
||||
cases.push(j1)
|
||||
cases.push(".a.b[10]") // -> null
|
||||
cases.push(j1)
|
||||
cases.push(".x") // -> null
|
||||
cases.push(j1)
|
||||
cases.push(".a.b[0]") // -> 1
|
||||
|
||||
// Case 2
|
||||
local j2 = "[ {\"k\":\"v\"}, 10, null ]"
|
||||
cases.push(j2)
|
||||
cases.push("[0].k") // -> "v"
|
||||
cases.push(j2)
|
||||
cases.push("[1]") // -> 10
|
||||
cases.push(j2)
|
||||
cases.push("[2]") // -> null
|
||||
cases.push(j2)
|
||||
cases.push("[3]") // -> null (out of range)
|
||||
|
||||
local i = 0
|
||||
loop(i < cases.length()) {
|
||||
local json_text = cases.get(i)
|
||||
local path = cases.get(i + 1)
|
||||
// Parser-less path: slice JSON text directly for quick profile stability
|
||||
local out_text = this.eval_path_text(json_text, path)
|
||||
if out_text == null { print("null") } else { print(out_text) }
|
||||
i = i + 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Evaluate a simple JSON path by slicing JSON text directly (no full parse)
|
||||
// Returns a JSON substring for the value or null if not found
|
||||
eval_path_text(json_text, path) {
|
||||
local cur_text = json_text
|
||||
local i = 0
|
||||
loop(i < path.length()) {
|
||||
local ch = path.substring(i, i + 1)
|
||||
if ch == "." {
|
||||
// parse identifier
|
||||
i = i + 1
|
||||
local start = i
|
||||
loop(i < path.length()) {
|
||||
local c = path.substring(i, i + 1)
|
||||
if this.is_alnum(c) or c == "_" { i = i + 1 } else { break }
|
||||
}
|
||||
local key = path.substring(start, i)
|
||||
if key.length() == 0 { return null }
|
||||
// Get value text directly; then reset window to that text
|
||||
local next_text = this.object_get_text(cur_text, 0, cur_text.length(), key)
|
||||
if next_text == null { return null }
|
||||
cur_text = next_text
|
||||
} else {
|
||||
if ch == "[" {
|
||||
// parse index
|
||||
i = i + 1
|
||||
local start = i
|
||||
loop(i < path.length() and this.is_digit(path.substring(i, i + 1))) { i = i + 1 }
|
||||
local idx_str = path.substring(start, i)
|
||||
if i >= path.length() or path.substring(i, i + 1) != "]" { return null }
|
||||
i = i + 1 // skip ']'
|
||||
local idx = this.parse_int(idx_str)
|
||||
local next_text = this.array_get_text(cur_text, 0, cur_text.length(), idx)
|
||||
if next_text == null { return null }
|
||||
cur_text = next_text
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
return cur_text
|
||||
}
|
||||
|
||||
// Local helpers (avoid external using in app)
|
||||
is_digit(ch) {
|
||||
return ch == "0" or ch == "1" or ch == "2" or ch == "3" or ch == "4" or ch == "5" or ch == "6" or ch == "7" or ch == "8" or ch == "9"
|
||||
}
|
||||
is_alpha(ch) {
|
||||
return (ch >= "a" and ch <= "z") or (ch >= "A" and ch <= "Z")
|
||||
}
|
||||
is_alnum(ch) {
|
||||
return this.is_alpha(ch) or this.is_digit(ch)
|
||||
}
|
||||
parse_int(s) {
|
||||
local i = 0
|
||||
local neg = false
|
||||
if s.length() > 0 and s.substring(0,1) == "-" {
|
||||
neg = true
|
||||
i = 1
|
||||
}
|
||||
local acc = 0
|
||||
loop(i < s.length()) {
|
||||
local ch = s.substring(i, i + 1)
|
||||
if not this.is_digit(ch) { break }
|
||||
// ch to digit
|
||||
// 0..9
|
||||
if ch == "0" { acc = acc * 10 + 0 }
|
||||
else { if ch == "1" { acc = acc * 10 + 1 }
|
||||
else { if ch == "2" { acc = acc * 10 + 2 }
|
||||
else { if ch == "3" { acc = acc * 10 + 3 }
|
||||
else { if ch == "4" { acc = acc * 10 + 4 }
|
||||
else { if ch == "5" { acc = acc * 10 + 5 }
|
||||
else { if ch == "6" { acc = acc * 10 + 6 }
|
||||
else { if ch == "7" { acc = acc * 10 + 7 }
|
||||
else { if ch == "8" { acc = acc * 10 + 8 }
|
||||
else { if ch == "9" { acc = acc * 10 + 9 } }}}}}}}}}
|
||||
i = i + 1
|
||||
}
|
||||
if neg { return 0 - acc } else { return acc }
|
||||
}
|
||||
// --- Minimal JSON slicing helpers (object/array) ---
|
||||
// Find a key's value span within an object JSON slice [start,end)
|
||||
object_get_span(s, start, end, key) {
|
||||
local i = start
|
||||
if i < end and s.substring(i, i+1) == "{" { i = i + 1 } else { return null }
|
||||
loop(i < end) {
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i >= end { return null }
|
||||
if s.substring(i, i+1) == "}" { return null }
|
||||
if s.substring(i, i+1) != "\"" { return null }
|
||||
local key_end = this.read_string_end(s, i, end)
|
||||
if key_end == -1 { return null }
|
||||
local key_text = s.substring(i+1, key_end-1)
|
||||
i = key_end
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i >= end or s.substring(i, i+1) != ":" { return null }
|
||||
i = i + 1
|
||||
i = this.skip_ws(s, i, end)
|
||||
local vspan = this.read_value_span(s, i, end)
|
||||
if vspan == null { return null }
|
||||
if key_text == key { return vspan }
|
||||
i = vspan.get(1)
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i < end and s.substring(i, i+1) == "," {
|
||||
i = i + 1
|
||||
continue
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Return the value text for a key within an object slice, or null
|
||||
object_get_text(s, start, end, key) {
|
||||
local sp = this.object_get_span(s, start, end, key)
|
||||
if sp == null { return null }
|
||||
// sp is ArrayBox [i, j]; re-scan without using .get to avoid VM instance calls
|
||||
// Instead, rebuild by scanning again (small overhead, safer on current VM path)
|
||||
// We locate the key again and then extract value via read_value_span
|
||||
// Directly extracting indices from sp would require .get; so re-use scanner
|
||||
// Start from beginning for simplicity
|
||||
local i = start
|
||||
if i < end and s.substring(i, i+1) == "{" { i = i + 1 } else { return null }
|
||||
loop(i < end) {
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i >= end { return null }
|
||||
if s.substring(i, i+1) == "}" { return null }
|
||||
if s.substring(i, i+1) != "\"" { return null }
|
||||
local key_end = this.read_string_end(s, i, end)
|
||||
if key_end == -1 { return null }
|
||||
local key_text = s.substring(i+1, key_end-1)
|
||||
i = key_end
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i >= end or s.substring(i, i+1) != ":" { return null }
|
||||
i = i + 1
|
||||
i = this.skip_ws(s, i, end)
|
||||
local vspan = this.read_value_span(s, i, end)
|
||||
if vspan == null { return null }
|
||||
if key_text == key { return s.substring(i, vspan.get(1)) }
|
||||
i = vspan.get(1)
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i < end and s.substring(i, i+1) == "," { i = i + 1 continue } else { return null }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the span of the idx-th element at top-level of an array slice [start,end)
|
||||
array_get_span(s, start, end, idx) {
|
||||
local i = start
|
||||
if i < end and s.substring(i, i+1) == "[" {
|
||||
i = i + 1
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
local cur = 0
|
||||
loop(i < end) {
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i >= end { return null }
|
||||
if s.substring(i, i+1) == "]" { return null }
|
||||
local vspan = this.read_value_span(s, i, end)
|
||||
if vspan == null { return null }
|
||||
if cur == idx { return vspan }
|
||||
i = vspan.get(1)
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i < end and s.substring(i, i+1) == "," {
|
||||
i = i + 1
|
||||
cur = cur + 1
|
||||
continue
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Return the text of idx-th element within an array slice, or null
|
||||
array_get_text(s, start, end, idx) {
|
||||
local sp = this.array_get_span(s, start, end, idx)
|
||||
if sp == null { return null }
|
||||
// Re-scan to compute exact [i,j) and return substring without ArrayBox.get
|
||||
local i = start
|
||||
if i < end and s.substring(i, i+1) == "[" { i = i + 1 } else { return null }
|
||||
local cur = 0
|
||||
loop(i < end) {
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i >= end { return null }
|
||||
if s.substring(i, i+1) == "]" { return null }
|
||||
local vspan = this.read_value_span(s, i, end)
|
||||
if vspan == null { return null }
|
||||
if cur == idx { return s.substring(i, vspan.get(1)) }
|
||||
i = vspan.get(1)
|
||||
i = this.skip_ws(s, i, end)
|
||||
if i < end and s.substring(i, i+1) == "," { i = i + 1 cur = cur + 1 continue } else { return null }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Read a JSON value span starting at i; returns [start,end)
|
||||
read_value_span(s, i, end) {
|
||||
if i >= end { return null }
|
||||
local ch = s.substring(i, i+1)
|
||||
if ch == "\"" {
|
||||
local j = this.read_string_end(s, i, end)
|
||||
if j == -1 { return null } else {
|
||||
local out = new ArrayBox()
|
||||
out.push(i)
|
||||
out.push(j)
|
||||
return out
|
||||
}
|
||||
}
|
||||
if ch == "{" {
|
||||
local j = this.matching_brace(s, i, end, "{", "}")
|
||||
if j == -1 { return null } else {
|
||||
local out = new ArrayBox()
|
||||
out.push(i)
|
||||
out.push(j)
|
||||
return out
|
||||
}
|
||||
}
|
||||
if ch == "[" {
|
||||
local j = this.matching_brace(s, i, end, "[", "]")
|
||||
if j == -1 { return null } else {
|
||||
local out = new ArrayBox()
|
||||
out.push(i)
|
||||
out.push(j)
|
||||
return out
|
||||
}
|
||||
}
|
||||
// number/bool/null: read until comma or closing
|
||||
local j = i
|
||||
loop(j < end) {
|
||||
local c = s.substring(j, j+1)
|
||||
if c == "," or c == "}" or c == "]" or this.is_ws_char(c) { break }
|
||||
j = j + 1
|
||||
}
|
||||
local out = new ArrayBox()
|
||||
out.push(i)
|
||||
out.push(j)
|
||||
return out
|
||||
}
|
||||
|
||||
// Find end index (exclusive) of a JSON string literal starting at i ('"')
|
||||
read_string_end(s, i, end) {
|
||||
local j = i + 1
|
||||
loop(j < end) {
|
||||
local c = s.substring(j, j+1)
|
||||
if c == "\\" {
|
||||
j = j + 2
|
||||
continue
|
||||
}
|
||||
if c == "\"" { return j + 1 }
|
||||
j = j + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Match paired braces/brackets with simple string-awareness; returns end index (exclusive)
|
||||
matching_brace(s, i, end, open, close) {
|
||||
local depth = 0
|
||||
local j = i
|
||||
loop(j < end) {
|
||||
local c = s.substring(j, j+1)
|
||||
if c == "\"" {
|
||||
j = this.read_string_end(s, j, end)
|
||||
if j == -1 { return -1 }
|
||||
continue
|
||||
}
|
||||
if c == open {
|
||||
depth = depth + 1
|
||||
} else {
|
||||
if c == close {
|
||||
depth = depth - 1
|
||||
if depth == 0 { return j + 1 }
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Whitespace utilities
|
||||
skip_ws(s, i, end) {
|
||||
local j = i
|
||||
loop(j < end and this.is_ws_char(s.substring(j, j+1))) { j = j + 1 }
|
||||
return j
|
||||
}
|
||||
is_ws_char(ch) { return ch == " " or ch == "\t" or ch == "\n" or ch == "\r" }
|
||||
// Note: normalization now lives in JsonNode (object_get/array_get)
|
||||
}
|
||||
7
apps/examples/json_query/nyash.toml
Normal file
7
apps/examples/json_query/nyash.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[using.json_native]
|
||||
path = "apps/lib/json_native/"
|
||||
main = "parser/parser.nyash"
|
||||
|
||||
[using.aliases]
|
||||
json = "json_native"
|
||||
|
||||
Reference in New Issue
Block a user