Skip to content

other/JSON 全类型 key 布局方案(key 转义 + 数组对象表示) #67

Description

@miaobyte

方案文件:XValue类型系统与JSON全类型key布局.md(解决 byteseek 复杂 JSON 序列化问题)

Related: array2d/byteseek#14


XValue 类型系统与 JSON 全类型 key 布局

本文定义 kvlang XValue 的类型表达式(kindexp)如何递归覆盖 JSON 全部类型,以及存储层 key 路径如何显式区分 compact 数组与成员数组(散 key)。
核心约束先行:key 必须是字符串。因此数组下标与成员名在存储层都是字符串键,不构成两类东西。

1. 核心约束与统一模型

1.1 key 是字符串

kvspace 是一棵 KV 树,每个 key 都是路径字符串(如 /vthread/1/arr.0)。不存在「整数 key」「非字符串下标」——散 key 数组的「下标」0 是字符串 "0",与成员名 "name" 是同一类事物。

由此得到一个统一模型:

复合值 = 「字符串键 → 子值」的树。数组与对象在存储层同构,区别仅在键的约定与 kind 标记。

JSON 结构键约定kind 标记
object {"x":1}任意字符串键objindex
array [a,b,c]连续数字字符串键 "0" "1" "2"strkeymapindex

二者存储层完全同构(都是 p.<字符串键>),kind 只作语义标注(有序 vs 无序),不做存储形态区分。

1.2 三态 key(存储层显式区分)

复合值的「单值」与「键值树」由 key 后缀显式区分,不看值内容、不做 None 探测:

key 形态语义例子
p(无后缀)单 XValue:标量 / compact 数组 / 字符串/data/x/data/cont(dims 在 head)
p.k(点号 + 字符串键)成员 / 散 key 元素,k 是字符串obj.namearr.0
p/(斜杠层级)目录树(index)/lib/main/
  • compact:单 XValue,key 无后缀,ndim/dims 在 XValueHead,元素连续打包在 body。
  • 成员数组(散 key):目录标记 XValue(kind=strkeymapindex/objindex)+ 子键 p.k,每个元素一个 key。

这条把「是 compact 还是散 key」从「读值猜」(当前 runtime 靠读参是否 None 探测)改成「看 key 后缀」——有 . 就是字符串键树,没有就是单值。

2. kindexp 递归化

严格类型 = XValue kind 精确:kvlang 的「严格类型」指每个落盘的 XValue 其 kind 精确、定宽、无别名int64 而非 intchar/utf32 而非 string),而非「变量在编译期有静态类型」。因此 json/strkeymapindex/objindex容器元素可动态(运行时 kind 自描述),但每个元素落盘时 kind 仍精确——值严格、容器动态,两级不冲突。

现有 type_expratom = [dims] (any|kind) 不支持递归,[]obj 语义未定、map 漏在 known_kind。扩展为递归文法:

json := None | bool | int64 | float64 | char/utf32 | strkeymapindex | objindex # 递归闭包
kind += "json" # 任意 JSON 值(递归 union)
| "strkeymapindex" # 散 key 序列:有序下标,键为数字字符串,元素 json
| "objindex" # 命名成员对象:键为任意字符串,值 json

json 是严格六元闭包,非宽泛 unionjson 只含 None | bool | int64 | float64 | char/utf32 | strkeymapindex | objindex 六元。float32/int8/uint64/char/utf8/char/ascii 等精确 kind 不是 JSON 类型——写入 json 容器(或经 json.from 入)时报错拒绝,须先显式转成六元之一(如 float32→float64)。由此「值严格、容器动态」的「严格」收紧为六元:容器元素 kind 可随值自描述,但取值范围封闭在六元内。

数组两种形态的显式约定

  • []T / [d0,d1]T = compact ndarray,T 必须是定宽标量(bool/int/uint/float)。[]char/utf32 是「字符串」(单 XValue,ndim=1)。
  • strkeymapindex = 散 key 序列,元素任意 json,变长可增删。
  • objindex = 命名成员,值任意 json
  • []obj[]json = 不合法(元素变长不能 compact)——对象数组、异构数组一律写 strkeymapindex

于是「对象数组」「字符串数组」不再是「无类型标注的模糊态」,而是一个明确的 strkeymapindex(元素是 objindex / char)。[] 恒 compact、strkeymapindex 恒散 key,语法层无歧义。

