Languages: English · Русский · 简体中文
本文件记录 ktav crate 的全部重要变更。格式参照
Keep a Changelog;crate
遵循 Semantic Versioning,并采用 Cargo 惯例:
在 1.0 之前,MINOR 递进视为破坏性变更。
格式规范自身的历史,请见
ktav-lang/spec 仓库。
实现同日发布的
Ktav 0.7.0。
spec-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 的u64、i128或u128返回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 键的 定界符。
\uXXXXescape 在值与键中均可解码(§ 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.000001对1e-6)。因此用to_value构建的文档在render/emit_canonical+parse之后不再与自身相等。数值位与 canonical 输出一直是正确的,分歧 仅在于存放的表示。- 类型化的 map 键在两个读 API 与两个写 API 上行为一致。
from_str与de::from_value在数字、bool、unit enum 与 newtype 键上互相分歧;to_string与ser::to_value对同一个值给出不同的键名(1对1.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 复合值边界只计算一次,并通过
InlineBoundsmemo 复用;三个 inline 扫描器合为一个共享状态机;ScopeFrame压缩进单个字节。 - thin 路径直接把 inline 复合值扫描为事件,不再绕行构建 owned
Value, 把 inline 数组以扁平事件块暂存,并通过共享的 path 节点索引一次性注册 子路径。 - 无需去转义时解码器采用借用。
- § 5.6 前缀扫描改为在迭代器上惰性进行,canonical 多行 body 不再拆分后 再拼接。
- map 键名直接写入可 inline 的
Scalar,包括经由collect_str(Display与fmt::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 检出中运行,那里不 排除任何文件。
- 严格解析现在接受 writer 为 Float 输出的规范形式。
1e-3、1.5e-3、-1e-3与1e7等科学形式按 § 5.9.8 policy 比较, 但存储的值仍保持与普通parse()相同的 Ryu 形式。
- crate 版本为
0.6.4,conformance 规范固定为0.6.4。 ktav-lang/specsubmodule 固定到对应的 0.6.4 规范 commit, 包含 float 表示边界 fixture。
- 键现在接受 § 3.7 全部十种转义序列,而非三种。 § 3.7 / § 4 规定
\\、\,、\}、\]、\{、\[、\n、\r、\.、\:在键中均 合法;但实际只有\\、\.和\:可用。a\,b: 1及其余六种形式 都会以InvalidKey被拒绝。根因在于顺序:键先被转义解码,随后再次 校验,而校验会一律拒绝解码后字节中的,{}[]LFCR, 并不区分该字节是经由合法转义而来,还是原样写入的。现在校验改在 原始(解码前)的段上进行:原样的结构字节依旧被拒绝,而其转义形式 得以通过(#7)。 - 事件解析器与树解析器不再对键产生分歧。
src/thin/event_parser.rs自带一份键校验副本,除了同样的顺序缺陷, 还有它自己的偏差:其禁止集合遗漏了LF/CR,因此parse()与from_str::<T>()早已接受不同的键集合。该副本已删除;两个前端现在 共用同一个校验器。 - 写入端输出完整的转义集合。
push_escaped_key_segment只转义\\、.和:,因此通过 API 构造(而非解析得到)的Value,若键 中含有,{}[]LF或CR,序列化出的文档将无法被解析 回来 —— 三个写入面因共用该辅助函数而全部受影响(emit_canonical、 serdeto_string、render/to_string_force_strings)。(与)刻意保持原状:§ 3.7 未为其定义转义,因此含有这两个字符的键仍不可 表示(对其显式拒绝属于 #5, 在0.7分支处理)。
本次变更不具破坏性:此前能解析的输入仍解析为相同的 Value;写入端的
输出仅在那些此前根本无法解析回来的键上发生改变。
- 固定的
ktav-lang/spec子模块现已包含:此前未覆盖的七种转义的 一致性测试夹具、四个用于钉住边界的负向夹具(原样的{/}仍为InvalidKey;\(/\)仍为BadEscapeSequence),以及关于空复合 根包裹的 § 5.9.3 措辞修正 (spec#6、 spec#4)。测试集从 102 valid / 31 invalid 增至 110 valid / 35 invalid。
parse_strict()—— 可选的严格解析模式,拒绝有损标量:即词法形式 与其将被推断成的数字的规范形式不一致的值(1.10→1.1、01234→1234、+7、0x1A、0o755、1_000、5e3)。默认的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)。
- 文档:将所有 README 示例改写为 spec 0.6 语法(裸数字替代已移除的
:i/:f标记;##注释替代#)。
实现 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。
实现 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)。
- 行终止符为
LF、CR或CR LF;CR永远不作为内容字节。 ErrorKind::InlineNonEmptyCompound与InvalidTypedScalar标记为 废弃(#[doc(hidden)]);解析器不再产生它们。
- 内联复合
{k: v, …}/[i, …](规范 § 5.8),支持尾随逗号、 值中部的花括号字面量(§ 5.8.5),嵌套深度上限 128。 - 内联标量中的八种转义序列:
\\、\,、\}、\]、\{、\[、\n、\r(规范 § 3.7)。 - 数字字面量文法 ——
0x、0o、0b、十进制、下划线分隔符; i64 溢出回退为 String(规范 § 3.6)。 emit_canonical()—— 规范 § 5.9 规定的 writer 输出,跨实现 字节确定。src/parser/inline.rs—— 内联复合解析器。src/render/canonical.rs—— 规范化 writer。- 新增错误变体:
UnterminatedInlineCompound、MalformedInlineCompound、BadEscapeSequence、OrphanLineAfterTopLevelInline。 - 三重 conformance 测试台(
tests/spec_conformance.rs):取自spec/versions/0.5/tests/的 93 个有效 + 31 个无效 fixture。 - 解析器快速路径:纯十进制整数、无下划线浮点、ryu 复用、首字节快速 拒绝、仅按 LF 切行、预分配的 Bump arena。
- 许可证:
MIT→MIT OR Apache-2.0。 - 规范 submodule 固定到
v0.5.0(4d0a8aa)。 - 关闭 doctest(
[lib] doctest = false);示例以text块形式保留在 文档注释中。
向后兼容的功能发布,跟进规范 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 —— 对任意给定输入,parse、parse_events 与 from_str 对根
的种类判断一致。
次要发布,含一项破坏性的解析器严格化、一处诊断范围修复,以及类型化 反序列化热路径上的微优化。
-
ErrorKind::DuplicateKey与ErrorKind::KeyPathConflict现在携带 出错键自身的 span,而不是本应赋给它的那个复合结构的收尾}/]。此前,当冲突在attach_child_value处被发现时 (例如value: { ... }与更早的value: ...重复),保存的 span 指向收尾括号——那是解析器在发现冲突的那一刻手头持有的位置。现在解析器在打开复合结构时把键自身的 span(
pending_key_span) 存入父帧,并在复合结构收尾、值被挂接时复用它。依据Span绘制 诊断下划线的编辑器 / IDE 现在会指向键。这是对 span 取值的修复,不是 API 变更 ——
ErrorKind的形态未变。
key: (value)与key: ((value))现在以ErrorKind::InlineNonEmptyCompound { body: "paren-string" }报错。 这些形态过去被当作普通字符串标量(value)接受,但它们与多行开启符 在视觉上无法区分,会让读者困惑。带 raw 标记的形式key:: (value)仍然有效,并且是编码此类字面量的规范写法。ktav-lsp 的格式化器会在 保存时自动改写旧形式。
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搭配使用时零开销)。
次要发布,带两项 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 // )序列化双路径(
Value→render::render(&value)与T: Serialize→ser::to_string(&t))同步更新,行为一致。 -
类型标记
:f接受整数字面量。 尾数中的小数点现在是可选::f 42合法(解析为42.0),沿用 JSON / TOML / YAML 的惯例 (整数字面量隐式提升为 float)。:f 1.(无小数部分)与:f .5(无整数部分)仍然非法。依赖:f 42报InvalidTypedScalar的 代码需要更新。
spec/versions/0.1/tests中的 fixturetyped_float_without_decimal从invalid/移到valid/typed_float_integer_body,以反映新语义。 spec submodule 已同步。
主要发布:带字节偏移 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::new、Span::EMPTY、slice(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::slice与Span::line_col的边界情形(多字节 UTF-8、按字符边界取整);tests/error_accessors.rs—— 逐一验证每个Error变体的line()/span()是否如文档所述返回Some/None;tests/non_exhaustive.rs——Error与ErrorKind的 wildcard 分支 可达性证明;tests/thin_public.rs—— 事件序列、嵌套复合、marker 元素、错误传播、 借用契约。 -
benches/下的合成 Criterion 基准,覆盖 small_1k / medium_50k / large_500k 负载在成功路径和错误路径上的解析性能。baseline 数字 位于bench-baseline.md。
- 对
Error、ErrorKind、ConflictKind、CompoundKind追溯应用#[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 一并交付。
按
Cargo SemVer 参考,
为先前未标注的枚举(Error、ConflictKind、CompoundKind)添加
#[non_exhaustive] 是破坏性变更,通常需要主版本号 bump(0.2.0)。
本次发布有意作为 0.1.5 推出:
- Pre-1.0 的 Cargo 惯例允许在任何 bump(包括 patch)上做破坏性 变更。
ktav::Error所有已知的下游消费者(ktav-lang/下的六个语言 绑定)均仅调用Err(e) => e.to_string()。生态系统中不存在会被 该变更悄悄破坏的穷尽match err { Error::Io(_) => …, Error::Syntax(_) => …, Error::Message(_) => … }模式。- 七个标准类别的 Display 字符串与 0.1.4 字节相同,因此任何在 tree 之外做字符串匹配的假想消费者也无需修改即可继续工作。
如果你的代码确实保留了对 ktav::Error 的穷尽 match 而本次发布
将其破坏 —— 请添加 _ => … 分支。该分支从此永久必须存在,且不会
随未来变体的添加而再次需要变更。
Frame::Object初始容量 4 → 8(src/parser/frame.rs)。 解析器的 per-compoundIndexMap现在预分配 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)不受影响。
与已 yank 的 0.1.2 内容完全相同 —— 通过新的自动化 Release 工作流
(CI verify → cargo publish)重新发布,从而后续发布不再依赖维护
者本地机器上的手动 cargo publish。0.1.2 被 yank 仅用于在一个全新
版本上端到端验证流水线(crates.io 不可变,无法重新发布 0.1.2 本身)。
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 完全一致 —— 仅空白字符不同。
- 类型化反序列化快路径 ——
from_str与from_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 用例仍然通过。
首次发布。实现 Ktav spec 0.1.0。
- 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_str、from_file、to_string、to_file接受任何T: Serialize/DeserializeOwned,包括#[derive]生成的类型、嵌套结构体、Vec、Option、HashMap以及常见的 externally-tagged 枚举形式。Rust 整数类型 (u8..u128、i8..i128、usize、isize)以:i序列化; 浮点(f32、f64)以:f;NaN与±Infinity被序列化器 拒绝(Ktav 0.1.0 不表示)。 - Raw 标记
::—— 强制将值视为字面量 String,既可用于键值对 位置(key:: value),也可作为数组元素的前缀(:: value)。 - 类型化标记
:i与:f—— 在键值对位置显式声明 Integer / Float(port:i 8080、ratio:f 0.5),也可作为数组元素前缀 (:i 42、:f 3.14)。在Value层以字符串存储,以保留任意 精度。 - 多行字符串 ——
( ... )(剥除公共缩进)与(( ... ))(逐字节保留)。通过逐字节形式实现字节级 round-trip。 - 公共
Value枚举 ——Null、Bool、Integer、Float、String、Array、Object(底层为IndexMap,使用rustc_hash::FxBuildHasher)。访问器Value::as_integer/as_float;ThinValue上有对应方法。 - 错误报告 —— 每个语法错误都携带行号;反序列化错误携带
点分路径(
upstreams.[0].port)。类型化标量违规在消息前缀 中以InvalidTypedScalar标示。 - Spec conformance 测试 ——
tests/spec_conformance.rs从ktav-lang/spec仓库读取语言无关测试套件(通过 envKTAV_SPEC_DIR或回退../spec解析路径)。三项检查: Value 匹配 JSON oracle、invalid fixture 被拒绝、通过渲染器的 Value 级 round-trip 无损。
parse → struct: 275 µs(~80 MB/s)render struct → text: 46 µs(~475 MB/s)round-trip: 377 µs
serde(含derive)indexmap(启用serde特性)rustc-hash(FxHash —— 快且确定性;不抗碰撞,而配置解析器 并不需要抗碰撞)
rustc 1.70 或更新版本。