Skip to content

Latest commit

 

History

History
657 lines (532 loc) · 33.8 KB

File metadata and controls

657 lines (532 loc) · 33.8 KB

变更日志 —— ktav crate

Languages: English · Русский · 简体中文

本文件记录 ktav crate 的全部重要变更。格式参照 Keep a Changelog;crate 遵循 Semantic Versioning,并采用 Cargo 惯例: 在 1.0 之前,MINOR 递进视为破坏性变更。

格式规范自身的历史,请见 ktav-lang/spec 仓库。

[0.7.0] —— 2026-09-10

实现同日发布的 Ktav 0.7.0spec-version 元数据升至 0.7.0,固定的 spec submodule 也移至 0.7.0 发布提交,因此本 crate 所对照的 conformance 语料库正是已发布的那一份。

破坏性变更

  • ( … ) 去缩进多行字符串现在会剥除每个内容行的行尾空白(§ 5.6), 与它早已对行首空白所做的一致。此前行尾空白逐字节保留,这意味着编辑器 的「保存时去除行尾空白」可能悄然改变字符串内容。(( … )) 不受影响, 两端仍然完全逐字——当行尾空白有意义时请使用该形式。
  • 被识别的 escape 序列强制归类为 String(§ 3.7 / § 5.2)。写作 \u0031 的 body 解码为 1,但仍是 String:escape 是意图的证据,因此解码 后的文本不再被重新归类为数字或关键字。
  • 空白是各处统一的、冻结的 § 3.3 25 码点集合,不再委派给宿主语言的 Unicode 判定。键段修剪从仅 ASCII 扩展到同一集合(§ 4),因此仅在被修剪 边缘相差一个非 ASCII 空白字符的两个键,现在会合并为同一个键。
  • i64 域之外的整数是 String,通过 ser::to_value 也是如此。 解析器 的 Integer 域一直是 i64(§ 5、§ 5.2 规则 13);而 serde 桥接层此前会 产生更宽的 Value::Integer,它在下一次 roundtrip 就会改变变体与 canonical 字节。现在 to_value 对超出 i64 的 u64i128u128 返回 Value::String——与解析同一字面量所得的 Value 完全一致。域内 数值不变。

新增

  • 前导 BOM 处理(§ 3.1):若 U+FEFF 是文档的首个码点,则恰好跳过一个; canonical writer 永不输出 BOM。位于其他位置的 U+FEFF 是普通内容。
  • 带引号的键(§ 5.3.3):quote-aware 的键与复合值扫描器、带 quoted 段验证与解码的 0.7 escape 表,以及所有 writer 中 § 5.9.10 的 canonical 键形式选择与重新转义规则。thin event 路径会剥除 quoted 键的 定界符。
  • \uXXXX escape 在值与键中均可解码(§ 3.7.1),对孤立代理与不匹配 代理给出 BadEscapeSequence 诊断。
  • Array 作为根的文档:serde root 序列化器与两个解析器均支持,并在每个 writer 中带有 § 5.9.6 / § 5.9.12 的 Array 根首元素保护。
  • writer 侧对不可表示值的拒绝(§ 5.9.0),三个 writer 面上均带 reason code。
  • InvalidUtf8 成为携带字节偏移的独立错误类别,位于字节边界面 (§ 6.15);UnterminatedQuotedKey(§ 6.16)用于未闭合的带引号键。
  • 两个反序列化器都支持 i128 / u128 它们此前能够正确序列化却读不 回来:除非覆盖,serde 的默认方法会直接拒绝这两个类型。现在 owned 与 thin 两条路径都精确解析数字,不经过 64 位或 f64 中转。
  • conformance runner 扩展到 0.7 语料库,包含其 unrepresentable/parseable-unrepresentable/ 类别。