3. JSON 全类型 → kindexp → key 映射

JSONkindexp存储 key识别
nullNonep(空 TLV / key 缺席,读为 None)读为 None
true/falseboolphead kind=bool
整数int64phead kind=int64
浮点float64phead kind=float64
字符串char/utf32p(单 XValue,ndim=1)head kind=char/*
数组strkeymapindexp(目录标记)+ p.0p.1目录 kind=strkeymapindex + .数字串 子键
对象objindexp(目录标记)+ p.name目录 kind=objindex + .任意串 子键

反向映射以父节点 kind 为准strkeymapindex 元素键与 objindex 成员键在存储层同为 p.<字符串键>(§5.4 允许数字入成员名,故 p.0 既可能是数组第 0 项、也可能是名为 "0" 的成员)。json.to 反序列化时唯一判别依据是父节点落盘 kindstrkeymapindex → 按 score 数值升序输出数组,objindex → 输出对象;二者不可靠子键名字面量区分。

4. 递归嵌套的 key 组合

三类 key 后缀可任意叠加,构成任意嵌套 JSON 的路径。用户语法 arr[i] 是语法糖,desugar 成 arr.<str(i)>i 转字符串键):

{"a": [1, {"b": "x"}], "d": null}
root (objindex, 目录标记)
├── root.a (成员, 值=strkeymapindex 目录标记)
│ ├── root.a.0 = 1 (散 key 元素, 键 "0")
│ └── root.a.1 (元素, 值=objindex 目录标记)
│ └── root.a.1.b = "x" (成员)
└── root.d = None (JSON null)

每个路径段靠后缀 + 父节点 kind 判别:无后缀 = 单值;带 . = 键值树。键值树里是「序列元素」还是「成员」由父节点 kind 决定——strkeymapindex.数字串 是序列元素,objindex/index.任意串 是成员;单看 p.0 后缀无法区分「数组第 0 项」与「名为 "0" 的成员」。判别不再依赖读值、不再有 None 探测。

5. 语义细节

5.1 JSON null = None(不新增 kind,可无损往返)

kvspace 的 None(空 TLV)直接对应 JSON null,不新增 kind。

无损往返的关键:kvspace 底层能区分「key 存在但空字节」与「key 不存在」——store.set(key, &[]) 创建空 key、scan/list 可列出,get 读回 None。因此:

  • json.from(null)写空字节 key(key 存在,值 encode 为空),而非「不写 key」。
  • json.to 序列化 objindex/strkeymapindex按成员列表(body 成员名 / ZSET)遍历,而非「读值非 None 才算成员」。

由此 {"d": null}root.d(空字节 key,在成员列表里)→ 还原 {"d":null}{} 无成员 → {}。null 与缺失在存储层可区分,不合并。

5.2 异构数组与同构数组的落盘

strkeymapindex(散 key)元素任意,天然支持异构 [1,"a",true]json.from 默认落 strkeymapindex,保证任意 JSON 数组无损往返;同构定宽数组可后续用 array.compact 压成 compact(优化,非正确性要求)。

5.3 compact 与散 key 的选择

场景形态理由
定宽数值/布尔元素、多维张量compact []T单 XValue,O(1) 元素寻址,对齐 tensor 连续布局
字符串/obj/map 元素、异构、变长散 key strkeymapindex元素变长无法紧凑打包
JSON 反序列化默认 strkeymapindex任意异构无损,compact 是后续优化

5.4 JSON key 字符约束(免转义,纯拼接)

JSON object 的 key(成员名)约束为不含影响 kvspace 存储分隔的字符,据此 json ↔ KV 转换直接拼接,不做任何转义

  • 禁止字符/(层级)、.(成员)、[](下标)、\n/\r(body 成员名列表分隔)、\0(C 串终止)、 U+2025(kvspace 私有后缀)、ASCII 控制字符(< 0x20)。
  • 允许字符:字母、数字、_-、Unicode(中文/emoji 等非分隔字符)。
  • json.from 遇含禁止字符的 key 报错拒绝(不静默丢键、不转义)。
  • 字符串值(value)不受此限"a.b\nx" 作为值存 body,任意字符皆可——约束只落在 key/成员名。

由此 {"a.b":1} 报错(而非歧义成 {"a":{"b":1}}),{"a":{"b":1}}p.a.b,二者无需转义即可区分。JSON「类型」全覆盖(null/bool/number/string/array/object),「key 字符集」是命名约束,与类型正交。

6. index 类 XValue 的 body 与存储层特别实现

6.1 成员长度必入 body

index/objindex/strkeymapindex/extindex 四类 index 的 body 一律存成员长度,前缀 [4B count LE]

index/objindex/strkeymapindex: body = [4B count LE][name1\nname2...]
extindex: body = [4B count LE][…extpath][name1\nname2...] # extpath 不计入 count

成员数 O(1) 取,不再靠 split('\n') 现数。此 body 是 index XValue 的规范自描述:成员名列表是成员树(子键 p.<name>)的快照,成员在子键里;增删成员时两者同步。fs/shm 后端 body 权威,redis 后端成员名权威在 ZSET、body 由 ZSET 合成(见 §6.2)——两 impl 对「合成后的规范 body」语义对称,底层存储各自实现。

6.2 redis 后端:index 类改原生 ZSET(特别实现,不走统一 TLV 落盘)

redis 后端把 index 类从「整块 TLV blob 存 STRING + 整块 read-modify-write」改成两个 redis key

key存什么
p(目录 key)Redis ZSET(成员名 → score,ZADD/ZREM/ZRANGE
p‥headheader XValue(kind/ro/vid + body=[4B count LE],存成员 size)
  • RUNTIME_MEMBER_SEP(U+2025),kvspace 私有后缀,list(p) 自动隐藏,对 kvlang 不可见。
  • 增删成员走 ZADD/ZREM(O(log N)),不再整块重写;ZCARD‥head 里的 count 互为校验。
  • score 语义strkeymapindex(有序数组)score = 数值下标(strtol);objindex/index(无序)score 统一 0 或写入序,不参与排序。
  • get(p) 读时由 ZSET(members) + ‥head(header) 合成完整 index XValue(head + body=[count][join(members)])。

6.3 list 顺序

  • strkeymapindex:按 score 数值升序返回(strtol 比较,非字节序——"10" 排在 "2" 后),保证 JSON array 顺序无损。
  • json.to 序列化 strkeymapindex按成员列表紧凑输出:score 缺失(delete 空洞)跳过;成员存在但值为空字节(显式 null)输出 null。二者由「成员在不在 ZSET」天然区分,无需额外约定。
  • objindex / index:不保证顺序(ZSET score 无排序语义)。
  • list 只返回成员名,隐藏 ‥head 等 kvspace 私有 key。

7. 与既有文档的演进关系

  • 承接 [[key系统与数组访问新方案]]:本文以「key 是字符串」为前提统一为单一约定:散 key 元素 key = . + 数字字符串(arr.0),与成员 key 同构。runtime 的 separated_*for-in 两套不再并存,统一走 .数字串
  • 承接 [[kindexp-bare-kind-and-string-shape]]:字符串恒一维 []char、裸 kind=单值、[][?] 一维,均不变;新增 json/none/strkeymapindex 递归 kind。
  • 承接 issue 1-layout/dict kind 拆分为 obj + map #49(dict 拆 obj + map):objindex/strkeymapindex 存储同构(字符串键),kind 是语义标注;本文确认其与 JSON object/array 的一一对应。

8. 待定项

  • 散 key 目录标记 XValue 的 body 是否存长度已定:存 [4B count LE](§6.1)。
  • map 命名已定objobjindexmapstrkeymapindex(「字符串键 map」+「index」后缀,与 index/objindex/extindex 一族)。
  • obj/map 是否留别名已定破坏性改名,不留别名obj/mapknown_kindkvkindruntime_internal.hbuiltin.c、codec 全部移除,同步改 parser/lower 与全部 tutorial、136/136 回归;旧 kind 串落盘即报错,不做兼容读取。
  • arr[i] 下标是否允许字符串键已定:下标只接受整数表达式,desugar 为数字字符串键(arr[i]arr.<str(i)>);字符串键访问走成员 .name,二者不重合。
  • none/null kind 名已定:不新增 kind,kvspace None(空 TLV)即 JSON null(§5.1)。

9. 承接

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions