遇到的问题
qjson_parse 在遇到非法 JSON 时,通过旁路出参 err_out(int*)返回一个错误码。错误码本身已经足够细(TRAILING_CONTENT / INVALID_NUMBER / INVALID_STRING / INVALID_UTF8 / NESTING_TOO_DEEP …),但没有任何位置信息:调用方知道"JSON 坏了、为什么坏",却不知道坏在哪个字节,排查困难。
位置信息其实在 Phase 1 的每个失败点都已经存在,只是被签名 Result<(), qjson_err> 压成了一个纯错误码丢弃掉了:
scan() 内部跑 validate_brackets,返回 Err(offset)(括号/引号失衡的字节位),但在 src/doc.rs:31 被 .map_err(|_| QJSON_PARSE_ERROR) 抹掉。
validate_trailing 知道 root_end(垃圾起点),只返回错误码。
validate_eager_values 在每个失败点都攥着 pos / scalar-gap 边界,只返回错误码。
validate_depth(LAZY)知道越界的 idx,只返回错误码。
解决思路
- 加宽 FFI 旁路通道。失败时
qjson_parse 返回 NULL,没有 doc 可挂数据,所以位置必须随错误码一起从 err_out 出来。把 err_out 从 int* 升级成一个错误结构体 qjson_error { int code; size_t offset; }*,让 code 和 offset 物理绑定、不可错位。选结构体而非"多加一个出参"是为了可扩展:后续要加路径上下文(见 Out of scope)时,往同一个 struct 加字段即可,签名零改动。
- 让 4 个 pass 把已有的位置带出来。把相关 validate 函数的错误类型从
qjson_err 升级成携带 usize 偏移,一路冒泡到 parse_with_options → FFI。
- 统一 offset 语义契约(见下),保证 SIMD 与 scalar 两条 scan 路径给出一致的偏移。
- lua wrapper 把 offset 拼进错误消息:
"JSON parse error at byte 1234"。
Spec
Scope
EAGER 模式下,parse 失败时除错误码外,再返回首个被拒字节的偏移。覆盖 Phase 1 的全部四个失败点:scan / validate_trailing / validate_eager_values / validate_depth。
API
typedef struct {
int code; // qjson_err
size_t offset; // 首个被拒字节的偏移;无位置语义时为 SIZE_MAX
} qjson_error;
// err_out 由 int* 升级为 qjson_error*(破坏现有 ABI)
qjson_doc *qjson_parse(const uint8_t *buf, size_t len, qjson_error *err_out);
qjson_doc *qjson_parse_ex(const uint8_t *buf, size_t len,
const qjson_options *opts, qjson_error *err_out);
⚠️ 这是对 qjson_parse / qjson_parse_ex 的一次性 ABI 破坏。需同步更新 include/qjson.h 与 lua/qjson.lua(cdef + err_box)。本库尚未做稳定承诺、唯一外部消费者是自带的 lua wrapper,代价可控,早破比晚破便宜。
offset 语义契约
offset = 首个被拒字节的偏移。
- value 级错误(数字 / 字符串 / UTF-8 非法)给出坏 token 的起点(scalar-gap 起点),不是坏 token 内部的具体坏字节——后者要继续往
validate_number / validate_string_span 下钻,本 issue 不做。
- 与位置无关的错误码(空输入、
QJSON_INVALID_ARG、QJSON_OOM)= SIZE_MAX。
scan 在未闭合开括号/未闭合字符串这类"扫到末尾才发现"的情况,offset = buf.len()。
实现要点
src/scan/mod.rs:validate_brackets 已返回 Err(usize);让 scan() 的 Err 把这个 offset 透传出来。
src/doc.rs:31:去掉 .map_err(|_| QJSON_PARSE_ERROR),改为保留 offset。
src/validate/mod.rs:validate_trailing / validate_eager_values / validate_depth 错误类型升级为携带 usize。validate_eager_values 内约 15 处 return Err(...) 需逐一带上对应 pos / gap 起点。
src/error.rs / include/qjson.h / lua/qjson.lua 三处枚举与新 qjson_error struct 保持同步(CLAUDE.md 要求)。
Acceptance Criteria
Out of scope(各自单开新 issue)
- 路径上下文
[42].foo:需给 eager validator 增加 breadcrumb 栈以保留 key/index 面包屑;qjson_error 预留了扩展空间但本 issue 不实现。
- LAZY / Phase-2 decode 错误的 offset:
qjson_get_* 在字段访问期返回的 DECODE_FAILED 等,触发时机与 Phase 1 不同,属另一条战线。
遇到的问题
qjson_parse在遇到非法 JSON 时,通过旁路出参err_out(int*)返回一个错误码。错误码本身已经足够细(TRAILING_CONTENT/INVALID_NUMBER/INVALID_STRING/INVALID_UTF8/NESTING_TOO_DEEP…),但没有任何位置信息:调用方知道"JSON 坏了、为什么坏",却不知道坏在哪个字节,排查困难。位置信息其实在 Phase 1 的每个失败点都已经存在,只是被签名
Result<(), qjson_err>压成了一个纯错误码丢弃掉了:scan()内部跑validate_brackets,返回Err(offset)(括号/引号失衡的字节位),但在src/doc.rs:31被.map_err(|_| QJSON_PARSE_ERROR)抹掉。validate_trailing知道root_end(垃圾起点),只返回错误码。validate_eager_values在每个失败点都攥着pos/ scalar-gap 边界,只返回错误码。validate_depth(LAZY)知道越界的idx,只返回错误码。解决思路
qjson_parse返回 NULL,没有 doc 可挂数据,所以位置必须随错误码一起从err_out出来。把err_out从int*升级成一个错误结构体qjson_error { int code; size_t offset; }*,让 code 和 offset 物理绑定、不可错位。选结构体而非"多加一个出参"是为了可扩展:后续要加路径上下文(见 Out of scope)时,往同一个 struct 加字段即可,签名零改动。qjson_err升级成携带usize偏移,一路冒泡到parse_with_options→ FFI。"JSON parse error at byte 1234"。Spec
Scope
EAGER 模式下,parse 失败时除错误码外,再返回首个被拒字节的偏移。覆盖 Phase 1 的全部四个失败点:
scan/validate_trailing/validate_eager_values/validate_depth。API
offset 语义契约
offset= 首个被拒字节的偏移。validate_number/validate_string_span下钻,本 issue 不做。QJSON_INVALID_ARG、QJSON_OOM)=SIZE_MAX。scan在未闭合开括号/未闭合字符串这类"扫到末尾才发现"的情况,offset=buf.len()。实现要点
src/scan/mod.rs:validate_brackets已返回Err(usize);让scan()的Err把这个 offset 透传出来。src/doc.rs:31:去掉.map_err(|_| QJSON_PARSE_ERROR),改为保留 offset。src/validate/mod.rs:validate_trailing/validate_eager_values/validate_depth错误类型升级为携带usize。validate_eager_values内约 15 处return Err(...)需逐一带上对应pos/ gap 起点。src/error.rs/include/qjson.h/lua/qjson.lua三处枚举与新qjson_errorstruct 保持同步(CLAUDE.md 要求)。Acceptance Criteria
qjson_errorstruct 落地,qjson_parse/qjson_parse_ex改用之;header 与 lua cdef 同步。offset。at byte N。offset正确。tests/scanner_crosscheck.rs的 proptest:比对 SIMD vs scalar 两条 scan 路径给出一致的 offset(防 backend drift)。Out of scope(各自单开新 issue)
[42].foo:需给 eager validator 增加 breadcrumb 栈以保留 key/index 面包屑;qjson_error预留了扩展空间但本 issue 不实现。qjson_get_*在字段访问期返回的DECODE_FAILED等,触发时机与 Phase 1 不同,属另一条战线。