修复

  • ser::to_value 现在存放解析器会存放的 payload。 Float payload 曾带 有文本字面量式的 .0 尾数(解析器存 1e100 处它存 1.0e100),而 f32 走的是 ryu 的 f32 阈值而非解析器所用的 f64 阈值(0.0000011e-6)。因此用 to_value 构建的文档在 render/emit_canonical + parse 之后不再与自身相等。数值位与 canonical 输出一直是正确的,分歧 仅在于存放的表示。
  • 类型化的 map 键在两个读 API 与两个写 API 上行为一致。 from_strde::from_value 在数字、bool、unit enum 与 newtype 键上互相分歧; to_stringser::to_value 对同一个值给出不同的键(11.0)且接受不同的键类型。现在两侧共用一套策略。被 Some(...) 包裹的 键此前可被 writer 接受却被两个 reader 拒绝——现在可以完整 roundtrip, 而字面名为 null 的键保持为字符串 "null",不会变成 None
  • inline 复合值扫描 的大量边界情形,由 0.7 语料库与系列评审揭示:引号 跟踪绑定到正确的 scope、值中的引号不再遮蔽结构性闭合符、标量中部的开启 符视作普通字节、裸标量在任何未转义闭合符处终止、逐 scope 的裸闭合符与 scope 恢复、逗号的键上下文取自当前 scope、空白跳过后的 EOF,以及 inline 键位置上的方括号报告为 InvalidKey 而非幻影复合值错误。
  • § 5.6 去缩进以 § 3.3 码点而非字节度量公共前缀,通过一次共享的前缀 扫描,并从每个非空行中扣除。
  • thin event 解析器中的 § 5.3.2 点分键重入,以及复合值子路径注册与裸 复合开启符的 merge frame。
  • i64::MIN 由带前缀的负整数字面量精确表示
  • serde 文本序列化器拒绝空的根 tuple-variant 名称。

性能

本次发布不声称任何计时数据——以下工作基于源码级分析与分配计数器,而非 benchmark。

  • inline 复合值边界只计算一次,并通过 InlineBounds memo 复用;三个 inline 扫描器合为一个共享状态机;ScopeFrame 压缩进单个字节。
  • thin 路径直接把 inline 复合值扫描为事件,不再绕行构建 owned Value, 把 inline 数组以扁平事件块暂存,并通过共享的 path 节点索引一次性注册 子路径。
  • 无需去转义时解码器采用借用。
  • § 5.6 前缀扫描改为在迭代器上惰性进行,canonical 多行 body 不再拆分后 再拼接。
  • map 键名直接写入可 inline 的 Scalar,包括经由 collect_str (Displayfmt::Arguments 键);短键名不再分配临时 String。读取 侧 inline 键以切片交付,而已有的 heap 缓冲区仍以移动而非复制交给目标。
  • 整数的 i64 域检查改为原生范围比较,不再格式化后重新解析十进制文本。

备注

  • MSRV 仍为 1.71。依赖未变。
  • 针对 spec 0.7.0 的十七轮独立实现评审归档于 docs/reviews/;每条发现要么 已在此修复,要么在那里明确记录为待剖析的候选项。
  • 发布的 tarball 不再携带随仓库附带的规范语料、内部评审记录与文档 生成器:文件数从 1,715(压缩后 1.4 MiB)降至 188(403 KiB)。构建库 所需的内容一个都没少——conformance 套件请在 git 检出中运行,那里不 排除任何文件。

[0.6.4] —— 2026-08-23

修复

  • 严格解析现在接受 writer 为 Float 输出的规范形式。 1e-31.5e-3-1e-31e7 等科学形式按 § 5.9.8 policy 比较, 但存储的值仍保持与普通 parse() 相同的 Ryu 形式。

变更

  • crate 版本为 0.6.4,conformance 规范固定为 0.6.4
  • ktav-lang/spec submodule 固定到对应的 0.6.4 规范 commit, 包含 float 表示边界 fixture。

[0.6.3] —— 2026-08-23

