背景与动机
kvlang 目前没有具名结构体。要表达"一条有固定字段、字段类型各异的记录",只能用无类型 {}(object)+ · 成员访问手搓(见 tutorial/12-struct/*:链表/树/图全是 l·head·val = 10 这种裸 dict)。缺点:
- 字段名写错、类型给错,无从检查,错误静默。
- 没有"类型"这个一等实体:无法声明
p: Point、无法在 rwir 签名里约束参数是某结构。
- 与
#67(否决递归 JSON 文法)、no-json-kind(JSON 全类型靠 object/stringkeymap + 动态元素覆盖)留下的缺口对应——异构具名记录这一档一直空缺。
关键观察:struct 与 stringkeymap 在物理存储上完全同构,差别只在 schema。因此 struct 不需要新的存储机制,只需让实例 kindexpr 指向 /lib 下的类型定义节点,并复用 stringkeymap 的 · 成员访问。
符号职责:/ 指向类型,· 访问成员
/ = tree index:类型定义就是 KV 树里的一个节点,实例的 kindexpr 直接写这个节点的完整路径 /{lib}/{structname}(如 /lib/Point)。引用一个类型 = 指向它在树里的位置,天然用 /。
· = member index:只用于访问实例成员 p·x(与 stringkeymap m·key 同符号)。
两者不混用:类型引用永不出现 ·,成员访问永不出现 /。
核心设计:struct = 注册在 /lib 的具名类型(kind=struct),实例 kindexpr = /{lib}/{structname}
沿用容器方案2(kvlang-container-memindex-split):任何容器落盘恒三层
p —— 值 XValue(kind 标记 + dims 落 head,body 空)
p· —— memindex(kind=index,body=[4B count][name\n…],成员列表唯一权威)
p·k —— 成员值(独立 key)
struct 实例完全套用这套,唯一变化是 p 的 kind 不是 stringkeymap/object,而是类型定义节点的完整路径 /{lib}/{structname}。
1. 定义(definition)
结构定义注册进 lib 命名空间(与 rwir/rwfunc 同级,/ 分隔):
struct Point {
x:float64=0.0
y:float64=0.0
}
字段语法 name:kindexpr 或 name:kindexpr=default(冒号分隔类型,可选 = 给默认值)。kindexpr 即签名类型表达式(float64、[]float32、[]char/utf8·int64、乃至另一个 struct 路径 /lib/Node)。layout 产物(默认包,pkg 规则与 rwfunc 一致):
/lib/Point kind=struct # 类型节点:其 kindexpr 就是关键字 struct(一档新 kind)
/lib/Point· kind=index # 字段列表 memindex: "x\ny"
/lib/Point·x kind=float64 =0.0 # 默认值 XValue:头部 kind 即字段 kindexpr,body 即默认值
/lib/Point·y kind=float64 =0.0
关键:字段描述符 /lib/Point·x 存的就是默认值 XValue —— XValue head 本就携带 kindexpr(kind+dims),body 就是默认值,一举给出"字段类型 + 字段默认值"两件事,无需另存类型字符串。省略 =default 时落该 kindexpr 的零值。
于是 /lib/Point· 整棵子树 = 一个默认值原型实例(一个 x=0,y=0 的 Point)。/lib/Point 这个 XValue 的 kind 就是 struct(不是 []char/utf8·[]char/utf8);struct 与 rwir/rwfunc/scope/stringkeymap 并列,进 known_kind。
2. 实例 kindexpr
实例的 kind(XValue head 里的 kindexpr)= 类型定义节点的完整路径 /lib/Point(/ 分隔,无 ·)。
- 解析(runtime,非 layout):kindexpr
/lib/Point 就是定义路径,直接据它读 · memindex 与各字段 kindexpr,无需任何变换。
- 成员访问另用
·(p·x),与类型引用的 / 井水不犯河水。
3. 创建(creation)
具名字面量(对齐 Go/Rust):
p = Point{x = 1.0, y = 2.0}
p2 = Point{x = 1.0} # y 省略 → 取定义里的默认值 0.0
p3 = Point{} # 全省 → 全默认值(= 克隆原型)
创建语义 = 克隆原型 + 覆盖给定字段:runtime 拷贝 /lib/Point· 子树(默认值原型)到实例根,再用字面量给出的字段覆盖。实例值标记 kind 记为 /lib/Point:
p kind=/lib/Point # 实例值标记(kind = 类型节点完整路径)
p· kind=index # "x\ny"
p·x float64 1.0
p·y float64 2.0
字段存在性、字段类型是否匹配定义、省略字段取默认 —— 由 runtime 完成,layout 不读 /lib(见"检查职责")。
4. 读写整体(whole read/write)
q = p —— 深拷贝子树(p、p·、p·*)到 q,值语义(对齐 C/Go/Rust 的 struct 值拷贝)。
- 作 rwir 读参:按根路径
p 传入,读参读整棵子树;作写参:写整棵子树。
kv·get/set/deltree 作用于 p 走既有容器子树语义,struct 不新增原语。
5. 读写成员(member read/write)—— 与 stringkeymap 共享 ·
- 读:
p·x → 单跳 kv get key p·x。静态成员链在 layout 编译期拼完整路径(#110),a·b·c → key a·b·c。
- 写:
p·x = 3.0 → kv·set(p·x, 3.0)。
- 与 stringkeymap
m·"key" 字面同形(同一个 ·);差别仅在字段是编译期名 vs 运行期值。
6. 回收(reclaim/free)
kv·deltree(p):memindex p· 是权威成员表,遍历它删除每个 p·field;字段自身是容器/struct/extindex 则递归;extindex 字段(落扩展存储的大值)经其扩展句柄释放。与 object/stringkeymap 完全同一套 GC,无新机制。
- scope 内的局部 struct 随帧结束回收。
检查职责:layout 不检查 /lib
layout 不读、不校验 kvspace 的 /lib(与 extension-rwir-not-hardcoded 一致:判定全留给 runtime)。因此:
- layout 只负责:解析
struct Point{…} → lower 为 /lib/Point 注册;解析 Point{…} 字面量、p·x 成员访问 → lower 为路径与容器落盘。
- 字段存在性、字段类型匹配 —— runtime 拿 kindexpr 路径
/lib/Point,读其 · memindex 与各字段 kindexpr 做判定并报错。
- 类型是否已注册 —— 同样 runtime 判定(
/lib/Point 不存在则 runtime 报错)。
参考五大语言
|
定义 |
创建 |
成员访问 |
回收 |
| C |
struct Point{double x,y;} |
struct Point p={1,2}; |
p.x |
栈自动 / free |
| Go |
type Point struct{X,Y float64} |
Point{X:1,Y:2} |
p.X |
GC |
| Rust |
struct Point{x:f64,y:f64} |
Point{x:1.0,y:2.0} |
p.x |
Drop / 所有权 |
| Python |
@dataclass class Point: x:float=0;y:float=0 |
Point(1,2) |
p.x |
GC |
| TypeScript |
interface Point{x:number;y:number} |
{x:1,y:2} |
p.x |
GC |
| kvlang(本提案) |
struct Point{x:float64=0.0;y:float64=0.0} → /lib/Point(kind=struct) |
Point{x=1.0}(余取默认) |
p·x |
deltree(p· 驱动) |
字段默认值:C(无,C++ 有)、Go(无,取零值)、Rust(无,靠 Default/struct-update)、Python dataclass 有 x:float=0.0、TS class 字段有。kvlang name:kindexpr=default 对齐 Python dataclass 的字段默认语义,且默认值直接以 XValue 落进定义子树。
成员访问符:C/Go/Rust/Py/TS 一律 .;kvlang 用 ·(. 已留给小数,见 const.h MEMBER_SEP 注释)。struct 与 stringkeymap 在 kvlang 里统一到 ·,这正是本提案的落点。
参考 stringkeymap(核心对比)
| 维度 |
stringkeymap |
struct(本提案) |
| 键集合 |
开放、运行期增长 |
闭合、定义期固定 |
| 值类型 |
同构(单一 value kindexpr) |
异构(每字段独立 kindexpr) |
| kind |
stringkeymap |
struct(定义节点)/ /lib/Name(实例引用完整路径) |
| 成员访问 |
m·key(key 是运行期值) |
s·field(field 是编译期名) |
| 检查 |
layout 查 key/value 类型 |
runtime 查字段存在性+类型(layout 不查 /lib) |
| 物理落盘 |
p + p· + p·k |
完全相同 |
一句话:struct 就是"字段名闭合、字段类型异构、由 /lib 具名类型驱动"的 stringkeymap;物理层零新增,实例 kindexpr 只是指向类型节点的路径 /lib/Name,schema 校验留给 runtime。
kindexpr 文法扩展
现 atom = shape | mapexpr(layout/src/kindexpr.rs)。新增:
atom = shape | mapexpr | structref
structref = "/" path # /lib/Point:指向 lib 下类型定义节点的完整路径
消歧(三者互斥):
structref 恒以 / 起头(路径)—— 现文法中 kindexpr 无 /,正好腾出。
mapexpr 恒含 · 且 key 段以 [ 起头(valid_key 已强制)。
shape 的 base 是 known_kind 或 any。
layout 侧 valid_kindexpr 只做语法承认(/ + 合法路径段),不解析 /lib 是否存在该类型 —— 存在性/字段一致性由 runtime 判。
待定问题(供讨论)
- 引用路径形态:绝对
/lib/Point(本提案取此);含包时 /lib/geom/Point 的 pkg 段规则;是否允许省略包段的相对引用。
- 嵌套 / 递归 struct:链表节点
next: /lib/Node 需指针语义——关联 ptr-full-kindexpr-single-hop(指针存完整 kindexpr+key,Set 期单跳检查)。struct 字段为 struct 时是内联子树还是指针?
- 值语义 vs 引用:
q = p 深拷贝子树;大 struct 是否退化为 extindex/指针以省拷贝。
- 方法:是否允许
Point·dist 绑定 rwfunc,与 stringkeymap/lib 命名空间统一。
- 匿名 / 内联 struct:无
/lib 注册的一次性结构是否允许(退化为当前 object)。
- 与
#151(stringkeymap→scatterarray 改名)协同:改名后 struct 与 scatterarray 的对称叙述。
关联
kvlang-container-memindex-split(容器方案2,· 唯一 memindex 标记)
extension-rwir-not-hardcoded(layout 不硬编码 /lib,判定留 runtime)
#187 kindexpr · map key·value
#67(否决递归 JSON 文法)、no-json-kind(struct 是异构具名记录这一档的答案)
#151 scatterarray 改名
#110 静态成员链编译期拼完整路径
tutorial/12-struct/*(现用无类型 dict 手搓,本提案的目标替代物)
背景与动机
kvlang 目前没有具名结构体。要表达"一条有固定字段、字段类型各异的记录",只能用无类型
{}(object)+·成员访问手搓(见tutorial/12-struct/*:链表/树/图全是l·head·val = 10这种裸 dict)。缺点:p: Point、无法在 rwir 签名里约束参数是某结构。#67(否决递归 JSON 文法)、no-json-kind(JSON 全类型靠 object/stringkeymap + 动态元素覆盖)留下的缺口对应——异构具名记录这一档一直空缺。关键观察:struct 与 stringkeymap 在物理存储上完全同构,差别只在 schema。因此 struct 不需要新的存储机制,只需让实例 kindexpr 指向
/lib下的类型定义节点,并复用 stringkeymap 的·成员访问。符号职责:
/指向类型,·访问成员/= tree index:类型定义就是 KV 树里的一个节点,实例的 kindexpr 直接写这个节点的完整路径/{lib}/{structname}(如/lib/Point)。引用一个类型 = 指向它在树里的位置,天然用/。·= member index:只用于访问实例成员p·x(与 stringkeymapm·key同符号)。两者不混用:类型引用永不出现
·,成员访问永不出现/。核心设计:struct = 注册在 /lib 的具名类型(kind=
struct),实例 kindexpr =/{lib}/{structname}沿用容器方案2(
kvlang-container-memindex-split):任何容器落盘恒三层p—— 值 XValue(kind 标记 + dims 落 head,body 空)p·—— memindex(kind=index,body=[4B count][name\n…],成员列表唯一权威)p·k—— 成员值(独立 key)struct 实例完全套用这套,唯一变化是
p的 kind 不是stringkeymap/object,而是类型定义节点的完整路径/{lib}/{structname}。1. 定义(definition)
结构定义注册进 lib 命名空间(与 rwir/rwfunc 同级,
/分隔):字段语法
name:kindexpr或name:kindexpr=default(冒号分隔类型,可选=给默认值)。kindexpr 即签名类型表达式(float64、[]float32、[]char/utf8·int64、乃至另一个 struct 路径/lib/Node)。layout 产物(默认包,pkg 规则与 rwfunc 一致):关键:字段描述符
/lib/Point·x存的就是默认值 XValue —— XValue head 本就携带 kindexpr(kind+dims),body 就是默认值,一举给出"字段类型 + 字段默认值"两件事,无需另存类型字符串。省略=default时落该 kindexpr 的零值。于是
/lib/Point·整棵子树 = 一个默认值原型实例(一个 x=0,y=0 的 Point)。/lib/Point这个 XValue 的 kind 就是struct(不是[]char/utf8·[]char/utf8);struct与rwir/rwfunc/scope/stringkeymap并列,进known_kind。2. 实例 kindexpr
实例的 kind(XValue head 里的 kindexpr)= 类型定义节点的完整路径
/lib/Point(/分隔,无·)。/lib/Point就是定义路径,直接据它读·memindex 与各字段 kindexpr,无需任何变换。·(p·x),与类型引用的/井水不犯河水。3. 创建(creation)
具名字面量(对齐 Go/Rust):
创建语义 = 克隆原型 + 覆盖给定字段:runtime 拷贝
/lib/Point·子树(默认值原型)到实例根,再用字面量给出的字段覆盖。实例值标记 kind 记为/lib/Point:字段存在性、字段类型是否匹配定义、省略字段取默认 —— 由 runtime 完成,layout 不读 /lib(见"检查职责")。
4. 读写整体(whole read/write)
q = p—— 深拷贝子树(p、p·、p·*)到q,值语义(对齐 C/Go/Rust 的 struct 值拷贝)。p传入,读参读整棵子树;作写参:写整棵子树。kv·get/set/deltree作用于p走既有容器子树语义,struct 不新增原语。5. 读写成员(member read/write)—— 与 stringkeymap 共享
·p·x→ 单跳 kv get keyp·x。静态成员链在 layout 编译期拼完整路径(#110),a·b·c→ keya·b·c。p·x = 3.0→kv·set(p·x, 3.0)。m·"key"字面同形(同一个·);差别仅在字段是编译期名 vs 运行期值。6. 回收(reclaim/free)
kv·deltree(p):memindexp·是权威成员表,遍历它删除每个p·field;字段自身是容器/struct/extindex 则递归;extindex 字段(落扩展存储的大值)经其扩展句柄释放。与 object/stringkeymap 完全同一套 GC,无新机制。检查职责:layout 不检查 /lib
layout 不读、不校验 kvspace 的
/lib(与extension-rwir-not-hardcoded一致:判定全留给 runtime)。因此:struct Point{…}→ lower 为/lib/Point注册;解析Point{…}字面量、p·x成员访问 → lower 为路径与容器落盘。/lib/Point,读其·memindex 与各字段 kindexpr 做判定并报错。/lib/Point不存在则 runtime 报错)。参考五大语言
struct Point{double x,y;}struct Point p={1,2};p.xfreetype Point struct{X,Y float64}Point{X:1,Y:2}p.Xstruct Point{x:f64,y:f64}Point{x:1.0,y:2.0}p.x@dataclass class Point: x:float=0;y:float=0Point(1,2)p.xinterface Point{x:number;y:number}{x:1,y:2}p.xstruct Point{x:float64=0.0;y:float64=0.0}→/lib/Point(kind=struct)Point{x=1.0}(余取默认)p·xdeltree(p·驱动)字段默认值:C(无,C++ 有)、Go(无,取零值)、Rust(无,靠
Default/struct-update)、Python dataclass 有x:float=0.0、TS class 字段有。kvlangname:kindexpr=default对齐 Python dataclass 的字段默认语义,且默认值直接以 XValue 落进定义子树。成员访问符:C/Go/Rust/Py/TS 一律
.;kvlang 用·(.已留给小数,见const.hMEMBER_SEP 注释)。struct 与 stringkeymap 在 kvlang 里统一到·,这正是本提案的落点。参考 stringkeymap(核心对比)
stringkeymapstruct(定义节点)//lib/Name(实例引用完整路径)m·key(key 是运行期值)s·field(field 是编译期名)p+p·+p·k一句话:struct 就是"字段名闭合、字段类型异构、由
/lib具名类型驱动"的 stringkeymap;物理层零新增,实例 kindexpr 只是指向类型节点的路径/lib/Name,schema 校验留给 runtime。kindexpr 文法扩展
现 atom =
shape | mapexpr(layout/src/kindexpr.rs)。新增:消歧(三者互斥):
structref恒以/起头(路径)—— 现文法中 kindexpr 无/,正好腾出。mapexpr恒含·且 key 段以[起头(valid_key已强制)。shape的 base 是known_kind或any。layout 侧
valid_kindexpr只做语法承认(/+ 合法路径段),不解析 /lib 是否存在该类型 —— 存在性/字段一致性由 runtime 判。待定问题(供讨论)
/lib/Point(本提案取此);含包时/lib/geom/Point的 pkg 段规则;是否允许省略包段的相对引用。next: /lib/Node需指针语义——关联ptr-full-kindexpr-single-hop(指针存完整 kindexpr+key,Set 期单跳检查)。struct 字段为 struct 时是内联子树还是指针?q = p深拷贝子树;大 struct 是否退化为 extindex/指针以省拷贝。Point·dist绑定 rwfunc,与 stringkeymap/lib 命名空间统一。/lib注册的一次性结构是否允许(退化为当前object)。#151(stringkeymap→scatterarray 改名)协同:改名后 struct 与 scatterarray 的对称叙述。关联
kvlang-container-memindex-split(容器方案2,·唯一 memindex 标记)extension-rwir-not-hardcoded(layout 不硬编码 /lib,判定留 runtime)#187kindexpr·map key·value#67(否决递归 JSON 文法)、no-json-kind(struct 是异构具名记录这一档的答案)#151scatterarray 改名#110静态成员链编译期拼完整路径tutorial/12-struct/*(现用无类型 dict 手搓,本提案的目标替代物)