use crate::error::{Error, Result, SyntaxErrorKind};
use crate::metal::{Dispatch, GpuBuffer, MetalContext, MjParams, THREADGROUP_SIZE};
use crate::pool::{Alloc, ScratchPool};
use crate::stage::{Stage, Stage1Buffers, Stage3Buffers, sort_passes};
use crate::tape::{make_final_root, make_root};
use super::numbers::{NO_NUMBER_ERROR, pack_number_error, patch_number_fixups};
use super::strings::patch_long_strings;
use super::stage2::{Stage2Accepted, Stage2Run};
use super::stage3::{Stage3, StructureDims};
use super::{
ERR_DEPTH_LIMIT, ERR_EMPTY_INPUT, ERR_INVALID_LITERAL, ERR_MISSING_COLON, ERR_MISSING_COMMA,
ERR_NUMBER, ERR_STRING, ERR_STRING_CONTROL, ERR_STRING_ESCAPE, ERR_TRAILING_CONTENT,
ERR_UNBALANCED, ERR_UNEXPECTED_TOKEN, ERR_UNTERMINATED_STRING, ERR_UTF8,
};
const ERR_SYNTAX_RESERVED: u32 = 2;
#[derive(Debug)]
pub enum GpuInput<'a> {
Bytes(&'a [u8]),
External {
buffer: GpuBuffer,
len: usize,
},
Pooled {
buffer: GpuBuffer,
len: usize,
},
}
#[derive(Debug)]
pub enum GpuParse {
Rejected(u64),
Accepted(GpuParseOutput),
}
#[derive(Debug)]
pub struct GpuParseOutput {
pub tape: GpuBuffer,
pub stringbuf: Option<GpuBuffer>,
pub fixup_tokens: Vec<u32>,
pub long_string_fixups: Vec<u32>,
}
struct StringBuffers {
record_offsets: GpuBuffer,
stringbuf: GpuBuffer,
chunk_error: GpuBuffer,
long_count: GpuBuffer,
long_list: GpuBuffer,
chunks: usize,
}
struct NumberBuffers {
err_min_pos: GpuBuffer,
fixup_count: GpuBuffer,
fixup_tokens: GpuBuffer,
}
#[derive(Debug)]
pub struct GpuPipeline {
stage3: Stage3,
parse_numbers: Stage,
string_offsets: Stage,
strings_unescape: Stage,
}
impl GpuPipeline {
#[must_use]
pub const fn new() -> Self {
Self {
stage3: Stage3::new(),
parse_numbers: Stage::new("parse_numbers"),
string_offsets: Stage::new("string_record_offsets"),
strings_unescape: Stage::new("strings_unescape"),
}
}
pub fn run(&self, ctx: &MetalContext, input: &[u8], max_depth: u32) -> Result<GpuParse> {
self.run_pooled(ctx, &ScratchPool::new(), GpuInput::Bytes(input), max_depth)
}
pub fn run_pooled(
&self,
ctx: &MetalContext,
pool: &ScratchPool,
input: GpuInput<'_>,
max_depth: u32,
) -> Result<GpuParse> {
super::timing::begin_parse();
let alloc = Alloc::Pool(pool);
let t = super::timing::start();
let mut bufs1 = match input {
GpuInput::Bytes(bytes) => {
let bufs = Stage1Buffers::new_in(ctx, alloc, bytes)?;
super::timing::record("stage1 alloc + input copy", t, 0.0);
bufs
}
GpuInput::External { buffer, len } => {
let bufs = Stage1Buffers::with_external_input(ctx, alloc, buffer, len)?;
super::timing::record("stage1 alloc (zero-copy input)", t, 0.0);
bufs
}
GpuInput::Pooled { buffer, len } => {
let bufs = Stage1Buffers::with_pooled_input(ctx, alloc, buffer, len)?;
super::timing::record("stage1 alloc (pooled input)", t, 0.0);
bufs
}
};
let result = self.drive(ctx, pool, &mut bufs1, max_depth);
let t = super::timing::start();
bufs1.recycle(pool);
super::timing::record("scratch recycle (stage1)", t, 0.0);
result
}
fn drive(
&self,
ctx: &MetalContext,
pool: &ScratchPool,
bufs1: &mut Stage1Buffers,
max_depth: u32,
) -> Result<GpuParse> {
let alloc = Alloc::Pool(pool);
let Stage2Accepted {
bufs2,
header,
gpu_seconds: _,
} = match self.stage3.stage2().run_to_lists(ctx, alloc, bufs1)? {
Stage2Run::Rejected(rejection) => {
return Ok(GpuParse::Rejected(rejection.packed));
}
Stage2Run::Accepted(run) => *run,
};
let t = super::timing::start();
let token_total = bufs2.token_total();
let skeleton_total =
usize::try_from(header.skeleton_total).expect("skeleton_total fits usize");
let string_total = usize::try_from(header.string_total).expect("string_total fits usize");
let scalar_total = usize::try_from(header.scalar_total).expect("scalar_total fits usize");
let stringbuf_total =
usize::try_from(header.stringbuf_total).expect("stringbuf_total fits usize");
let tape_words =
usize::try_from(header.tape_word_total).expect("tape_word_total fits usize") + 2;
let input_len = bufs1.input_len() as u64;
let mut tape_buf = alloc.buffer(ctx, tape_words * size_of::<u64>())?;
if skeleton_total == 0 {
let words = tape_buf.as_mut_slice::<u64>();
words[0] = make_root(tape_words as u64 - 1);
words[tape_words - 1] = make_final_root();
}
let mut bufs3 = if skeleton_total > 0 {
self.stage3.assert_threadgroup_support(ctx)?;
Some(Stage3Buffers::new_in(
ctx,
alloc,
skeleton_total,
sort_passes(max_depth),
)?)
} else {
None
};
let mut string_bufs = if string_total > 0 {
for stage in [&self.string_offsets, &self.strings_unescape] {
let max = stage.pipeline(ctx)?.max_total_threads_per_threadgroup();
assert!(
max >= THREADGROUP_SIZE,
"kernel `{}` supports only {max} threads/threadgroup (< {THREADGROUP_SIZE})",
stage.name()
);
}
let chunks = string_total.div_ceil(THREADGROUP_SIZE);
let stringbuf = alloc.buffer(ctx, stringbuf_total)?;
let mut long_count = alloc.buffer(ctx, size_of::<u32>())?;
long_count.as_mut_slice::<u32>()[0] = 0;
Some(StringBuffers {
record_offsets: alloc.buffer(ctx, string_total * size_of::<u64>())?,
stringbuf,
chunk_error: alloc.buffer(ctx, chunks * size_of::<u64>())?,
long_count,
long_list: alloc.buffer(ctx, string_total * size_of::<u32>())?,
chunks,
})
} else {
None
};
let mut number_bufs = if scalar_total > 0 {
let mut err_min_pos = alloc.buffer(ctx, size_of::<u32>())?;
err_min_pos.as_mut_slice::<u32>()[0] = NO_NUMBER_ERROR;
let mut fixup_count = alloc.buffer(ctx, size_of::<u32>())?;
fixup_count.as_mut_slice::<u32>()[0] = 0;
Some(NumberBuffers {
err_min_pos,
fixup_count,
fixup_tokens: alloc.buffer(ctx, scalar_total * size_of::<u32>())?,
})
} else {
None
};
super::timing::record("sync2: tape/scratch alloc", t, 0.0);
let t = super::timing::start();
if bufs3.is_some() || string_bufs.is_some() || number_bufs.is_some() {
let mut batch = ctx.batch()?;
let h_tape = batch.bind_write(&mut tape_buf);
let h_header = batch.bind_write(&mut bufs1.header);
if let Some(bufs3) = bufs3.as_mut() {
self.stage3.encode_structure(
&mut batch,
&bufs2,
bufs3,
h_tape,
h_header,
&StructureDims {
input_len,
max_depth,
tape_words: tape_words as u64,
},
)?;
}
let h_input = batch.bind_read(&bufs1.input);
let h_pos = batch.bind_read(bufs1.tok_pos.as_ref().expect("tokens allocated"));
let h_tape_ofs = batch.bind_read(&bufs2.tape_ofs);
if let Some(sb) = string_bufs.as_mut() {
let h_kind = batch.bind_read(bufs1.tok_kind.as_ref().expect("tokens allocated"));
let h_counts = batch.bind_read(&bufs2.chunk_counts);
let h_sbytes = batch.bind_read(&bufs2.chunk_string_bytes);
let h_strings =
batch.bind_read(bufs2.string_tokens.as_ref().expect("lists allocated"));
let h_offsets = batch.bind_write(&mut sb.record_offsets);
let h_sbuf = batch.bind_write(&mut sb.stringbuf);
let h_serr = batch.bind_write(&mut sb.chunk_error);
let h_lcount = batch.bind_write(&mut sb.long_count);
let h_llist = batch.bind_write(&mut sb.long_list);
let token_params = MjParams {
input_len,
element_count: token_total as u64,
..Default::default()
};
self.string_offsets.encode(
&mut batch,
&[h_pos, h_kind, h_counts, h_sbytes, h_offsets],
Some(&token_params),
Dispatch::Threadgroups(bufs2.chunks()),
)?;
let string_params = MjParams {
input_len,
element_count: string_total as u64,
reserved0: tape_words as u64, reserved1: token_total as u64, };
self.strings_unescape.encode(
&mut batch,
&[
h_input, h_pos, h_strings, h_offsets, h_tape_ofs, h_sbuf, h_tape, h_serr,
h_lcount, h_llist,
],
Some(&string_params),
Dispatch::Threadgroups(sb.chunks),
)?;
let fold_params = MjParams {
input_len,
element_count: sb.chunks as u64,
..Default::default()
};
self.stage3.finalize_stage().encode(
&mut batch,
&[h_serr, h_header],
Some(&fold_params),
Dispatch::Threadgroups(1),
)?;
}
if let Some(nb) = number_bufs.as_mut() {
let h_scalars =
batch.bind_read(bufs2.scalar_tokens.as_ref().expect("lists allocated"));
let h_nerr = batch.bind_write(&mut nb.err_min_pos);
let h_ncount = batch.bind_write(&mut nb.fixup_count);
let h_nfix = batch.bind_write(&mut nb.fixup_tokens);
let number_params = MjParams {
input_len,
element_count: scalar_total as u64,
..Default::default()
};
self.parse_numbers.encode(
&mut batch,
&[
h_input, h_scalars, h_pos, h_tape_ofs, h_tape, h_nerr, h_ncount, h_nfix,
],
Some(&number_params),
Dispatch::Threads(scalar_total),
)?;
}
let cb3_gpu = batch.commit_and_wait_timed()?;
super::timing::record("cb3 (structure + strings + numbers)", t, cb3_gpu);
} else {
super::timing::record("cb3 (skipped: root scalar)", t, 0.0);
}
let t = super::timing::start();
let header = bufs1.read_header();
let mut error: Option<u64> = header.first_error().map(|(o, c)| (o << 32) | u64::from(c));
fn merge(packed: u64, error: &mut Option<u64>) {
*error = Some(error.map_or(packed, |e| e.min(packed)));
}
let input = &bufs1.input.contents()[..input_len as usize];
let mut long_string_fixups = Vec::new();
if let Some(sb) = string_bufs.as_mut() {
let total = (sb.long_count.as_slice::<u32>()[0] as usize).min(string_total);
long_string_fixups = sb.long_list.as_slice::<u32>()[..total].to_vec();
long_string_fixups.sort_unstable();
if let Some(patch_error) = patch_long_strings(
input,
bufs1
.tok_pos
.as_ref()
.expect("tokens allocated")
.as_slice::<u32>(),
bufs2
.string_tokens
.as_ref()
.expect("lists allocated")
.as_slice::<u32>(),
sb.record_offsets.as_slice::<u64>(),
bufs2.tape_ofs.as_slice::<u32>(),
&long_string_fixups,
sb.stringbuf.contents_mut(),
tape_buf.as_mut_slice::<u64>(),
) {
merge(patch_error, &mut error);
}
}
let mut fixup_tokens = Vec::new();
if let Some(nb) = &mut number_bufs {
let err_pos = nb.err_min_pos.as_slice::<u32>()[0];
if err_pos != NO_NUMBER_ERROR {
merge(pack_number_error(u64::from(err_pos)), &mut error);
}
let total = (nb.fixup_count.as_slice::<u32>()[0] as usize).min(scalar_total);
fixup_tokens = nb.fixup_tokens.as_slice::<u32>()[..total].to_vec();
fixup_tokens.sort_unstable();
let tok_pos = bufs1.tok_pos.as_ref().expect("tokens allocated");
if let Some(patch_error) = patch_number_fixups(
input,
tok_pos.as_slice::<u32>(),
bufs2.tape_ofs.as_slice::<u32>(),
&fixup_tokens,
tape_buf.as_mut_slice::<u64>(),
) {
merge(patch_error, &mut error);
}
}
super::timing::record("sync3: verdict + fixup patches", t, 0.0);
let t = super::timing::start();
bufs2.recycle(pool);
if let Some(bufs3) = bufs3 {
bufs3.recycle(pool);
}
let stringbuf = string_bufs.map(|sb| {
let StringBuffers {
record_offsets,
stringbuf,
chunk_error,
long_count,
long_list,
chunks: _,
} = sb;
for buf in [record_offsets, chunk_error, long_count, long_list] {
pool.put_back(buf);
}
stringbuf
});
if let Some(nb) = number_bufs {
let NumberBuffers {
err_min_pos,
fixup_count,
fixup_tokens,
} = nb;
for buf in [err_min_pos, fixup_count, fixup_tokens] {
pool.put_back(buf);
}
}
super::timing::record("scratch recycle (stages 2-3)", t, 0.0);
if let Some(packed) = error {
pool.put_back(tape_buf);
if let Some(buf) = stringbuf {
pool.put_back(buf);
}
return Ok(GpuParse::Rejected(packed));
}
Ok(GpuParse::Accepted(GpuParseOutput {
tape: tape_buf,
stringbuf,
fixup_tokens,
long_string_fixups,
}))
}
}
impl Default for GpuPipeline {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn decode_packed_error(packed: u64, max_depth: u32) -> Error {
let offset = packed >> 32;
let code = packed as u32;
let syntax = |kind: SyntaxErrorKind| Error::Syntax { offset, kind };
match code {
ERR_UTF8 => Error::Utf8 { offset },
ERR_SYNTAX_RESERVED => syntax(SyntaxErrorKind::UnexpectedToken),
ERR_DEPTH_LIMIT => Error::DepthLimit {
offset,
limit: max_depth,
},
ERR_TRAILING_CONTENT => Error::TrailingContent { offset },
ERR_NUMBER => syntax(SyntaxErrorKind::InvalidNumber),
ERR_STRING => syntax(SyntaxErrorKind::UnterminatedString),
ERR_MISSING_COLON => syntax(SyntaxErrorKind::MissingColon),
ERR_MISSING_COMMA => syntax(SyntaxErrorKind::MissingComma),
ERR_UNEXPECTED_TOKEN => syntax(SyntaxErrorKind::UnexpectedToken),
ERR_INVALID_LITERAL => syntax(SyntaxErrorKind::InvalidLiteral),
ERR_UNBALANCED => syntax(SyntaxErrorKind::UnbalancedBrackets),
ERR_UNTERMINATED_STRING => syntax(SyntaxErrorKind::UnterminatedString),
ERR_EMPTY_INPUT => syntax(SyntaxErrorKind::EmptyInput),
ERR_STRING_ESCAPE => syntax(SyntaxErrorKind::InvalidStringEscape),
ERR_STRING_CONTROL => syntax(SyntaxErrorKind::ControlCharacterInString),
other => Error::CommandBuffer {
message: format!("GPU reported unknown error code {other} at byte {offset}"),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tape::{
make_close, make_double_marker, make_false, make_int64_marker, make_null, make_open,
make_string, make_true,
};
fn gpu_or_skip(test: &str) -> Option<(MetalContext, GpuPipeline)> {
match MetalContext::new() {
Ok(ctx) => Some((ctx, GpuPipeline::new())),
Err(err) => {
if std::env::var_os("METAL_JSON_REQUIRE_GPU").is_some_and(|v| v == "1") {
panic!("METAL_JSON_REQUIRE_GPU=1 but no usable Metal device: {err}");
}
eprintln!("SKIP {test}: no usable Metal device here ({err})");
None
}
}
}
fn run(pipeline: &GpuPipeline, ctx: &MetalContext, input: &[u8]) -> GpuParse {
pipeline
.run(ctx, input, crate::parser::DEFAULT_MAX_DEPTH)
.unwrap_or_else(|e| {
panic!(
"GPU pipeline failed on {:?}: {e}",
String::from_utf8_lossy(&input[..input.len().min(60)])
)
})
}
fn accept(pipeline: &GpuPipeline, ctx: &MetalContext, input: &[u8]) -> GpuParseOutput {
match run(pipeline, ctx, input) {
GpuParse::Accepted(out) => out,
GpuParse::Rejected(packed) => panic!(
"{:?} must parse, got packed error {:?}",
String::from_utf8_lossy(input),
(packed >> 32, packed as u32)
),
}
}
fn reject(pipeline: &GpuPipeline, ctx: &MetalContext, input: &[u8]) -> (u64, u32) {
match run(pipeline, ctx, input) {
GpuParse::Rejected(packed) => (packed >> 32, packed as u32),
GpuParse::Accepted(_) => panic!(
"{:?} must be rejected",
String::from_utf8_lossy(input)
),
}
}
#[test]
fn worked_example_full_tape_and_stringbuf() {
let Some((ctx, pipeline)) = gpu_or_skip("worked_example_full_tape_and_stringbuf") else {
return;
};
let out = accept(&pipeline, &ctx, br#"{"a":[1,2.5],"b":"x\n"}"#);
let expected: [u64; 13] = [
make_root(12),
make_open(b'{', 12, 2),
make_string(0),
make_open(b'[', 9, 2),
make_int64_marker(),
1,
make_double_marker(),
2.5f64.to_bits(),
make_close(b']', 3),
make_string(6),
make_string(12),
make_close(b'}', 1),
make_final_root(),
];
assert_eq!(out.tape.as_slice::<u64>(), expected);
let sb = out.stringbuf.expect("three strings");
let bytes = sb.contents();
assert_eq!(bytes.len(), 20);
assert_eq!(&bytes[0..6], &[1, 0, 0, 0, b'a', 0]);
assert_eq!(&bytes[6..12], &[1, 0, 0, 0, b'b', 0]);
assert_eq!(&bytes[12..19], &[2, 0, 0, 0, b'x', 0x0A, 0]);
assert_eq!(bytes[19], 0);
assert!(out.fixup_tokens.is_empty());
}
#[test]
fn root_scalars_produce_complete_tapes() {
let Some((ctx, pipeline)) = gpu_or_skip("root_scalars_produce_complete_tapes") else {
return;
};
let out = accept(&pipeline, &ctx, b"42");
assert_eq!(
out.tape.as_slice::<u64>(),
[make_root(3), make_int64_marker(), 42, make_final_root()]
);
assert!(out.stringbuf.is_none());
let out = accept(&pipeline, &ctx, b"true");
assert_eq!(
out.tape.as_slice::<u64>(),
[make_root(2), make_true(), make_final_root()]
);
let out = accept(&pipeline, &ctx, b" null \n");
assert_eq!(
out.tape.as_slice::<u64>(),
[make_root(2), make_null(), make_final_root()]
);
let out = accept(&pipeline, &ctx, br#""x""#);
assert_eq!(
out.tape.as_slice::<u64>(),
[make_root(2), make_string(0), make_final_root()]
);
let sb = out.stringbuf.expect("root string");
assert_eq!(sb.contents(), &[1, 0, 0, 0, b'x', 0]);
let out = accept(&pipeline, &ctx, b"-0.0");
assert_eq!(
out.tape.as_slice::<u64>(),
[
make_root(3),
make_double_marker(),
(-0.0f64).to_bits(),
make_final_root()
]
);
let out = accept(&pipeline, &ctx, b"[true,false,null]");
assert_eq!(
out.tape.as_slice::<u64>(),
[
make_root(6),
make_open(b'[', 6, 3),
make_true(),
make_false(),
make_null(),
make_close(b']', 1),
make_final_root()
]
);
}
#[test]
fn every_error_class_rejects_with_reference_offset_and_code() {
let Some((ctx, pipeline)) =
gpu_or_skip("every_error_class_rejects_with_reference_offset_and_code")
else {
return;
};
let cases: &[(&[u8], u64, u32)] = &[
(b"ab\x80", 2, ERR_UTF8), (b"\"abc", 4, ERR_STRING), (b"", 0, ERR_EMPTY_INPUT), (b" \t\n", 0, ERR_EMPTY_INPUT), (b"[1 true]", 3, ERR_MISSING_COMMA), (b"]", 0, ERR_UNEXPECTED_TOKEN), (b"nan", 0, ERR_INVALID_LITERAL), (br#"{"a" "b"}"#, 5, ERR_MISSING_COLON), (b"[1", 0, ERR_UNBALANCED), (b"{},1", 2, ERR_TRAILING_CONTENT), (b"[01]", 1, ERR_NUMBER), (br#"{"k":1e999}"#, 5, ERR_NUMBER), (br#"["\q"]"#, 2, ERR_STRING_ESCAPE), (b"[\"a\x01b\"]", 3, ERR_STRING_CONTROL), ];
for &(input, offset, code) in cases {
assert_eq!(
reject(&pipeline, &ctx, input),
(offset, code),
"{:?}",
String::from_utf8_lossy(input)
);
}
let GpuParse::Rejected(packed) = pipeline.run(&ctx, b"[[[[]]]]", 3).unwrap() else {
panic!("depth 4 at limit 3 must reject");
};
assert_eq!((packed >> 32, packed as u32), (3, ERR_DEPTH_LIMIT));
}
#[test]
fn scalar_error_merge_is_earliest_offset_first() {
let Some((ctx, pipeline)) = gpu_or_skip("scalar_error_merge_is_earliest_offset_first")
else {
return;
};
assert_eq!(
reject(&pipeline, &ctx, br#"{"a":"\q","b":01}"#),
(6, ERR_STRING_ESCAPE)
);
assert_eq!(
reject(&pipeline, &ctx, br#"[01,"\q"]"#),
(1, ERR_NUMBER)
);
}
#[test]
fn error_code_mapping_is_complete_and_pinned() {
let mut codes: Vec<(String, u32)> = Vec::new();
for src in [
include_str!("../../shaders/common.h"),
include_str!("../../shaders/13_strings.metal"),
] {
for line in src.lines() {
let line = line.trim();
let Some(rest) = line
.strip_prefix("MJ_ERR_")
.or_else(|| line.strip_prefix("constant constexpr uint MJ_ERR_"))
else {
continue;
};
let Some((name, rest)) = rest.split_once('=') else {
continue;
};
let digits: String = rest
.trim_start()
.chars()
.take_while(char::is_ascii_digit)
.collect();
let value: u32 = digits.parse().expect("MJ_ERR_ value parses");
codes.push((format!("MJ_ERR_{}", name.trim()), value));
}
}
assert_eq!(codes.len(), 15, "13 common.h codes + 2 K11 codes: {codes:?}");
for (name, code) in &codes {
let err = decode_packed_error((77 << 32) | u64::from(*code), 1024);
assert!(
!matches!(err, Error::CommandBuffer { .. }),
"{name} ({code}) must map to a public error, got {err:?}"
);
}
assert!(matches!(
decode_packed_error((77 << 32) | 7, 1024),
Error::CommandBuffer { .. }
));
let kind = |code: u32| match decode_packed_error((9 << 32) | u64::from(code), 1024) {
Error::Syntax { offset: 9, kind } => kind,
other => panic!("code {code}: expected Syntax, got {other:?}"),
};
assert!(matches!(
decode_packed_error((9 << 32) | u64::from(ERR_UTF8), 1024),
Error::Utf8 { offset: 9 }
));
assert!(matches!(
decode_packed_error((9 << 32) | u64::from(ERR_DEPTH_LIMIT), 64),
Error::DepthLimit {
offset: 9,
limit: 64
}
));
assert!(matches!(
decode_packed_error((9 << 32) | u64::from(ERR_TRAILING_CONTENT), 1024),
Error::TrailingContent { offset: 9 }
));
assert_eq!(kind(ERR_SYNTAX_RESERVED), SyntaxErrorKind::UnexpectedToken);
assert_eq!(kind(ERR_NUMBER), SyntaxErrorKind::InvalidNumber);
assert_eq!(kind(ERR_STRING), SyntaxErrorKind::UnterminatedString);
assert_eq!(kind(ERR_MISSING_COLON), SyntaxErrorKind::MissingColon);
assert_eq!(kind(ERR_MISSING_COMMA), SyntaxErrorKind::MissingComma);
assert_eq!(kind(ERR_UNEXPECTED_TOKEN), SyntaxErrorKind::UnexpectedToken);
assert_eq!(kind(ERR_INVALID_LITERAL), SyntaxErrorKind::InvalidLiteral);
assert_eq!(kind(ERR_UNBALANCED), SyntaxErrorKind::UnbalancedBrackets);
assert_eq!(
kind(ERR_UNTERMINATED_STRING),
SyntaxErrorKind::UnterminatedString
);
assert_eq!(kind(ERR_EMPTY_INPUT), SyntaxErrorKind::EmptyInput);
assert_eq!(kind(ERR_STRING_ESCAPE), SyntaxErrorKind::InvalidStringEscape);
assert_eq!(
kind(ERR_STRING_CONTROL),
SyntaxErrorKind::ControlCharacterInString
);
}
#[test]
fn fixup_numbers_inside_full_documents_are_patched() {
let Some((ctx, pipeline)) =
gpu_or_skip("fixup_numbers_inside_full_documents_are_patched")
else {
return;
};
let halfway = "1.00000000000000011102230246251565404236316680908203125";
let oracle: f64 = halfway.parse().unwrap();
assert_eq!(oracle.to_bits(), 1.0f64.to_bits(), "fixture sanity");
let json = format!(r#"{{"k":[{halfway},2],"s":"v"}}"#);
let out = accept(&pipeline, &ctx, json.as_bytes());
assert!(
!out.fixup_tokens.is_empty(),
"a truncated halfway literal must take the fixup path"
);
let tape = out.tape.as_slice::<u64>();
assert_eq!(tape[4], make_double_marker());
assert_eq!(tape[5], oracle.to_bits(), "CPU-patched value word");
assert_eq!(tape[6], make_int64_marker());
assert_eq!(tape[7], 2);
}
}