修复

  • 键现在接受 § 3.7 全部十种转义序列,而非三种。 § 3.7 / § 4 规定 \\\,\}\]\{\[\n\r\.\: 在键中均 合法;但实际只有 \\\.\: 可用。a\,b: 1 及其余六种形式 都会以 InvalidKey 被拒绝。根因在于顺序:键先被转义解码,随后再次 校验,而校验会一律拒绝解码后字节中的 , { } [ ] LF CR, 并不区分该字节是经由合法转义而来,还是原样写入的。现在校验改在 原始(解码前)的段上进行:原样的结构字节依旧被拒绝,而其转义形式 得以通过(#7)。
  • 事件解析器与树解析器不再对键产生分歧。 src/thin/event_parser.rs 自带一份键校验副本,除了同样的顺序缺陷, 还有它自己的偏差:其禁止集合遗漏了 LF/CR,因此 parse()from_str::<T>() 早已接受不同的键集合。该副本已删除;两个前端现在 共用同一个校验器。
  • 写入端输出完整的转义集合。 push_escaped_key_segment 只转义 \\.:,因此通过 API 构造(而非解析得到)的 Value,若键 中含有 , { } [ ] LFCR,序列化出的文档将无法被解析 回来 —— 三个写入面因共用该辅助函数而全部受影响(emit_canonical、 serde to_stringrender / to_string_force_strings)。() 刻意保持原状:§ 3.7 未为其定义转义,因此含有这两个字符的键仍不可 表示(对其显式拒绝属于 #5, 在 0.7 分支处理)。

本次变更不具破坏性:此前能解析的输入仍解析为相同的 Value;写入端的 输出仅在那些此前根本无法解析回来的键上发生改变。

变更

  • 固定的 ktav-lang/spec 子模块现已包含:此前未覆盖的七种转义的 一致性测试夹具、四个用于钉住边界的负向夹具(原样的 { / } 仍为 InvalidKey\( / \) 仍为 BadEscapeSequence),以及关于空复合 根包裹的 § 5.9.3 措辞修正 (spec#6spec#4)。测试集从 102 valid / 31 invalid 增至 110 valid / 35 invalid。

[0.6.2] —— 2026-08-19

新增

  • parse_strict() —— 可选的严格解析模式,拒绝有损标量:即词法形式 与其将被推断成的数字的规范形式不一致的值(1.101.1012341234+70x1A0o7551_0005e3)。默认的 parse() 会静默地将这类值规范化,因此往返一次就会改写文档且没有任何提示; 严格模式则将其暴露出来,错误信息同时给出两种形式,并提示两种修复 方式(追加 :: 以保留字符串,或直接写规范数字)。被 parse_strict() 接受的文档产生的 Value 树与 parse() 完全一致。
  • ErrorKind::LossyScalar { line, body, canonical, span } —— 仅在严格 模式下出现的新错误变体,已接入 ErrorKind::line() / ErrorKind::span()ErrorKind 标注了 #[non_exhaustive],因此这是 一次增量式变更。

parse()、serde 路径(from_str)与 C ABI 的行为均未改变;serde 事件路径目前尚无严格模式变体。

变更

  • MSRV 从 1.70 提升至 1.71。 这并非我们的主动选择:serde_core 要求 serde_derive = "=1.0.229",而该版本(2026-07-18)声明 rust-version = 1.71。库不会随包发布自己的 Cargo.lock,因此 1.70 的用户本就已经无法构建本 crate —— 现在清单如实反映了真实要求。

感谢 @chappihappymeal 报告问题 并贡献实现(#1#2)。

[0.6.1] — 2026-06-05

  • 文档:将所有 README 示例改写为 spec 0.6 语法(裸数字替代已移除的 :i/:f 标记;## 注释替代 #)。

[0.6.0] —— 2026-06-01

实现 Ktav 规范 0.6.0。新增 键转义:键现在处理 §3.7 转义集,并 新增两条转义 —— \.\: —— 允许在键段内出现字面意义的点 / 冒号。

破坏性变更

  • 键中的字面 \ 现在需要写作 \\。此前解析器把键中的 \ 当作 无转义的内容字节。源文件中含单个 \ 的键需要改为双反斜杠以保 持相同的键字节。值的处理不变。

新增

  • 转义表由 8 条扩展到 10 条:原有 8 条(\\\,\}\]\{\[\n\r)之上新增 \.(键段中的字面点 —— 不分割 路径)与 \:(字面冒号 —— 不作为键/值分隔符)。两条新转义在值 上下文中亦合法(冗余)。
  • 键扫描器支持转义:首个 未转义:(或 ::)为对分隔符; 点分路径仅在 未转义. 处分割。内联 compound ({a\.b: 1})采用同样规则。
  • 渲染器在输出每个键段时回写转义 \.:,保证含字面点/冒 号的键的 parse → render → parse 同一性。
  • 零拷贝事件路径下,不含 \ 的键段继续从源缓冲区借用;含转义的 键段解码到 bump-arena —— &'a str 生命周期保持不变。

错误

  • 键中 \X(X 不在十条转义之列)现在抛出 BadEscapeSequence\.\: 在任何上下文均不再是 BadEscapeSequence

[0.5.0] —— 2026-05-28

实现 Ktav 规范 0.5.0。这是一次破坏性发布:解析器、序列化器与 Value 模型均按新的语言语义重写。

破坏性变更

  • 移除类型标记 :i:f。数字、布尔与 null 由词法形态推断 (规范 §§ 3.6、5.2)。
  • 注释使用 ##(仅限行首)。单个 # 字节属于内容。
  • 裸写的 port: 8080 现在是 Integer(8080),不再是 String("8080")。 若要保留 String,请写 port:: 8080
  • 首个内容行上单独的 { / [ 开启多行的根 Object / Array (规范 § 5.0.1 规则 4–5)。0.1.1 的 JSONL 式语义已移除。
  • Float 值不再携带文本形态;改由 ryu 规范化。
  • 键段的首尾空白会被裁剪(规范 § 4)。
  • 行终止符为 LFCRCR LFCR 永远不作为内容字节。
  • ErrorKind::InlineNonEmptyCompoundInvalidTypedScalar 标记为 废弃(#[doc(hidden)]);解析器不再产生它们。

新增

  • 内联复合 {k: v, …} / [i, …](规范 § 5.8),支持尾随逗号、 值中部的花括号字面量(§ 5.8.5),嵌套深度上限 128。
  • 内联标量中的八种转义序列\\\,\}\]\{\[\n\r(规范 § 3.7)。
  • 数字字面量文法 —— 0x0o0b、十进制、下划线分隔符; i64 溢出回退为 String(规范 § 3.6)。
  • emit_canonical() —— 规范 § 5.9 规定的 writer 输出,跨实现 字节确定。
  • src/parser/inline.rs —— 内联复合解析器。
  • src/render/canonical.rs —— 规范化 writer。
  • 新增错误变体:UnterminatedInlineCompoundMalformedInlineCompoundBadEscapeSequenceOrphanLineAfterTopLevelInline
  • 三重 conformance 测试台(tests/spec_conformance.rs):取自 spec/versions/0.5/tests/ 的 93 个有效 + 31 个无效 fixture。
  • 解析器快速路径:纯十进制整数、无下划线浮点、ryu 复用、首字节快速 拒绝、仅按 LF 切行、预分配的 Bump arena。

变更

  • 许可证:MITMIT OR Apache-2.0
  • 规范 submodule 固定到 v0.5.04d0a8aa)。
  • 关闭 doctest([lib] doctest = false);示例以 text 块形式保留在 文档注释中。

[0.3.1] —— 2026-05-10

向后兼容的功能发布,跟进规范 0.1.1。

新增

  • 顶层 Array 支持(规范 § 5.0.1,于规范 0.1.1 加入)——首个内容行 具备数组元素形态(裸标量、:: 文本:i 42:f 3.14、单独的 { / [,或多行开启符 ( / (()的文档,现在解析为根级 Value::Array。此前根始终是 Value::Object,因此第 1 行的裸标量 会以 MissingSeparator 报错。空文档与仅含注释的文档仍默认为空 Object(保持 0.3.0 行为)。
  • ktav::to_string_force_strings(value) —— 渲染任意 Value,并将 每个标量强制为 String。类型化整数(:i)、类型化浮点(:f)、 布尔与 null 会被展平为其文本形态;复合结构保持不变。输出经解析器 round-trip 后仍是同一组 String 标量。适用于面向不理解类型标记的 下游消费者的「一切皆字符串」转储,或便于 diff 的规范化文本。
  • 新的 Render 出口:render 在顶层同时接受 Object 与 Array (顶层 Array 渲染为每行一个裸元素,不带 [...] 方括号)。

兼容性

严格增量。凡在 0.3.0 下有效的文档,在 0.3.1 下依然有效并产生相同的 Value。只有此前被 0.3.0 以 MissingSeparator 拒绝的输入(首行为 裸标量)现在被接受为 Array。Object 路径上的错误变体及其 span 未变。

解析器、render、thin 事件解析器与 thin 事件反序列化器一致遵循规范 § 5.0.1 —— 对任意给定输入,parseparse_eventsfrom_str 对根 的种类判断一致。

[0.3.0] —— 2026-05-08

次要发布,含一项破坏性的解析器严格化、一处诊断范围修复,以及类型化 反序列化热路径上的微优化。

修复

  • ErrorKind::DuplicateKeyErrorKind::KeyPathConflict 现在携带 出错键自身的 span,而不是本应赋给它的那个复合结构的收尾 } / ]。此前,当冲突在 attach_child_value 处被发现时 (例如 value: { ... } 与更早的 value: ... 重复),保存的 span 指向收尾括号——那是解析器在发现冲突的那一刻手头持有的位置。

    现在解析器在打开复合结构时把键自身的 span(pending_key_span) 存入父帧,并在复合结构收尾、值被挂接时复用它。依据 Span 绘制 诊断下划线的编辑器 / IDE 现在会指向键。

    这是对 span 取值的修复,不是 API 变更 —— ErrorKind 的形态未变。

变更(breaking —— 解析器严格化)

  • key: (value)key: ((value)) 现在以 ErrorKind::InlineNonEmptyCompound { body: "paren-string" } 报错。 这些形态过去被当作普通字符串标量 (value) 接受,但它们与多行开启符 在视觉上无法区分,会让读者困惑。带 raw 标记的形式 key:: (value) 仍然有效,并且是编码此类字面量的规范写法。ktav-lsp 的格式化器会在 保存时自动改写旧形式。

优化(无 API 变更)

  • render::render 通过递归的 estimate_size(value) 预设输出 String 的容量,从而跳过 push_str 链在数 KiB 输出上本会触发的倍增式 重分配。
  • EventCursor::peek / next 使用 unsafe get_unchecked 以消除热 路径上的边界检查;解析器的良构流不变式保证每次调用时 pos < len()。不变式被破坏时回退为 None,因此畸形输入依然安全 —— 这里的 unsafe 路径纯粹是省去一次分支判断的收益。
  • MapAccess::next_key_seed 将冗余的 peek + next 合并为单次 next (两个分支反正都会消费游标)。
  • 事件 BumpVec 的容量提示从 text.len() / 8 + 16 提高到 text.len() / 4 + 64。旧提示低估了 synth fixture 上约每 5 字节 1 个事件的密度,在 500 KiB 文档上会在 bump arena 内触发 8–10 次 realloc-copy。

实验(已回退)

  • 一次流式反序列化器重构(按需解析,不保留整份文档的 Vec<Event>) 已实现并测试 —— 全部 404 个测试通过,但在本机硬件上 parse_to_struct 相对既有游标回退了 15–60 %。原因:游标沿连续切片 行进,只有一条单调分支,预测器命中率 100 %;而流式方案把解析器状态 机的工作与反序列化器的工作交错,打乱了预测器。流式代码已删除;为该 实验引入的 EventSink<'a> trait 作为无害的泛型基础设施保留在 event.rs 中(仅与 BumpVec 搭配使用时零开销)。

[0.2.0] —— 2026-05-07

次要发布,带两项 breaking 输出 / 校验改动:

变更(breaking)

  • 多行字符串默认输出为缩进 stripped 形式 ( ... ),而非 verbatim (( ... ))。当内容自带前导空白(会被解析侧的 dedent 吞掉)或包含 仅为 ) 的行(会提前关闭 stripped)时,fallback 到 verbatim。逐字节 比较 to_string / render 输出与硬编码 ((...)) 的代码需要更新。 Round-trip(parse(to_string(v)) == v)未受影响。

    // 之前 (0.1.5):
    // body: ((
    // line1
    // line2
    // ))
    //
    // 之后 (0.2.0):
    // body: (
    //     line1
    //     line2
    // )
    

    序列化双路径(Valuerender::render(&value)T: Serializeser::to_string(&t))同步更新,行为一致。

  • 类型标记 :f 接受整数字面量。 尾数中的小数点现在是可选: :f 42 合法(解析为 42.0),沿用 JSON / TOML / YAML 的惯例 (整数字面量隐式提升为 float)。:f 1.(无小数部分)与 :f .5 (无整数部分)仍然非法。依赖 :f 42InvalidTypedScalar 的 代码需要更新。

Spec

  • spec/versions/0.1/tests 中的 fixture typed_float_without_decimalinvalid/ 移到 valid/typed_float_integer_body,以反映新语义。 spec submodule 已同步。

[0.1.5] —— 2026-05-01

主要发布:带字节偏移 span 的结构化错误、公开的事件式解析器 API、 对错误枚举追溯应用 #[non_exhaustive] 以保证前向兼容。

新增

  • ErrorKind 枚举(10 个规范定义变体 + Other),每个变体携带 字节偏移 span: Span,直接向下游消费者暴露 (line, column, kind), 无需通过正则解析格式化消息。

    pub enum ErrorKind {
        MissingSeparatorSpace { line, column, marker, span },
        InvalidTypedScalar    { line, marker, body, span },
        DuplicateKey          { line, key, span },
        KeyPathConflict       { line, path, kind: ConflictKind, span },
        EmptyKey              { line, span },
        InvalidKey            { line, key, span },
        UnclosedCompound      { kind: CompoundKind, span },
        UnbalancedBracket     { line, expected: CompoundKind, found: char, span },
        InlineNonEmptyCompound{ line, body, span },
        MissingSeparator      { line, span },
        Other                 { line: Option<u32>, message, span },
    }
    
  • 现有 Error 枚举上的 Error::Structured(ErrorKind) 变体。

  • pub struct Span { start: u32, end: u32 },提供 Span::newSpan::EMPTYslice(input)line_col(input)(line 从 1 起, column 从 0 起按字节计;多字节 UTF-8 已通过西里尔与 🦀 测试固定)。

  • Error::line() -> Option<u32>Error::span() -> Option<Span> — 覆盖每个变体的便捷访问器。

  • pub mod thin —— 公开的事件式解析器 API: ktav::parse_events(input, callback) 对从 input 借用的每个事件 调用所提供的 FnMut(ParseEvent<'_>)ParseEvent#[non_exhaustive] 枚举,有 10 个变体。内部 bumpalo arena 保持私有 —— 公共 API 不泄露 arena 类型。

  • src/lib.rs 的 crate 级可运行 doctest,演示 Error::Structured 匹配配合 Span::slice 以及 parse_events 回调形态。

  • 六个新顶层测试文件: tests/error_format.rs —— Display 字符串的回归网(为 LSP 与绑定所 依赖的 7 个类别做规范化钉定); tests/structured_errors.rs —— 按规范的每个无效 fixture 校验变体 身份以及 (line, span) 字节范围; tests/error_spans.rs —— span 字节范围语义,以及 Span::sliceSpan::line_col 的边界情形(多字节 UTF-8、按字符边界取整); tests/error_accessors.rs —— 逐一验证每个 Error 变体的 line() / span() 是否如文档所述返回 Some / None; tests/non_exhaustive.rs —— ErrorErrorKind 的 wildcard 分支 可达性证明; tests/thin_public.rs —— 事件序列、嵌套复合、marker 元素、错误传播、 借用契约。

  • benches/ 下的合成 Criterion 基准,覆盖 small_1k / medium_50k / large_500k 负载在成功路径和错误路径上的解析性能。baseline 数字 位于 bench-baseline.md

变更

  • ErrorErrorKindConflictKindCompoundKind 追溯应用 #[non_exhaustive]。未来添加变体对下游的 match 不再是破坏性 变更 —— 调用方现在必须包含 _ => 分支。
  • 解析器在任何内部调用点都不再构造 Error::Syntax(format!(...)) (约 37 个站点重构为 Error::Structured)。回归保护测试 (parser_no_longer_emits_legacy_syntax_variant)运行 12 个非法 输入,若有人在 src/ 内重新引入旧变体,CI 会大声失败。
  • parser/parse_str.rs 用维护累积 line_start 计数的手工字节遍历 循环替代 str::lines(),以便在每个错误站点计算字节偏移 span 而 无需重新扫描。thin/event_parser.rs 在零拷贝路径上镜像同一 plumbing。
  • Display for ErrorKind 与解析器先前格式化进 Error::Syntax(...) 的字符串完全字节相同(覆盖七个先前已固定类别)—— 这是让现有 基于字符串的调用方在 STRUCTURED_ERRORS.md 描述的生态系统 级迁移期间保持不变工作的契约。
  • 三个先前归入 Other 的形态升级为命名 ErrorKind 变体: UnbalancedBracket(孤立闭符 / 形状不匹配)、 InlineNonEmptyCompound(x: {foo} —— 规范 § 6.7)、 MissingSeparator(无 : 的行)。升级后,Other 仅保留没有任何 规范非法夹具能触发的解析器内部不变量。

性能

cargo bench --bench parse -- --quick 与 0.1.4 baseline 对比:

0.1.4 baseline 0.1.5 Δ
parse_synth/small_1k 16.1 µs 16.0 µs −0.6 %
parse_synth/medium_50k 896 µs 663 µs −26 %
parse_synth/large_500k 9.49 ms 9.27 ms −2.3 %
parse_synth_error/small_1k 7.5 µs 7.2 µs −4.0 %
parse_synth_error/medium_50k 340 µs 346 µs +1.8 %
parse_synth_error/large_500k 4.47 ms 4.50 ms +0.7 %

结论:成功路径零回归。错误路径略快——新的 Display 实现在调用 .to_string() 时才惰性构造格式化字符串,而此前的 format!(...) 会在 每一处错误点即时分配一个 String。支撑 span 的累积字节计数器在统计上 是免费的。

备注

  • 为了向后兼容,Error::Syntax(String) 保留 —— 公共 API 仍不拒绝 老调用方。移除推迟到 ktav 1.0。
  • 测试数:332 (0.1.4) → 391 (+59),外加 1 个新 doctest。
  • cabi/绑定迁移以通过 FFI 边界消费 ErrorKind 的工作单独记录在 STRUCTURED_ERRORS.md,并作为协调发布 的生态系统 0.2.0 一并交付。

SemVer 说明

Cargo SemVer 参考, 为先前未标注的枚举(ErrorConflictKindCompoundKind)添加 #[non_exhaustive] 是破坏性变更,通常需要主版本号 bump(0.2.0)。 本次发布有意作为 0.1.5 推出:

  1. Pre-1.0 的 Cargo 惯例允许在任何 bump(包括 patch)上做破坏性 变更。
  2. ktav::Error 所有已知的下游消费者(ktav-lang/ 下的六个语言 绑定)均仅调用 Err(e) => e.to_string()。生态系统中不存在会被 该变更悄悄破坏的穷尽 match err { Error::Io(_) => …, Error::Syntax(_) => …, Error::Message(_) => … } 模式。
  3. 七个标准类别的 Display 字符串与 0.1.4 字节相同,因此任何在 tree 之外做字符串匹配的假想消费者也无需修改即可继续工作。

如果你的代码确实保留了对 ktav::Error 的穷尽 match 而本次发布 将其破坏 —— 请添加 _ => … 分支。该分支从此永久必须存在,且不会 随未来变体的添加而再次需要变更。

[0.1.4] —— 2026-04-26

变更

  • Frame::Object 初始容量 4 → 8(src/parser/frame.rs)。 解析器的 per-compound IndexMap 现在预分配 8 个槽位而非 4 个, 消除了典型配置行(5–8 字段)的首次扩容/rehash。这是 untyped 解析路径(ktav::parse → Value)—— 也是所有 C-ABI 绑定 (PHP/JS/Python/Go/Java/C#)通过 cabi 走的路径,因此一旦它们 升级到 0.1.4 就会获得相同的加速。
  • parse_to_value 基准上的影响(3 次运行中位数):small −30%(18.9 µs → 13.3 µs)、large −13%(5.04 ms → 4.4 ms)、 medium 在噪声范围内(~−3%)。

单行改动;完整测试套件(334 个用例,含 spec conformance)不受影响。

[0.1.3] —— 2026-04-26

与已 yank 的 0.1.2 内容完全相同 —— 通过新的自动化 Release 工作流 (CI verify → cargo publish)重新发布,从而后续发布不再依赖维护 者本地机器上的手动 cargo publish。0.1.2 被 yank 仅用于在一个全新 版本上端到端验证流水线(crates.io 不可变,无法重新发布 0.1.2 本身)。

[0.1.2] —— 2026-04-26

0.1.1 内容的重新发布,源码经过 cargo fmt 处理。0.1.1 被 yank,因为 新增文件(benches/vs_json.rs, src/thin/event*.rs, src/thin/fast_num.rs)在发布前未经 rustfmt 处理,导致 CI lint 在 tag push 时失败。功能与 0.1.1 完全一致 —— 仅空白字符不同。

[0.1.1] —— 2026-04-26

变更

  • 类型化反序列化快路径 —— from_strfrom_file 不再构建中间 ThinValue 树。解析器直接将事件序列(Vec<Event>)发射到 bump arena,serde 反序列化器以单一游标线性遍历它 —— 每个文档一次分配 而非每个复合节点一次,且无需通过 Box 间接加载枚举判别式。 在 275 KB 配置上的实测:parse → struct −18.7%(3.60 ms → 2.93 ms)。
  • fast_num 字节循环 atoi —— 类型化反序列化器中的 i8..i64 / u8..u64 路径绕过通用的 <T as FromStr> 路线,改为调用手写 的 parse_i64 / parse_u64 并附带宽度检查。浮点路径仍走 f64::from_str

新增

  • 内部 Event 枚举与 EventCursor 遍历器(thin/event*.rs)。

移除

  • ThinValue 枚举及其 ThinDeserializer(已被事件流取代;两者均为 pub(crate),公开 API 不受影响)。

行为变化

  • dotted-key 前缀的交错使用现在会被拒绝为 conflict。 形如 a.x: 1\nb.y: 2\na.z: 3 的文档(合成对象 a 被打开,被 b. 关闭,然后被 a.z 尝试重新打开)以前会通过 tree-builder 静默 合并为单一 a 对象。事件流标记器在不缓冲整个文档的情况下无法 做到这一点,因此现在会返回清晰的 conflict 错误,提示用户将相同 前缀的行分组在一起。使用分组 dotted-key(规范模式)的文档不受 影响 —— 所有 spec-conformance 用例仍然通过。

[0.1.0] —— 2026-04-22

首次发布。实现 Ktav spec 0.1.0

Added

  • Parser —— 将 Ktav 文本转换为 Value(拥有所有权)或 ThinValue(在输入缓冲区上的零拷贝视图)。基于行的状态机, 支持点分键展开、多行字符串(剥除缩进与逐字节两种)、 JSON 风格关键字 null / true / false,以及类型化标量标记 :i(Integer)与 :f(Float)。
  • Serializer —— 两条路径:
    • ktav::to_string(直接文本输出,主路径)。
    • ktav::ser::to_value / ktav::render(两步路径,便于在中间 检视 Value)。 两者都会在字符串可能被解析器误读时自动输出 ::,并为 Rust 数值类型发出 :i / :f
  • Deserializer —— 通过 ThinValue<'a>ThinDeserializer 走零拷贝路径。对象键与单行标量值直接从输入借用;只有多行 字符串会发生分配。接受带标记与不带标记两种数字形式 —— 不含 标记的旧文档仍能通过 FromStr 透明反序列化。
  • Serde integration —— from_strfrom_fileto_stringto_file 接受任何 T: Serialize / DeserializeOwned,包括 #[derive] 生成的类型、嵌套结构体、VecOptionHashMap 以及常见的 externally-tagged 枚举形式。Rust 整数类型 (u8..u128i8..i128usizeisize)以 :i 序列化; 浮点(f32f64)以 :f;NaN±Infinity 被序列化器 拒绝(Ktav 0.1.0 不表示)。
  • Raw 标记 :: —— 强制将值视为字面量 String,既可用于键值对 位置(key:: value),也可作为数组元素的前缀(:: value)。
  • 类型化标记 :i:f —— 在键值对位置显式声明 Integer / Float(port:i 8080ratio:f 0.5),也可作为数组元素前缀 (:i 42:f 3.14)。在 Value 层以字符串存储,以保留任意 精度。
  • 多行字符串 —— ( ... )(剥除公共缩进)与 (( ... )) (逐字节保留)。通过逐字节形式实现字节级 round-trip。
  • 公共 Value 枚举 —— NullBoolIntegerFloatStringArrayObject(底层为 IndexMap,使用 rustc_hash::FxBuildHasher)。访问器 Value::as_integer / as_float;ThinValue 上有对应方法。
  • 错误报告 —— 每个语法错误都携带行号;反序列化错误携带 点分路径(upstreams.[0].port)。类型化标量违规在消息前缀 中以 InvalidTypedScalar 标示。
  • Spec conformance 测试 —— tests/spec_conformance.rsktav-lang/spec 仓库读取语言无关测试套件(通过 env KTAV_SPEC_DIR 或回退 ../spec 解析路径)。三项检查: Value 匹配 JSON oracle、invalid fixture 被拒绝、通过渲染器的 Value 级 round-trip 无损。

Performance(criterion,22 KB 的 typed 配置,Windows release)

  • parse → struct: 275 µs(~80 MB/s)
  • render struct → text: 46 µs(~475 MB/s)
  • round-trip: 377 µs

Dependencies

  • serde(含 derive)
  • indexmap(启用 serde 特性)
  • rustc-hash(FxHash —— 快且确定性;不抗碰撞,而配置解析器 并不需要抗碰撞)

MSRV

rustc 1.70 或更新版本。