using: safer seam defaults (fix_braces OFF by default) + path-alias handling; json_native: robust integer parse + EscapeUtils unquote; add JsonCompat layer; builder: preindex static methods + fallback for bare calls; diagnostics: seam dump + function-call trace
This commit is contained in:
108
apps/lib/json_native/core/compat.nyash
Normal file
108
apps/lib/json_native/core/compat.nyash
Normal file
@ -0,0 +1,108 @@
|
||||
// Json Native Compat Layer — emulate JsonDocBox/JsonNodeBox API (MVP)
|
||||
// Purpose: Provide a thin, script-only compatibility wrapper so existing Ny code
|
||||
// that expects JsonDocBox/JsonNodeBox style methods can operate
|
||||
// with json_native without changing call sites.
|
||||
|
||||
using "apps/lib/json_native/parser/parser.nyash" as JsonParserUtils
|
||||
using "apps/lib/json_native/core/node.nyash" as JsonNode
|
||||
|
||||
// Box that mimics a document holder with parse()/root()/error()
|
||||
box JsonDocCompat {
|
||||
_root: Box
|
||||
_error: StringBox
|
||||
|
||||
birth() {
|
||||
me._root = null
|
||||
me._error = ""
|
||||
}
|
||||
|
||||
// Parse JSON text and populate state; returns bool success
|
||||
parse(text) {
|
||||
local node = JsonParserUtils.parse_json(text)
|
||||
if node == null {
|
||||
me._root = null
|
||||
me._error = "parse_error"
|
||||
return false
|
||||
}
|
||||
me._root = node
|
||||
me._error = ""
|
||||
return true
|
||||
}
|
||||
|
||||
// Return root node (wrapped by JsonNodeCompat)
|
||||
root() {
|
||||
if me._root == null { return null }
|
||||
return new JsonNodeCompat(me._root)
|
||||
}
|
||||
|
||||
// Return last error string (empty when no error)
|
||||
error() {
|
||||
return me._error
|
||||
}
|
||||
}
|
||||
|
||||
// Node wrapper that offers JsonNodeBox-like APIs
|
||||
box JsonNodeCompat {
|
||||
_node: Box
|
||||
|
||||
birth(node) {
|
||||
me._node = node
|
||||
}
|
||||
|
||||
// kind(): "null"|"bool"|"int"|"float"|"string"|"array"|"object"
|
||||
kind() {
|
||||
if me._node == null { return "null" }
|
||||
return me._node.get_kind()
|
||||
}
|
||||
|
||||
// get(k): object lookup
|
||||
get(key) {
|
||||
if me._node == null { return null }
|
||||
if me._node.get_kind() != "object" { return null }
|
||||
local v = me._node.object_get(key)
|
||||
if v == null { return null }
|
||||
return new JsonNodeCompat(v)
|
||||
}
|
||||
|
||||
// at(i): array lookup (0-based)
|
||||
at(i) {
|
||||
if me._node == null { return null }
|
||||
if me._node.get_kind() != "array" { return null }
|
||||
local v = me._node.array_get(i)
|
||||
if v == null { return null }
|
||||
return new JsonNodeCompat(v)
|
||||
}
|
||||
|
||||
// size(): array/object length
|
||||
size() {
|
||||
if me._node == null { return 0 }
|
||||
if me._node.get_kind() == "array" { return me._node.array_size() }
|
||||
if me._node.get_kind() == "object" {
|
||||
// MapBox.keys() is expected to exist on underlying map
|
||||
local keys = me._node.object_keys()
|
||||
return keys.length()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Scalar extractors
|
||||
str() {
|
||||
if me._node == null { return "" }
|
||||
return me._node.as_string()
|
||||
}
|
||||
int() {
|
||||
if me._node == null { return 0 }
|
||||
return me._node.as_int()
|
||||
}
|
||||
bool() {
|
||||
if me._node == null { return false }
|
||||
return me._node.as_bool()
|
||||
}
|
||||
|
||||
// toString (stringify underlying JSON)
|
||||
stringify() {
|
||||
if me._node == null { return "null" }
|
||||
return me._node.stringify()
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,11 +141,21 @@ box JsonScanner {
|
||||
is_digit_char(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"
|
||||
}
|
||||
|
||||
|
||||
// 16進数字判定(内蔵)
|
||||
is_hex_digit_char(ch) {
|
||||
return me.is_digit_char(ch) or ch == "a" or ch == "b" or ch == "c" or ch == "d" or ch == "e" or ch == "f" or ch == "A" or ch == "B" or ch == "C" or ch == "D" or ch == "E" or ch == "F"
|
||||
}
|
||||
|
||||
// 英字判定(内蔵)
|
||||
is_alpha_char(ch) {
|
||||
return (ch >= "a" and ch <= "z") or (ch >= "A" and ch <= "Z")
|
||||
}
|
||||
|
||||
// 英数字 + 下線(識別子向け)
|
||||
is_alphanumeric_or_underscore(ch) {
|
||||
return me.is_alpha_char(ch) or me.is_digit_char(ch) or ch == "_"
|
||||
}
|
||||
|
||||
// ===== 高レベル読み取りメソッド =====
|
||||
|
||||
@ -198,6 +208,18 @@ box JsonScanner {
|
||||
|
||||
return me.text.substring(start_pos, me.position)
|
||||
}
|
||||
|
||||
// 識別子を読み取り(英数字+アンダースコア)
|
||||
read_identifier() {
|
||||
local start_pos = me.position
|
||||
if not me.is_alpha_char(me.current()) and me.current() != "_" {
|
||||
return ""
|
||||
}
|
||||
loop(not me.is_eof() and me.is_alphanumeric_or_underscore(me.current())) {
|
||||
me.advance()
|
||||
}
|
||||
return me.text.substring(start_pos, me.position)
|
||||
}
|
||||
|
||||
// 数値文字列を読み取り
|
||||
read_number() {
|
||||
@ -344,4 +366,4 @@ box JsonScanner {
|
||||
to_debug_string() {
|
||||
return "Scanner{pos:" + me.position + "/" + me.length + ", line:" + me.line + ", col:" + me.column + ", context:'" + me.get_context(5) + "'}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
using "apps/lib/json_native/lexer/scanner.nyash" as JsonScanner
|
||||
using "apps/lib/json_native/lexer/token.nyash" as JsonToken
|
||||
using "apps/lib/json_native/utils/escape.nyash" as EscapeUtils
|
||||
// Removed other dependencies - using self-contained methods
|
||||
|
||||
// 🎯 高精度JSONトークナイザー(Everything is Box)
|
||||
@ -109,8 +110,8 @@ box JsonTokenizer {
|
||||
return new JsonToken("ERROR", "Unterminated string literal", start_pos, me.scanner.get_position())
|
||||
}
|
||||
|
||||
// エスケープ解除して値を取得
|
||||
local unescaped = me.unquote_string(literal)
|
||||
// エスケープ解除して値を取得(厳密版)
|
||||
local unescaped = EscapeUtils.unquote_string(literal)
|
||||
|
||||
// 文字列妥当性検証
|
||||
if not me.validate_string(unescaped) {
|
||||
@ -141,8 +142,8 @@ box JsonTokenizer {
|
||||
tokenize_keyword() {
|
||||
local start_pos = me.scanner.get_position()
|
||||
|
||||
// アルファベット文字を読み取り
|
||||
local keyword = me.scanner.read_while(this.is_identifier_char)
|
||||
// アルファベット/数字/下線を読み取り(関数参照を避ける安全版)
|
||||
local keyword = me.scanner.read_identifier()
|
||||
|
||||
// キーワード判定
|
||||
local token_type = me.keyword_to_token_type(keyword)
|
||||
@ -190,10 +191,7 @@ box JsonTokenizer {
|
||||
}
|
||||
}
|
||||
|
||||
// 識別子文字かどうか判定
|
||||
is_identifier_char(ch) {
|
||||
return me.is_alphanumeric_char(ch) or ch == "_"
|
||||
}
|
||||
|
||||
|
||||
// 数値形式の妥当性検証
|
||||
validate_number_format(num_str) {
|
||||
|
||||
@ -14,6 +14,8 @@ static box JsonParserModule {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
box JsonParser {
|
||||
tokens: ArrayBox // トークン配列
|
||||
position: IntegerBox // 現在のトークン位置
|
||||
@ -356,7 +358,8 @@ box JsonParser {
|
||||
out.set("value", StringUtils.parse_integer(number_str))
|
||||
return out
|
||||
}
|
||||
if StringUtils.is_float(number_str) {
|
||||
// Float detection disabled in MVP pending StringUtils float API stabilization
|
||||
if false {
|
||||
out.set("kind", "float")
|
||||
out.set("value", StringUtils.parse_float(number_str))
|
||||
return out
|
||||
|
||||
13
apps/lib/json_native/tests/compat_smoke.nyash
Normal file
13
apps/lib/json_native/tests/compat_smoke.nyash
Normal file
@ -0,0 +1,13 @@
|
||||
using "apps/lib/json_native/core/compat.nyash" as JsonCompat
|
||||
|
||||
print("compat: begin")
|
||||
|
||||
local doc = new JsonDocCompat()
|
||||
doc.parse("{\"hello\": [1,2,3], \"msg\": \"hi\"}")
|
||||
local root = doc.root()
|
||||
print("kind(root): " + root.kind())
|
||||
print("size(hello): " + root.get("hello").size())
|
||||
print("msg: " + root.get("msg").str())
|
||||
print("roundtrip: " + root.stringify())
|
||||
|
||||
print("compat: end")
|
||||
@ -236,6 +236,14 @@ static box StringUtils {
|
||||
}
|
||||
start = 1
|
||||
}
|
||||
|
||||
// 先頭ゼロの禁止("0" 単独は許可、符号付きの "-0" も許可)
|
||||
if s.length() - start > 1 and s.substring(start, start + 1) == "0" {
|
||||
// 2文字目以降が数字なら先頭ゼロ(不正)
|
||||
if this.is_digit(s.substring(start + 1, start + 2)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
local i = start
|
||||
loop(i < s.length()) {
|
||||
@ -252,84 +260,30 @@ static box StringUtils {
|
||||
if not this.is_integer(s) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 超簡易実装: よく使われる数値のみ対応
|
||||
// JSON処理に必要な数値パース実装
|
||||
// 基本数字 0-9
|
||||
if s == "0" { return 0 } if s == "1" { return 1 } if s == "2" { return 2 }
|
||||
if s == "3" { return 3 } if s == "4" { return 4 } if s == "5" { return 5 }
|
||||
if s == "6" { return 6 } if s == "7" { return 7 } if s == "8" { return 8 } if s == "9" { return 9 }
|
||||
|
||||
// よく使われる2桁数値 10-20
|
||||
if s == "10" { return 10 } if s == "11" { return 11 } if s == "12" { return 12 }
|
||||
if s == "13" { return 13 } if s == "14" { return 14 } if s == "15" { return 15 }
|
||||
if s == "16" { return 16 } if s == "17" { return 17 } if s == "18" { return 18 }
|
||||
if s == "19" { return 19 } if s == "20" { return 20 }
|
||||
|
||||
// JSON頻出数値
|
||||
if s == "42" { return 42 } if s == "100" { return 100 } if s == "200" { return 200 }
|
||||
if s == "404" { return 404 } if s == "500" { return 500 } if s == "1000" { return 1000 }
|
||||
|
||||
// 負数
|
||||
if s == "-1" { return -1 } if s == "-2" { return -2 } if s == "-10" { return -10 }
|
||||
|
||||
// TODO: より完全な数値パース実装が必要(算術演算による動的パース)
|
||||
// 現在はJSON処理によく使われる数値のみ対応
|
||||
return 0
|
||||
}
|
||||
|
||||
// 文字列が浮動小数点数表現かどうか(簡易版: 10進/指数部のみ対応)
|
||||
// 許容: [-+]? DIGITS '.' DIGITS ([eE] [+-]? DIGITS)?
|
||||
// | [-+]? DIGITS ([eE] [+-]? DIGITS)
|
||||
is_float(s) {
|
||||
if s.length() == 0 { return false }
|
||||
// 符号処理
|
||||
local start = 0
|
||||
if s.substring(0, 1) == "-" or s.substring(0, 1) == "+" {
|
||||
if s.length() == 1 { return false }
|
||||
start = 1
|
||||
// 汎用整数パース(符号と任意桁に対応)
|
||||
local neg = false
|
||||
local i = 0
|
||||
if s.substring(0, 1) == "-" {
|
||||
neg = true
|
||||
i = 1
|
||||
}
|
||||
local i = start
|
||||
local has_digit = false
|
||||
local has_dot = false
|
||||
local has_exp = false
|
||||
|
||||
local acc = 0
|
||||
loop(i < s.length()) {
|
||||
// 各桁の数値を計算
|
||||
local ch = s.substring(i, i + 1)
|
||||
if this.is_digit(ch) {
|
||||
has_digit = true
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
if ch == "." {
|
||||
if has_dot or has_exp { return false }
|
||||
has_dot = true
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
if ch == "e" or ch == "E" {
|
||||
if has_exp or not has_digit { return false }
|
||||
has_exp = true
|
||||
i = i + 1
|
||||
// 指数部符号
|
||||
if i < s.length() {
|
||||
local sgn = s.substring(i, i + 1)
|
||||
if sgn == "+" or sgn == "-" { i = i + 1 }
|
||||
}
|
||||
// 指数部の桁
|
||||
local j = i
|
||||
local exp_digit = false
|
||||
loop(j < s.length()) {
|
||||
local ch2 = s.substring(j, j + 1)
|
||||
if this.is_digit(ch2) { exp_digit = true j = j + 1 } else { break }
|
||||
}
|
||||
if not exp_digit { return false }
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
return false
|
||||
// '0'..'9' 前提(is_integer で検証済み)
|
||||
// 文字を数値へ: (ch - '0') 相当の分岐
|
||||
local digit = 0
|
||||
if ch == "0" { digit = 0 } else { if ch == "1" { digit = 1 } else { if ch == "2" { digit = 2 } else { if ch == "3" { digit = 3 } else {
|
||||
if ch == "4" { digit = 4 } else { if ch == "5" { digit = 5 } else { if ch == "6" { digit = 6 } else { if ch == "7" { digit = 7 } else {
|
||||
if ch == "8" { digit = 8 } else { if ch == "9" { digit = 9 } else { digit = 0 } } } } } } } } }
|
||||
acc = acc * 10 + digit
|
||||
i = i + 1
|
||||
}
|
||||
if not has_digit { return false }
|
||||
return has_dot or has_exp
|
||||
|
||||
if neg { return 0 - acc } else { return acc }
|
||||
}
|
||||
|
||||
// 浮動小数点の簡易パース(現段階は正規化のみ。数値演算は行わない)
|
||||
@ -360,3 +314,4 @@ static box StringUtils {
|
||||
return s.substring(s.length() - suffix.length(), s.length()) == suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user