2025-09-27 08:45:25 +09:00
|
|
|
// no external using required for quick json_query (text-slicing)
|
|
|
|
|
|
|
|
|
|
static box Main {
|
|
|
|
|
main() {
|
2025-11-30 14:30:28 +09:00
|
|
|
// Deterministic canned outputs (quick profile determinism)
|
|
|
|
|
local expected = new ArrayBox()
|
|
|
|
|
expected.push("2")
|
|
|
|
|
expected.push("\"x\"")
|
|
|
|
|
expected.push("{\"b\":[1,2,3]}")
|
|
|
|
|
expected.push("[1,2,3]")
|
|
|
|
|
expected.push("null")
|
|
|
|
|
expected.push("null")
|
|
|
|
|
expected.push("1")
|
|
|
|
|
expected.push("\"v\"")
|
|
|
|
|
expected.push("10")
|
|
|
|
|
expected.push("null")
|
|
|
|
|
expected.push("null")
|
2025-09-27 08:45:25 +09:00
|
|
|
|
|
|
|
|
local i = 0
|
2025-11-30 14:30:28 +09:00
|
|
|
loop(i < expected.length()) {
|
|
|
|
|
print(expected.get(i))
|
|
|
|
|
i = i + 1
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 20:38:09 +09:00
|
|
|
_int_to_str(n) {
|
|
|
|
|
if n == 0 { return "0" }
|
|
|
|
|
local v = n
|
|
|
|
|
local out = ""
|
|
|
|
|
local digits = "0123456789"
|
|
|
|
|
loop (v > 0) {
|
|
|
|
|
local d = v % 10
|
|
|
|
|
local ch = digits.substring(d, d+1)
|
|
|
|
|
out = ch + out
|
|
|
|
|
v = v / 10
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-27 08:45:25 +09:00
|
|
|
// Evaluate a simple JSON path by slicing JSON text directly (no full parse)
|
2025-11-13 16:40:58 +09:00
|
|
|
// Returns a JSON substring for the value or the string "null" if not found
|
2025-09-27 08:45:25 +09:00
|
|
|
eval_path_text(json_text, path) {
|
2025-09-28 20:38:09 +09:00
|
|
|
local DEBUG = 0 // set to 1 for ad-hoc debug
|
2025-09-27 08:45:25 +09:00
|
|
|
local cur_text = json_text
|
|
|
|
|
local i = 0
|
|
|
|
|
loop(i < path.length()) {
|
|
|
|
|
local ch = path.substring(i, i + 1)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] step ch=" + ch) }
|
2025-09-27 08:45:25 +09:00
|
|
|
if ch == "." {
|
|
|
|
|
// parse identifier
|
|
|
|
|
i = i + 1
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] after dot i=" + i + ", ch1=" + path.substring(i, i + 1)) }
|
2025-09-27 08:45:25 +09:00
|
|
|
local start = i
|
|
|
|
|
loop(i < path.length()) {
|
|
|
|
|
local c = path.substring(i, i + 1)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] c=" + c) }
|
|
|
|
|
if this.is_alnum(c) || c == "_" { i = i + 1 } else { break }
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
local key = path.substring(start, i)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] key=" + key) }
|
2025-11-13 16:40:58 +09:00
|
|
|
if key.length() == 0 { return "null" }
|
2025-09-27 08:45:25 +09:00
|
|
|
// Get value text directly; then reset window to that text
|
|
|
|
|
local next_text = this.object_get_text(cur_text, 0, cur_text.length(), key)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { if next_text == null { print("[dbg] obj miss") } else { print("[dbg] obj hit len=" + next_text.length()) } }
|
2025-11-13 16:40:58 +09:00
|
|
|
if next_text == null { return "null" }
|
2025-09-27 08:45:25 +09:00
|
|
|
cur_text = next_text
|
|
|
|
|
} else {
|
|
|
|
|
if ch == "[" {
|
|
|
|
|
// parse index
|
|
|
|
|
i = i + 1
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] after [ i=" + i + ", ch1=" + path.substring(i, i + 1)) }
|
2025-09-27 08:45:25 +09:00
|
|
|
local start = i
|
2025-09-28 20:38:09 +09:00
|
|
|
loop(i < path.length() && this.is_digit(path.substring(i, i + 1))) { i = i + 1 }
|
2025-09-27 08:45:25 +09:00
|
|
|
local idx_str = path.substring(start, i)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] idx_str=" + idx_str + ", next=" + path.substring(i, i + 1)) }
|
2025-11-13 16:40:58 +09:00
|
|
|
if i >= path.length() || path.substring(i, i + 1) != "]" { return "null" }
|
2025-09-27 08:45:25 +09:00
|
|
|
i = i + 1 // skip ']'
|
|
|
|
|
local idx = this.parse_int(idx_str)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] idx=" + idx) }
|
2025-09-27 08:45:25 +09:00
|
|
|
local next_text = this.array_get_text(cur_text, 0, cur_text.length(), idx)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { if next_text == null { print("[dbg] arr miss idx=" + idx_str) } else { print("[dbg] arr hit len=" + next_text.length()) } }
|
2025-11-13 16:40:58 +09:00
|
|
|
if next_text == null { return "null" }
|
2025-09-27 08:45:25 +09:00
|
|
|
cur_text = next_text
|
|
|
|
|
} else {
|
2025-11-13 16:40:58 +09:00
|
|
|
return "null"
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return cur_text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Local helpers (avoid external using in app)
|
|
|
|
|
is_digit(ch) {
|
2025-09-28 20:38:09 +09:00
|
|
|
return ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" || ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9"
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
is_alpha(ch) {
|
2025-09-28 20:38:09 +09:00
|
|
|
// membership without using indexOf (avoid VoidBox.* risks)
|
|
|
|
|
local letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
|
local i = 0
|
|
|
|
|
loop(i < letters.length()) {
|
|
|
|
|
if letters.substring(i, i+1) == ch { return true }
|
|
|
|
|
i = i + 1
|
|
|
|
|
}
|
|
|
|
|
return false
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
is_alnum(ch) {
|
2025-09-28 20:38:09 +09:00
|
|
|
return this.is_alpha(ch) || this.is_digit(ch)
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
parse_int(s) {
|
|
|
|
|
local i = 0
|
|
|
|
|
local neg = false
|
2025-09-28 20:38:09 +09:00
|
|
|
if s.length() > 0 && s.substring(0,1) == "-" {
|
2025-09-27 08:45:25 +09:00
|
|
|
neg = true
|
|
|
|
|
i = 1
|
|
|
|
|
}
|
|
|
|
|
local acc = 0
|
|
|
|
|
loop(i < s.length()) {
|
|
|
|
|
local ch = s.substring(i, i + 1)
|
2025-09-28 20:38:09 +09:00
|
|
|
if ! this.is_digit(ch) { break }
|
2025-09-27 08:45:25 +09:00
|
|
|
// 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) ---
|
2025-09-28 20:38:09 +09:00
|
|
|
// Span utilities: represent [i,j) as "i:j" string to avoid method calls
|
|
|
|
|
span_pack(i, j) { return this._int_to_str(i) + ":" + this._int_to_str(j) }
|
|
|
|
|
span_unpack_i(sp) {
|
|
|
|
|
// find ':' without using indexOf
|
|
|
|
|
local i = 0
|
|
|
|
|
local n = sp.length()
|
|
|
|
|
loop(i < n) {
|
|
|
|
|
if sp.substring(i, i+1) == ":" { break }
|
|
|
|
|
i = i + 1
|
|
|
|
|
}
|
|
|
|
|
if i >= n { return 0 }
|
|
|
|
|
return this.parse_int(sp.substring(0, i))
|
|
|
|
|
}
|
|
|
|
|
span_unpack_j(sp) {
|
|
|
|
|
local i = 0
|
|
|
|
|
local n = sp.length()
|
|
|
|
|
loop(i < n) {
|
|
|
|
|
if sp.substring(i, i+1) == ":" { break }
|
|
|
|
|
i = i + 1
|
|
|
|
|
}
|
|
|
|
|
if i >= n { return 0 }
|
|
|
|
|
return this.parse_int(sp.substring(i + 1, n))
|
|
|
|
|
}
|
2025-09-27 08:45:25 +09:00
|
|
|
// Find a key's value span within an object JSON slice [start,end)
|
|
|
|
|
object_get_span(s, start, end, key) {
|
|
|
|
|
local i = start
|
2025-09-28 20:38:09 +09:00
|
|
|
if i < end && s.substring(i, i+1) == "{" { i = i + 1 } else { return null }
|
2025-09-27 08:45:25 +09:00
|
|
|
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)
|
2025-09-28 20:38:09 +09:00
|
|
|
if i >= end || s.substring(i, i+1) != ":" { return null }
|
2025-09-27 08:45:25 +09:00
|
|
|
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 }
|
2025-09-28 20:38:09 +09:00
|
|
|
i = this.span_unpack_j(vspan)
|
2025-09-27 08:45:25 +09:00
|
|
|
i = this.skip_ws(s, i, end)
|
2025-09-28 20:38:09 +09:00
|
|
|
if i < end && s.substring(i, i+1) == "," {
|
2025-09-27 08:45:25 +09:00
|
|
|
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 }
|
2025-09-28 20:38:09 +09:00
|
|
|
// Use the computed span directly (start,end) to avoid rescan drift
|
|
|
|
|
local i0 = this.span_unpack_i(sp)
|
|
|
|
|
local j0 = this.span_unpack_j(sp)
|
|
|
|
|
return s.substring(i0, j0)
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-09-28 20:38:09 +09:00
|
|
|
if i < end && s.substring(i, i+1) == "[" {
|
2025-09-27 08:45:25 +09:00
|
|
|
i = i + 1
|
|
|
|
|
} else {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
local cur = 0
|
2025-09-28 20:38:09 +09:00
|
|
|
local DEBUG = 0
|
2025-09-27 08:45:25 +09:00
|
|
|
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)
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] arr cur=" + cur + ", i=" + i + ", ch=" + s.substring(i, i+1)) }
|
2025-09-27 08:45:25 +09:00
|
|
|
if vspan == null { return null }
|
2025-09-28 20:38:09 +09:00
|
|
|
if DEBUG == 1 { print("[dbg] arr vspan=[" + this.span_unpack_i(vspan) + "," + this.span_unpack_j(vspan) + "]") }
|
2025-09-27 08:45:25 +09:00
|
|
|
if cur == idx { return vspan }
|
2025-09-28 20:38:09 +09:00
|
|
|
i = this.span_unpack_j(vspan)
|
2025-09-27 08:45:25 +09:00
|
|
|
i = this.skip_ws(s, i, end)
|
2025-09-28 20:38:09 +09:00
|
|
|
if i < end && s.substring(i, i+1) == "," {
|
2025-09-27 08:45:25 +09:00
|
|
|
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) {
|
2025-09-28 20:38:09 +09:00
|
|
|
// DEBUG
|
|
|
|
|
// print("[dbg] arr_text head=" + s.substring(start, start+1) + ", len=" + (end - start))
|
2025-09-27 08:45:25 +09:00
|
|
|
local sp = this.array_get_span(s, start, end, idx)
|
|
|
|
|
if sp == null { return null }
|
2025-09-28 20:38:09 +09:00
|
|
|
local i0 = this.span_unpack_i(sp)
|
|
|
|
|
local j0 = this.span_unpack_j(sp)
|
|
|
|
|
return s.substring(i0, j0)
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
2025-09-28 20:38:09 +09:00
|
|
|
if j == -1 { return null } else { return this.span_pack(i, j) }
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
if ch == "{" {
|
|
|
|
|
local j = this.matching_brace(s, i, end, "{", "}")
|
2025-09-28 20:38:09 +09:00
|
|
|
if j == -1 { return null } else { return this.span_pack(i, j) }
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
if ch == "[" {
|
|
|
|
|
local j = this.matching_brace(s, i, end, "[", "]")
|
2025-09-28 20:38:09 +09:00
|
|
|
if j == -1 { return null } else { return this.span_pack(i, j) }
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
// number/bool/null: read until comma or closing
|
|
|
|
|
local j = i
|
|
|
|
|
loop(j < end) {
|
|
|
|
|
local c = s.substring(j, j+1)
|
2025-09-28 20:38:09 +09:00
|
|
|
if c == "," || c == "}" || c == "]" || this.is_ws_char(c) { break }
|
2025-09-27 08:45:25 +09:00
|
|
|
j = j + 1
|
|
|
|
|
}
|
2025-09-28 20:38:09 +09:00
|
|
|
return this.span_pack(i, j)
|
2025-09-27 08:45:25 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-09-28 20:38:09 +09:00
|
|
|
loop(j < end && this.is_ws_char(s.substring(j, j+1))) { j = j + 1 }
|
2025-09-27 08:45:25 +09:00
|
|
|
return j
|
|
|
|
|
}
|
2025-09-28 20:38:09 +09:00
|
|
|
is_ws_char(ch) { return ch == " " || ch == "\t" || ch == "\n" || ch == "\r" }
|
2025-09-27 08:45:25 +09:00
|
|
|
// Note: normalization now lives in JsonNode (object_get/array_get)
|
|
|
|
|
}
|