方案文件: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} 任意字符串键 objindexarray [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.name、arr.0p/(斜杠层级)目录树(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 而非 int、char/utf32 而非 string),而非「变量在编译期有静态类型」。因此 json/strkeymapindex/objindex 的容器元素可动态 (运行时 kind 自描述),但每个元素落盘时 kind 仍精确——值严格、容器动态 ,两级不冲突。
现有 type_expr 的 atom = [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 是严格六元闭包,非宽泛 union :json 只含 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 映射 JSON kindexp 存储 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 反序列化时唯一判别依据是父节点落盘 kind :strkeymapindex → 按 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 命名 → 已定 :obj→objindex、map→strkeymapindex(「字符串键 map」+「index」后缀,与 index/objindex/extindex 一族)。obj/map 是否留别名 → 已定 :破坏性改名,不留别名 。obj/map 从 known_kind、kvkind、runtime_internal.h、builtin.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. 承接
Related: array2d/byteseek#14
XValue 类型系统与 JSON 全类型 key 布局
1. 核心约束与统一模型
1.1 key 是字符串
kvspace 是一棵 KV 树,每个 key 都是路径字符串(如
/vthread/1/arr.0)。不存在「整数 key」「非字符串下标」——散 key 数组的「下标」0是字符串"0",与成员名"name"是同一类事物。由此得到一个统一模型:
{"x":1}objindex[a,b,c]"0" "1" "2"strkeymapindex二者存储层完全同构(都是
p.<字符串键>),kind 只作语义标注(有序 vs 无序),不做存储形态区分。1.2 三态 key(存储层显式区分)
复合值的「单值」与「键值树」由 key 后缀显式区分,不看值内容、不做 None 探测:
p(无后缀)/data/x、/data/cont(dims 在 head)p.k(点号 + 字符串键)obj.name、arr.0p/(斜杠层级)/lib/main/ndim/dims在 XValueHead,元素连续打包在 body。strkeymapindex/objindex)+ 子键p.k,每个元素一个 key。这条把「是 compact 还是散 key」从「读值猜」(当前 runtime 靠读参是否 None 探测)改成「看 key 后缀」——有
.就是字符串键树,没有就是单值。2. kindexp 递归化
现有
type_expr的atom = [dims] (any|kind)不支持递归,[]obj语义未定、map漏在known_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 映射
nullNonep(空 TLV / key 缺席,读为 None)true/falseboolpint64pfloat64pchar/utf32p(单 XValue,ndim=1)strkeymapindexp(目录标记)+p.0p.1….数字串子键objindexp(目录标记)+p.name….任意串子键4. 递归嵌套的 key 组合
三类 key 后缀可任意叠加,构成任意嵌套 JSON 的路径。用户语法
arr[i]是语法糖,desugar 成arr.<str(i)>(i转字符串键):{"a": [1, {"b": "x"}], "d": 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 的选择
[]Tstrkeymapindexstrkeymapindex5.4 JSON key 字符约束(免转义,纯拼接)
JSON object 的 key(成员名)约束为不含影响 kvspace 存储分隔的字符,据此
json ↔ KV转换直接拼接,不做任何转义:/(层级)、.(成员)、[](下标)、\n/\r(body 成员名列表分隔)、\0(C 串终止)、‥U+2025(kvspace 私有后缀)、ASCII 控制字符(< 0x20)。_、-、Unicode(中文/emoji 等非分隔字符)。json.from遇含禁止字符的 key 报错拒绝(不静默丢键、不转义)。"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]:成员数 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:
p(目录 key)ZADD/ZREM/ZRANGE)p‥head[4B count LE],存成员 size)‥是RUNTIME_MEMBER_SEP(U+2025),kvspace 私有后缀,list(p)自动隐藏,对 kvlang 不可见。ZADD/ZREM(O(log N)),不再整块重写;ZCARD与‥head里的 count 互为校验。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. 与既有文档的演进关系
.+ 数字字符串(arr.0),与成员 key 同构。runtime 的separated_*与for-in两套不再并存,统一走.数字串。[]char、裸 kind=单值、[]⇔[?]一维,均不变;新增json/none/strkeymapindex递归 kind。objindex/strkeymapindex存储同构(字符串键),kind 是语义标注;本文确认其与 JSON object/array 的一一对应。8. 待定项
散 key 目录标记 XValue 的 body 是否存长度→ 已定:存[4B count LE](§6.1)。→ 已定:map命名obj→objindex、map→strkeymapindex(「字符串键 map」+「index」后缀,与index/objindex/extindex一族)。→ 已定:破坏性改名,不留别名。obj/map是否留别名obj/map从known_kind、kvkind、runtime_internal.h、builtin.c、codec 全部移除,同步改 parser/lower 与全部 tutorial、136/136 回归;旧 kind 串落盘即报错,不做兼容读取。→ 已定:下标只接受整数表达式,desugar 为数字字符串键(arr[i]下标是否允许字符串键arr[i]→arr.<str(i)>);字符串键访问走成员.name,二者不重合。→ 已定:不新增 kind,kvspacenone/nullkind 名None(空 TLV)即 JSONnull(§5.1)。9. 承接
json六元闭包(union),但 head 的kind[32]无法承载|并集,定义侧须拆出 DefHeader。