use std::fmt::{Debug, Formatter};
use std::ops::Range;
use std::str::FromStr;
use winnow::ascii::alphanumeric1;
use winnow::combinator::{
alt, delimited, empty, eof, not, opt, peek, preceded, repeat, separated_pair, terminated,
};
use winnow::error::{ErrMode, Needed};
use winnow::stream::{
Accumulate, CompareResult, ContainsToken, FindSlice, Location, SliceLen, Stream,
StreamIsPartial,
};
use winnow::token::{one_of, take_till, take_until, take_while};
use winnow::{dispatch, Parser};
use crate::lazy::decoder::{LazyRawValueExpr, RawValueExpr};
use crate::lazy::encoding::{TextEncoding, TextEncoding_1_0, TextEncoding_1_1};
use crate::lazy::expanded::EncodingContextRef;
use crate::lazy::raw_stream_item::{EndPosition, LazyRawStreamItem, RawStreamItem};
use crate::lazy::text::encoded_value::EncodedTextValue;
use crate::lazy::text::matched::{
MatchedBlob, MatchedClob, MatchedDecimal, MatchedFieldName, MatchedFieldNameSyntax,
MatchedFloat, MatchedInt, MatchedString, MatchedSymbol, MatchedTimestamp,
MatchedTimestampOffset, MatchedValue,
};
use crate::lazy::text::parse_result::IonParseError;
use crate::lazy::text::parse_result::{IonMatchResult, IonParseResult};
use crate::lazy::text::raw::v1_1::arg_group::{EExpArg, EExpArgExpr, TextEExpArgGroup};
use crate::lazy::text::raw::v1_1::reader::{
MacroIdLike, MacroIdRef, SystemMacroAddress, TextEExpression_1_1,
};
use crate::lazy::text::value::{
LazyRawTextValue, LazyRawTextValue_1_0, LazyRawTextValue_1_1, LazyRawTextVersionMarker,
};
use crate::result::DecodingError;
use crate::{
Encoding, HasRange, IonError, IonResult, IonType, RawSymbolRef, Span, TimestampPrecision,
};
use crate::lazy::expanded::macro_table::ION_1_1_SYSTEM_MACROS;
use crate::lazy::expanded::template::{Parameter, RestSyntaxPolicy};
use crate::lazy::text::as_utf8::AsUtf8;
use crate::lazy::text::raw::sequence::RawTextSExpIterator;
use crate::lazy::text::token_kind::{ValueTokenKind, TEXT_ION_TOKEN_KINDS};
use bumpalo::collections::Vec as BumpVec;
use winnow::ascii::{digit0, digit1};
macro_rules! scalar_value_matchers {
($($parser:expr => $variant:ident => $new_parser:ident),*$(,)?) => {
$(fn $new_parser<E: TextEncoding>(&mut self) -> IonParseResult<'top, EncodedTextValue<'top, E>> {
$parser.map(|matched| EncodedTextValue::new(MatchedValue::$variant(matched))).parse_next(self)
})*
};
}
impl Debug for TextBuffer<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
const CHARS_TO_SHOW: usize = 32;
write!(f, "TextBuffer {{")?;
let text_result = std::str::from_utf8(self.bytes());
if let Ok(text) = text_result {
let mut chars = text.chars();
let text_head = (&mut chars).take(CHARS_TO_SHOW).collect::<String>();
let ellipse = if chars.next().is_some() { "..." } else { "" };
write!(f, "text=\"{text_head}\"{ellipse}",)?;
} else {
write!(f, "Invalid UTF-8, bytes=")?;
for byte in self.bytes().iter().take(CHARS_TO_SHOW) {
write!(f, "{:x?} ", *byte)?;
}
if self.bytes().len() > CHARS_TO_SHOW {
write!(f, "...{} more bytes", self.bytes().len() - CHARS_TO_SHOW)?;
}
}
let range = self.range();
write!(f, ", range={}..{}}}", range.start, range.end)
}
}
pub(crate) const WHITESPACE_BYTES: &[u8] = b" \t\r\n\x09\x0B\x0C";
#[derive(Clone, Copy)]
pub struct TextBuffer<'top> {
input_span: Span<'top>,
pub(crate) context: EncodingContextRef<'top>,
is_final_data: bool,
}
impl PartialEq for TextBuffer<'_> {
fn eq(&self, other: &Self) -> bool {
self.input_span == other.input_span
}
}
impl<'top> TextBuffer<'top> {
pub fn from_span(
context: EncodingContextRef<'top>,
input_span: Span<'top>,
is_final_data: bool,
) -> TextBuffer<'top> {
TextBuffer {
context,
input_span,
is_final_data,
}
}
pub fn new(context: EncodingContextRef<'top>, bytes: &'top [u8]) -> TextBuffer<'top> {
Self::with_offset(context, 0, bytes, true)
}
pub fn with_offset(
context: EncodingContextRef<'top>,
offset: usize,
bytes: &'top [u8],
is_final_data: bool,
) -> TextBuffer<'top> {
TextBuffer {
context,
input_span: Span::with_offset(offset, bytes),
is_final_data,
}
}
pub fn consume(&mut self, num_bytes: usize) {
debug_assert!(
self.len() >= num_bytes,
"tried to conusume {num_bytes} bytes, but only {} were available",
self.len()
);
self.input_span = self.input_span.slice_to_end(num_bytes);
}
pub fn context(&self) -> EncodingContextRef<'top> {
self.context
}
pub fn slice(&self, offset: usize, length: usize) -> TextBuffer<'top> {
TextBuffer {
input_span: self.input_span.slice(offset, length),
is_final_data: true,
..*self
}
}
pub fn slice_to_end(&self, offset: usize) -> TextBuffer<'top> {
TextBuffer {
input_span: self.input_span.slice_to_end(offset),
..*self
}
}
pub fn bytes(&self) -> &'top [u8] {
self.input_span.bytes()
}
pub fn peek_byte(&self) -> IonParseResult<'top, u8> {
let Some(byte) = self.bytes().first().copied() else {
return self.incomplete("a value");
};
Ok(byte)
}
pub fn offset(&self) -> usize {
self.input_span.offset()
}
pub fn len(&self) -> usize {
self.input_span.len()
}
pub fn range(&self) -> Range<usize> {
self.input_span.range()
}
pub fn is_empty(&self) -> bool {
self.input_span.is_empty()
}
pub fn as_text<'a>(&'a self) -> IonResult<&'top str> {
std::str::from_utf8(self.bytes()).map_err(move |_| {
let decoding_error =
DecodingError::new("encountered invalid UTF-8").with_position(self.offset());
IonError::Decoding(decoding_error)
})
}
#[inline]
fn match_nothing(&mut self) -> IonMatchResult<'top> {
empty.take().parse_next(self)
}
pub fn match_whitespace1(&mut self) -> IonMatchResult<'top> {
let result = take_while(1.., WHITESPACE_BYTES).parse_next(self)?;
Ok(result)
}
pub fn match_whitespace0(&mut self) -> IonMatchResult<'top> {
let result = take_while(0.., WHITESPACE_BYTES).parse_next(self)?;
Ok(result)
}
#[inline]
pub fn match_optional_comments_and_whitespace(&mut self) -> IonMatchResult<'top> {
pub fn full_match_optional_comments_and_whitespace<'t>(
input: &mut TextBuffer<'t>,
) -> IonMatchResult<'t> {
zero_or_more(alt((
TextBuffer::match_whitespace1,
TextBuffer::match_comment,
)))
.parse_next(input)
}
if let Some(&byte) = self.bytes().first() {
if WHITESPACE_BYTES.contains_token(byte) || byte == b'/' {
return full_match_optional_comments_and_whitespace
.context("reading whitespace/comments")
.parse_next(self);
}
}
self.match_nothing()
}
pub fn match_comment(&mut self) -> IonMatchResult<'top> {
alt((
Self::match_rest_of_line_comment,
Self::match_multiline_comment,
))
.parse_next(self)
}
fn match_rest_of_line_comment(&mut self) -> IonMatchResult<'top> {
("//", take_till(.., b"\r\n")).take().parse_next(self)
}
fn match_multiline_comment(&mut self) -> IonMatchResult<'top> {
let result = (
"/*",
take_until(.., "*/"),
"*/",
)
.take()
.parse_next(self)?;
Ok(result)
}
pub fn match_ivm<E: TextEncoding>(
&mut self,
) -> IonParseResult<'top, LazyRawTextVersionMarker<'top, E>> {
let ((matched_major, matched_minor), matched_marker) = terminated(
preceded("$ion_", separated_pair(digit1, "_", digit1)),
peek(whitespace_and_then(not(":"))),
)
.with_taken()
.parse_next(self)?;
let major_version = u8::from_str(matched_major.as_text().unwrap()).map_err(|_| {
matched_major
.invalid("value did not fit in an unsigned byte")
.context("reading an IVM major version")
.cut_err()
})?;
let minor_version = u8::from_str(matched_minor.as_text().unwrap()).map_err(|_| {
matched_major
.invalid("value did not fit in an unsigned byte")
.context("reading an IVM minor version")
.cut_err()
})?;
let marker =
LazyRawTextVersionMarker::<E>::new(matched_marker, major_version, minor_version);
Ok(marker)
}
#[inline]
pub fn match_annotations(&mut self) -> IonMatchResult<'top> {
#[inline(never)]
fn full_match_annotations<'t>(input: &mut TextBuffer<'t>) -> IonMatchResult<'t> {
let matched = one_or_more(TextBuffer::match_annotation).parse_next(input)?;
if matched.len() > u16::MAX as usize {
matched
.invalid("the maximum supported annotations sequence length is 65KB")
.context("reading an annotations sequence")
.cut()
} else {
Ok(matched)
}
}
if let Some(&byte) = self.bytes().first() {
if [b'\'', b'$', b'_'].contains(&byte) || byte.is_ascii_alphabetic() {
return full_match_annotations(self);
}
};
self.match_nothing()
}
pub fn match_annotation(&mut self) -> IonParseResult<'top, (MatchedSymbol, TextBuffer<'top>)> {
terminated(
whitespace_and_then(Self::match_symbol.with_taken()),
whitespace_and_then(("::", Self::match_optional_comments_and_whitespace)),
)
.parse_next(self)
}
pub fn match_sexp_item_1_1(
&mut self,
) -> IonParseResult<'top, Option<LazyRawValueExpr<'top, TextEncoding_1_1>>> {
let input = *self;
let result = whitespace_and_then(alt((
Self::match_e_expression.map(|matched| Some(RawValueExpr::EExp(matched))),
peek(")").value(None),
(
opt(Self::match_annotations),
whitespace_and_then(alt((
Self::match_value::<TextEncoding_1_1>,
Self::match_operator,
))),
)
.map(|(maybe_annotations, value)| input.apply_annotations(maybe_annotations, value))
.map(RawValueExpr::ValueLiteral)
.map(Some),
)))
.parse_next(self);
result
}
#[inline]
pub(crate) fn apply_annotations<Encoding: TextEncoding>(
&self,
maybe_annotations: Option<TextBuffer<'top>>,
mut value: LazyRawTextValue<'top, Encoding>,
) -> LazyRawTextValue<'top, Encoding> {
fn full_apply_annotations<'t, T: TextEncoding>(
input: &TextBuffer<'t>,
annotations: &TextBuffer<'t>,
value: &mut LazyRawTextValue<'t, T>,
) {
let annotations_length =
u16::try_from(annotations.len()).expect("already length checked");
value.encoded_value = value
.encoded_value
.with_annotations_sequence(annotations_length);
let unannotated_value_length = value.input.len();
value.input = input.slice(
annotations.offset() - input.offset(),
annotations_length as usize + unannotated_value_length,
);
}
if let Some(annotations) = maybe_annotations {
full_apply_annotations(self, &annotations, &mut value);
}
value
}
pub fn match_annotated_value<E: TextEncoding>(
&mut self,
) -> IonParseResult<'top, E::Value<'top>> {
let input = *self;
(
opt(Self::match_annotations),
whitespace_and_then(Self::match_value::<E>),
)
.map(|(maybe_annotations, value)| input.apply_annotations(maybe_annotations, value))
.parse_next(self)
}
pub fn match_struct_field_name(&mut self) -> IonParseResult<'top, MatchedFieldName<'top>> {
alt((
Self::match_string.map(MatchedFieldNameSyntax::String),
Self::match_symbol.map(MatchedFieldNameSyntax::Symbol),
))
.with_taken()
.map(
#[inline]
|(syntax, matched_input)| MatchedFieldName::new(matched_input, syntax),
)
.parse_next(self)
}
pub fn match_top_level_item_1_0(
&mut self,
) -> IonParseResult<'top, LazyRawStreamItem<'top, TextEncoding_1_0>> {
let _discarded_ws = self.match_optional_comments_and_whitespace()?;
if self.is_empty() {
return Ok(RawStreamItem::EndOfStream(EndPosition::new(
TextEncoding_1_0.encoding(),
self.offset(),
)));
}
alt((
Self::match_ivm::<TextEncoding_1_0>.map(RawStreamItem::VersionMarker),
Self::match_annotated_value::<TextEncoding_1_0>
.map(LazyRawTextValue_1_0::from)
.map(RawStreamItem::Value),
))
.parse_next(self)
}
pub fn match_top_level_item_1_1(
&mut self,
) -> IonParseResult<'top, LazyRawStreamItem<'top, TextEncoding_1_1>> {
let _discarded_whitespace = self.match_optional_comments_and_whitespace()?;
if self.is_empty() {
return Ok(RawStreamItem::EndOfStream(EndPosition::new(
TextEncoding_1_1.encoding(),
self.offset(),
)));
}
alt((
Self::match_ivm::<TextEncoding_1_1>.map(RawStreamItem::VersionMarker),
Self::match_e_expression.map(RawStreamItem::EExp),
Self::match_annotated_value::<TextEncoding_1_1>
.map(LazyRawTextValue_1_1::from)
.map(RawStreamItem::Value),
))
.context("reading a v1.1 top-level expression")
.parse_next(self)
}
pub fn match_value<E: TextEncoding>(&mut self) -> IonParseResult<'top, E::Value<'top>> {
use ValueTokenKind::*;
dispatch! {
|input: &mut TextBuffer<'top>| Ok(TEXT_ION_TOKEN_KINDS[input.peek_byte()? as usize]);
NumberOrTimestamp => alt((
Self::match_int_value,
Self::match_float_value,
Self::match_decimal_value,
Self::match_timestamp_value,
)),
Letter => alt((
Self::match_null_value,
Self::match_bool_value,
Self::match_identifier_value,
Self::match_float_special_value, )),
Symbol => Self::match_symbol_value,
QuotedText => alt((Self::match_string_value, Self::match_symbol_value)),
List => E::list_matcher(),
SExp => E::sexp_matcher(),
LobOrStruct => alt((
Self::match_blob_value,
Self::match_clob_value,
E::struct_matcher(),
)),
Invalid(_byte) => |input: &mut TextBuffer<'top>| {
input
.unrecognized()
.context("reading a value")
.backtrack()
},
}
.with_taken()
.map(|(encoded_value, input)| E::new_value(input, encoded_value))
.parse_next(self)
}
pub fn match_e_expression_arg_group(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, TextEExpArgGroup<'top>> {
alt((
Self::parser_with_arg(Self::match_explicit_arg_group, parameter),
Self::parser_with_arg(Self::match_rest, parameter),
))
.parse_next(self)
}
pub fn parser_with_arg<A: 'top, O>(
mut parser: impl FnMut(&mut Self, &'top A) -> IonParseResult<'top, O>,
arg_to_pass: &'top A,
) -> impl IonParser<'top, O> {
move |input: &mut TextBuffer<'top>| parser(input, arg_to_pass)
}
pub fn match_explicit_arg_group(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, TextEExpArgGroup<'top>> {
TextEncoding_1_1::container_matcher(
"an explicit argument group",
"(::",
RawTextSExpIterator::<TextEncoding_1_1>::new,
whitespace_and_then(")"),
)
.with_taken()
.map(|(expr_cache, input)| TextEExpArgGroup::new(parameter, input, expr_cache))
.parse_next(self)
}
pub fn match_e_expression_name(&mut self) -> IonParseResult<'top, MacroIdRef<'top>> {
let (matched_symbol, macro_id_bytes) =
Self::match_identifier.with_taken().parse_next(self)?;
let name = match matched_symbol
.read(self.context.allocator(), macro_id_bytes)
.expect("matched identifier but failed to read its bytes")
{
RawSymbolRef::SymbolId(_) => unreachable!("matched a text identifier, returned a SID"),
RawSymbolRef::Text(text) => text,
RawSymbolRef::SystemSymbol_1_1(system_symbol) => system_symbol.text(),
};
Ok(MacroIdRef::LocalName(name))
}
pub fn match_e_expression_address(&mut self) -> IonParseResult<'top, MacroIdRef<'top>> {
let address = Self::match_address(self)?;
let id = MacroIdRef::LocalAddress(address);
Ok(id)
}
pub fn match_system_eexp_id(&mut self) -> IonParseResult<'top, MacroIdRef<'top>> {
let _matched_system_annotation =
("$ion", whitespace_and_then("::"), Self::match_whitespace0)
.take()
.parse_next(self)?;
let id = alt((
Self::match_e_expression_address,
Self::match_e_expression_name,
))
.parse_next(self)?;
let system_id = match id {
MacroIdRef::LocalName(name) => {
let Some(macro_address) = ION_1_1_SYSTEM_MACROS.address_for_name(name) else {
return self
.invalid(format!("found unrecognized system macro name: '{name}'"))
.context("reading an e-expression's macro ID as a local name")
.cut();
};
MacroIdRef::SystemAddress(SystemMacroAddress::new_unchecked(macro_address))
}
MacroIdRef::LocalAddress(address) => {
let Some(system_address) = SystemMacroAddress::new(address) else {
return self
.invalid(format!(
"found out-of-bounds system macro address {address}",
))
.context("reading an e-expression's macro ID as a system address")
.cut();
};
MacroIdRef::SystemAddress(system_address)
}
MacroIdRef::SystemAddress(_) => {
unreachable!("`match_e_expression_address` always returns a LocalAddress")
}
};
Ok(system_id)
}
pub fn match_e_expression_id(&mut self) -> IonParseResult<'top, MacroIdRef<'top>> {
let id = alt((
Self::match_system_eexp_id,
Self::match_e_expression_name,
Self::match_e_expression_address,
))
.parse_next(self)?;
Ok(id)
}
pub fn match_e_expression(&mut self) -> IonParseResult<'top, TextEExpression_1_1<'top>> {
let original_input = *self;
let parser = |input: &mut TextBuffer<'top>| {
let _opening_tag = "(:".parse_next(input)?;
let id = Self::match_e_expression_id(input)?;
let mut arg_expr_cache = BumpVec::new_in(input.context.allocator());
let macro_ref = id.resolve(input.context().macro_table()).map_err(|_| {
(*input)
.invalid(format!("could not find macro with id {id:?}"))
.context("reading an e-expression")
.cut_err()
})?;
let signature_params: &'top [Parameter] = macro_ref.signature().parameters();
for (index, param) in signature_params.iter().enumerate() {
let maybe_arg = input.match_argument_for(param)?;
match maybe_arg {
Some(arg) => arg_expr_cache.push(arg),
None => {
for param in &signature_params[index..] {
if !param.can_be_omitted() {
return input
.invalid(format!(
"e-expression did not include an argument for param '{}'",
param.name()
))
.context("reading an e-expression")
.cut();
}
}
break;
}
}
}
match whitespace_and_then(")").parse_next(input) {
Ok(_closing_delimiter) => Ok((id, macro_ref, arg_expr_cache)),
Err(ErrMode::Incomplete(_)) => input.incomplete("an e-expression"),
Err(_e) => {
(*input)
.invalid(format!(
"macro {id} signature has {} parameter(s), e-expression had an extra argument",
signature_params.len()
))
.context("reading an e-expression's arguments")
.cut()
}
}
};
let ((macro_id, macro_ref, mut arg_expr_cache), matched_input) =
parser.with_taken().parse_next(self)?;
let parameters = macro_ref.signature().parameters();
if arg_expr_cache.len() < parameters.len() {
let last_explicit_arg_end = arg_expr_cache
.last()
.map(|arg| arg.expr().range().end)
.unwrap_or(self.offset());
let empty_end_slice =
original_input.slice(last_explicit_arg_end - original_input.offset(), 0);
for parameter in ¶meters[arg_expr_cache.len()..] {
arg_expr_cache.push(EExpArg::new(
parameter,
EExpArgExpr::ArgGroup(TextEExpArgGroup::new(parameter, empty_end_slice, &[])),
));
}
}
debug_assert!(
arg_expr_cache.len() == parameters.len(),
"every parameter must have an argument, explicit or implicit"
);
Ok(TextEExpression_1_1::new(
macro_id,
matched_input,
arg_expr_cache.into_bump_slice(),
))
}
pub fn match_argument_for(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, Option<EExpArg<'top, TextEncoding_1_1>>> {
use crate::lazy::expanded::template::ParameterCardinality::*;
match parameter.cardinality() {
ExactlyOne => {
let arg = self.match_exactly_one(parameter)?;
Ok(Some(arg))
}
ZeroOrOne => self.match_zero_or_one(parameter),
ZeroOrMore => self.match_zero_or_more(parameter),
OneOrMore => self.match_one_or_more(parameter),
}
}
pub fn match_exactly_one(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, EExpArg<'top, TextEncoding_1_1>> {
let _whitespace = self.match_optional_comments_and_whitespace()?;
if self.bytes().starts_with(b"(::") {
return self
.invalid(format!("parameter '{}' has cardinality `ExactlyOne`; it cannot accept an expression group", parameter.name()))
.context("reading an e-expression argument with `exactly-one` cardinality")
.cut();
}
let maybe_expr = Self::match_sexp_item_1_1
.map(|expr| expr.map(EExpArgExpr::<TextEncoding_1_1>::from))
.parse_next(self)?;
match maybe_expr {
Some(expr) => Ok(EExpArg::new(parameter, expr)),
None => self
.invalid(format!(
"expected argument for required parameter '{}'",
parameter.name()
))
.context("reading an e-expression argument with `exactly-one` cardinality")
.cut(),
}
}
pub fn match_empty_arg_group(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, EExpArg<'top, TextEncoding_1_1>> {
("(::", whitespace_and_then(")"))
.take()
.map(|matched_expr| {
let arg_group = TextEExpArgGroup::new(parameter, matched_expr, &[]);
EExpArg::new(parameter, EExpArgExpr::ArgGroup(arg_group))
})
.parse_next(self)
}
pub fn match_zero_or_one(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, Option<EExpArg<'top, TextEncoding_1_1>>> {
whitespace_and_then(alt((
Self::parser_with_arg(Self::match_empty_arg_group, parameter).map(Some),
Self::match_sexp_item_1_1.map(|maybe_expr| {
maybe_expr.map(|expr| {
EExpArg::new(parameter, EExpArgExpr::<TextEncoding_1_1>::from(expr))
})
}),
)))
.parse_next(self)
}
pub fn match_zero_or_more(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, Option<EExpArg<'top, TextEncoding_1_1>>> {
let maybe_expr = preceded(
Self::match_optional_comments_and_whitespace,
alt((
Self::parser_with_arg(Self::match_e_expression_arg_group, parameter)
.map(|group| Some(EExpArg::new(parameter, EExpArgExpr::ArgGroup(group)))),
Self::match_sexp_item_1_1.map(|expr| {
expr.map(EExpArgExpr::from)
.map(|expr| EExpArg::new(parameter, expr))
}),
peek(")").value(None),
)),
)
.parse_next(self)?;
Ok(maybe_expr)
}
pub fn match_one_or_more(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, Option<EExpArg<'top, TextEncoding_1_1>>> {
if self.match_empty_arg_group(parameter).is_ok() {
return self
.unrecognized()
.context("reading an e-expression argument with `one-or-more` cardinality")
.backtrack();
}
self.match_zero_or_more(parameter)
}
pub fn match_rest(
&mut self,
parameter: &'top Parameter,
) -> IonParseResult<'top, TextEExpArgGroup<'top>> {
if parameter.rest_syntax_policy() == RestSyntaxPolicy::NotAllowed {
return self
.unrecognized()
.context("reading a parameter that does not support rest syntax")
.backtrack();
}
let mut cache = BumpVec::new_in(self.context().allocator());
let parser = |input: &mut TextBuffer<'top>| {
while let Some(expr) = alt((
whitespace_and_then(peek(")")).value(None),
Self::match_sexp_item_1_1,
))
.parse_next(input)?
{
cache.push(expr);
}
Ok(())
};
let (_, matched_input) = parser.with_taken().parse_next(self)?;
Ok(TextEExpArgGroup::new(
parameter,
matched_input,
cache.into_bump_slice(),
))
}
pub fn match_bool(&mut self) -> IonParseResult<'top, bool> {
terminated(
alt(("true".value(true), "false".value(false))),
Self::peek_stop_character,
)
.parse_next(self)
}
pub fn match_null(&mut self) -> IonParseResult<'top, IonType> {
terminated(
alt((
("null.", Self::match_ion_type).map(|(_, ion_type)| ion_type),
"null".value(IonType::Null),
)),
Self::peek_stop_character,
)
.parse_next(self)
}
fn match_ion_type(&mut self) -> IonParseResult<'top, IonType> {
alt((
"null".value(IonType::Null),
"bool".value(IonType::Bool),
"int".value(IonType::Int),
"float".value(IonType::Float),
"decimal".value(IonType::Decimal),
"timestamp".value(IonType::Timestamp),
"symbol".value(IonType::Symbol),
"string".value(IonType::String),
"clob".value(IonType::Clob),
"blob".value(IonType::Blob),
"list".value(IonType::List),
"sexp".value(IonType::SExp),
"struct".value(IonType::Struct),
))
.parse_next(self)
}
fn match_stop_character(&mut self) -> IonMatchResult<'top> {
alt((
eof,
one_of("{}[](),\"' \t\n\r\u{0b}\u{0c}".as_bytes()).take(),
))
.parse_next(self)
}
fn peek_stop_character(&mut self) -> IonMatchResult<'top> {
peek(Self::match_stop_character).parse_next(self)
}
pub fn match_int(&mut self) -> IonParseResult<'top, MatchedInt> {
terminated(
alt((
Self::match_base_2_int,
Self::match_base_16_int,
Self::match_base_10_int,
)),
Self::peek_stop_character,
)
.parse_next(self)
}
scalar_value_matchers!(
Self::match_null => Null => match_null_value,
Self::match_bool => Bool => match_bool_value,
Self::match_int => Int => match_int_value,
Self::match_float => Float => match_float_value,
Self::match_float_special => Float => match_float_special_value,
Self::match_decimal => Decimal => match_decimal_value,
Self::match_timestamp => Timestamp => match_timestamp_value,
Self::match_string => String => match_string_value,
Self::match_symbol => Symbol => match_symbol_value,
Self::match_identifier => Symbol => match_identifier_value,
Self::match_blob => Blob => match_blob_value,
Self::match_clob => Clob => match_clob_value,
);
fn match_base_2_int(&mut self) -> IonParseResult<'top, MatchedInt> {
let initial_offset = self.offset();
separated_pair(opt("-"), alt(("0b", "0B")), Self::match_base_2_int_digits)
.map(|(maybe_sign, digits)| {
MatchedInt::new(2, maybe_sign.is_some(), digits.offset() - initial_offset)
})
.parse_next(self)
}
fn match_base_2_int_digits(&mut self) -> IonMatchResult<'top> {
terminated(
zero_or_more((take_while(1.., b"01"), "_")),
one_or_more(one_of(b"01")),
)
.take()
.parse_next(self)
}
fn match_base_10_int(&mut self) -> IonParseResult<'top, MatchedInt> {
let initial_offset = self.offset();
(opt("-"), Self::match_base_10_int_digits)
.map(|(maybe_sign, digits)| {
MatchedInt::new(10, maybe_sign.is_some(), digits.offset() - initial_offset)
})
.parse_next(self)
}
fn match_base_10_int_digits(&mut self) -> IonMatchResult<'top> {
Self::match_base_10_digits_before_dot(self)
}
fn match_base_10_digits_before_dot(&mut self) -> IonMatchResult<'top> {
alt((
"0",
(
Self::match_base_10_leading_digit,
Self::match_base_10_trailing_digits,
)
.take(),
))
.parse_next(self)
}
fn match_base_10_leading_digit(&mut self) -> IonMatchResult<'top> {
one_of(b"123456789").take().parse_next(self)
}
fn match_base_10_trailing_digits(&mut self) -> IonMatchResult<'top> {
zero_or_more(alt((
("_", one_of(|b: u8| b.is_ascii_digit())).take(),
digit1,
)))
.parse_next(self)
}
fn match_base_16_int(&mut self) -> IonParseResult<'top, MatchedInt> {
let initial_offset = self.offset();
separated_pair(
opt("-"),
alt(("0x", "0X")),
Self::match_base_16_int_trailing_digits,
)
.map(|(maybe_sign, digits)| {
MatchedInt::new(16, maybe_sign.is_some(), digits.offset() - initial_offset)
})
.parse_next(self)
}
fn match_base_16_int_trailing_digits(&mut self) -> IonMatchResult<'top> {
terminated(
zero_or_more((Self::take_base_16_digits1, "_")),
Self::take_base_16_digits1,
)
.take()
.parse_next(self)
}
fn take_base_16_digits1(&mut self) -> IonMatchResult<'top> {
(
one_of(|b: u8| b.is_ascii_hexdigit()),
take_while(.., |b: u8| b.is_ascii_hexdigit()),
)
.take()
.parse_next(self)
}
pub(crate) fn match_n_hex_digits(
count: usize,
) -> impl Parser<TextBuffer<'top>, TextBuffer<'top>, IonParseError<'top>> {
n_times(count, one_of(|b: u8| b.is_ascii_hexdigit())).take()
}
fn match_float(&mut self) -> IonParseResult<'top, MatchedFloat> {
terminated(
alt((Self::match_float_special, Self::match_float_numeric_value)),
Self::peek_stop_character,
)
.parse_next(self)
}
fn match_float_special(&mut self) -> IonParseResult<'top, MatchedFloat> {
alt((
"nan".value(MatchedFloat::NotANumber),
"+inf".value(MatchedFloat::PositiveInfinity),
"-inf".value(MatchedFloat::NegativeInfinity),
))
.parse_next(self)
}
fn match_float_numeric_value(&mut self) -> IonParseResult<'top, MatchedFloat> {
(
Self::match_number_with_optional_dot_and_digits,
Self::match_float_exponent_marker_and_digits,
)
.take()
.value(MatchedFloat::Numeric)
.parse_next(self)
}
fn match_number_with_optional_dot_and_digits(&mut self) -> IonMatchResult<'top> {
(
opt("-"),
Self::match_base_10_digits_before_dot,
opt(Self::match_dot_followed_by_base_10_digits),
)
.take()
.parse_next(self)
}
fn match_digits_before_dot(&mut self) -> IonMatchResult<'top> {
alt((
"0",
(Self::match_leading_digit, Self::match_trailing_digits).take(),
))
.parse_next(self)
}
fn match_leading_digit(&mut self) -> IonMatchResult<'top> {
one_of(b"123456789").take().parse_next(self)
}
fn match_trailing_digits(&mut self) -> IonMatchResult<'top> {
zero_or_more(preceded(opt("_"), digit1)).parse_next(self)
}
fn match_dot_followed_by_base_10_digits(&mut self) -> IonMatchResult<'top> {
(".", opt(Self::match_zero_or_more_digits_after_dot))
.take()
.parse_next(self)
}
fn match_one_or_more_digits_after_dot(&mut self) -> IonMatchResult<'top> {
(
zero_or_more((digit1, "_")),
(one_of(|b: u8| b.is_ascii_digit()), digit0),
)
.take()
.parse_next(self)
}
fn match_zero_or_more_digits_after_dot(&mut self) -> IonMatchResult<'top> {
terminated(
zero_or_more((
digit1,
terminated(
"_",
peek(one_of(|b: u8| b.is_ascii_digit())),
),
)),
digit1,
)
.take()
.parse_next(self)
}
fn match_float_exponent_marker_and_digits(&mut self) -> IonMatchResult<'top> {
preceded(
one_of(b"eE"),
Self::match_exponent_sign_and_one_or_more_digits.take(),
)
.parse_next(self)
}
fn match_exponent_sign_and_one_or_more_digits(&mut self) -> IonParseResult<'top, (bool, Self)> {
(
opt(Self::match_any_sign).map(|s| s == Some(b'-')),
Self::match_one_or_more_digits_after_dot,
)
.parse_next(self)
}
pub fn match_any_sign(&mut self) -> IonParseResult<'top, std::primitive::u8> {
one_of(b"-+").parse_next(self)
}
pub fn match_decimal_exponent(&mut self) -> IonParseResult<'top, (bool, TextBuffer<'top>)> {
preceded(
one_of(b"dD"),
Self::match_exponent_sign_and_one_or_more_digits,
)
.parse_next(self)
}
pub fn match_decimal(&mut self) -> IonParseResult<'top, MatchedDecimal> {
let initial_offset = self.offset();
terminated(
(
opt("-"),
Self::match_digits_before_dot,
alt((
(
".",
opt(Self::match_zero_or_more_digits_after_dot),
opt(Self::match_decimal_exponent),
)
.map(
|(dot, maybe_digits_after_dot, maybe_exponent)| {
let digits_after_dot = match maybe_digits_after_dot {
Some(digits) => digits,
None => dot.slice(1, 0),
};
let (exp_is_negative, exp_digits) = match maybe_exponent {
Some(exponent) => exponent,
None => {
(false, digits_after_dot.slice(digits_after_dot.len(), 0))
}
};
(digits_after_dot, exp_is_negative, exp_digits)
},
),
Self::match_decimal_exponent.with_taken().map(
|((exp_is_negative, exp_digits), matched)| {
let digits_after_dot = matched.slice(0, 0);
(digits_after_dot, exp_is_negative, exp_digits)
},
),
)),
),
Self::peek_stop_character,
)
.map(
|(maybe_sign, leading_digits, (digits_after_dot, exponent_is_negative, exp_digits))| {
let is_negative = maybe_sign.is_some();
let digits_offset = (leading_digits.offset() - initial_offset) as u16;
let digits_length = match digits_after_dot.len() {
0 => leading_digits.len() as u16,
trailing_digits_length => {
(leading_digits.len() + 1 + trailing_digits_length) as u16
}
};
let num_trailing_digits = digits_after_dot
.bytes()
.iter()
.filter(|b| b.is_ascii_digit())
.count() as u16;
let exponent_digits_offset = (exp_digits.offset() - initial_offset) as u16;
let exponent_digits_length = exp_digits.len() as u16;
MatchedDecimal::new(
is_negative,
digits_offset,
digits_length,
num_trailing_digits,
exponent_is_negative,
exponent_digits_offset,
exponent_digits_length,
)
},
)
.parse_next(self)
}
pub fn match_string(&mut self) -> IonParseResult<'top, MatchedString> {
alt((Self::match_short_string, Self::match_long_string)).parse_next(self)
}
pub(crate) fn match_short_string(&mut self) -> IonParseResult<'top, MatchedString> {
delimited("\"", Self::match_short_string_body, "\"")
.map(|(_matched, contains_escaped_chars)| {
if contains_escaped_chars {
MatchedString::ShortWithEscapes
} else {
MatchedString::ShortWithoutEscapes
}
})
.parse_next(self)
}
pub(crate) fn match_short_string_body(&mut self) -> IonParseResult<'top, (Self, bool)> {
Self::match_text_until_unescaped(self, b'\"', false)
}
pub fn match_long_string(&mut self) -> IonParseResult<'top, MatchedString> {
Self::match_long_string_segments.parse_next(self)
}
pub(crate) fn match_long_string_segments(&mut self) -> IonParseResult<'top, MatchedString> {
struct Stats(usize, bool);
impl Accumulate<bool> for Stats {
fn initial(_capacity: Option<usize>) -> Self {
Stats(0, false)
}
fn accumulate(&mut self, acc: bool) {
self.0 += 1;
self.1 |= acc;
}
}
repeat(1.., |input: &mut TextBuffer<'top>| {
let (_segment, found_escape) =
whitespace_and_then(Self::match_long_string_segment).parse_next(input)?;
Ok(found_escape)
})
.map(move |stats: Stats| match stats {
Stats(1, false) => MatchedString::LongSingleSegmentWithoutEscapes,
Stats(1, true) => MatchedString::LongSingleSegmentWithEscapes,
_ => MatchedString::Long,
})
.parse_next(self)
}
pub fn match_long_string_segment(&mut self) -> IonParseResult<'top, (Self, bool)> {
delimited("'''", Self::match_long_string_segment_body, "'''").parse_next(self)
}
fn match_long_string_segment_body(&mut self) -> IonParseResult<'top, (Self, bool)> {
Self::match_text_until_unescaped_str(self, "'''")
}
pub(crate) fn match_operator<E: TextEncoding>(
&mut self,
) -> IonParseResult<'top, LazyRawTextValue<'top, E>> {
one_or_more(one_of(b"!#%&*+-./;<=>?@^`|~"))
.map(|text: TextBuffer<'_>| LazyRawTextValue {
input: text,
encoded_value: EncodedTextValue::new(MatchedValue::Symbol(MatchedSymbol::Operator)),
})
.parse_next(self)
}
fn match_symbol(&mut self) -> IonParseResult<'top, MatchedSymbol> {
alt((
Self::match_symbol_id,
Self::match_identifier,
Self::match_quoted_symbol,
))
.parse_next(self)
}
fn match_symbol_id(&mut self) -> IonParseResult<'top, MatchedSymbol> {
("$", Self::match_address)
.value(MatchedSymbol::SymbolId)
.parse_next(self)
}
fn match_address(&mut self) -> IonParseResult<'top, usize> {
let initial_offset = self.offset();
terminated(digit1, not("_"))
.map(|buffer: TextBuffer<'_>| {
usize::from_str(buffer.as_utf8(initial_offset).unwrap()).unwrap()
})
.parse_next(self)
}
pub(crate) fn match_keyword(&mut self) -> IonMatchResult<'top> {
terminated(
alt(("true", "false", "null", "nan")),
Self::identifier_terminator,
)
.parse_next(self)
}
pub(crate) fn match_identifier(&mut self) -> IonParseResult<'top, MatchedSymbol> {
(
not(Self::match_keyword),
Self::identifier_initial_character,
Self::identifier_trailing_characters,
Self::identifier_terminator,
)
.value(MatchedSymbol::Identifier)
.parse_next(self)
}
fn identifier_terminator(&mut self) -> IonMatchResult<'top> {
not(Self::identifier_trailing_character)
.take()
.parse_next(self)
}
fn identifier_initial_character(&mut self) -> IonParseResult<'top, Self> {
alt((one_of(b"$_"), one_of(|b: u8| b.is_ascii_alphabetic())))
.take()
.parse_next(self)
}
fn identifier_trailing_character(&mut self) -> IonParseResult<'top, Self> {
alt((one_of(b"$_"), one_of(|c: u8| c.is_ascii_alphanumeric())))
.take()
.parse_next(self)
}
fn identifier_trailing_characters(&mut self) -> IonParseResult<'top, Self> {
zero_or_more(one_of(|b: u8| {
b.is_ascii_alphanumeric() || b"$_".contains(&b)
}))
.parse_next(self)
}
fn match_quoted_symbol(&mut self) -> IonParseResult<'top, MatchedSymbol> {
delimited("'", Self::match_quoted_symbol_body, "'")
.map(|(_matched, contains_escaped_chars)| {
if contains_escaped_chars {
MatchedSymbol::QuotedWithEscapes
} else {
MatchedSymbol::QuotedWithoutEscapes
}
})
.parse_next(self)
}
fn match_quoted_symbol_body(&mut self) -> IonParseResult<'top, (Self, bool)> {
Self::match_text_until_unescaped(self, b'\'', false)
}
fn match_text_until_unescaped(
&mut self,
delimiter: u8,
allow_unescaped_newlines: bool,
) -> IonParseResult<'top, (Self, bool)> {
let mut contains_escaped_chars = false;
let mut iter = self.bytes().iter().copied().enumerate();
while let Some((index, byte)) = iter.next() {
if byte == b'\\' {
contains_escaped_chars = true;
let next_two_bytes = self.bytes().get(index + 1..index + 3);
let bytes_to_skip = if next_two_bytes == Some(b"\r\n") {
2
} else {
1
};
let _ = iter.nth(bytes_to_skip - 1);
continue;
}
if byte == delimiter {
let matched = self.slice(0, index);
self.consume(index);
return Ok((matched, contains_escaped_chars));
}
if byte < 0x20 {
if byte == b'\r' {
contains_escaped_chars = true;
} else {
self.validate_string_control_character(byte, index, allow_unescaped_newlines)?;
}
}
}
self.incomplete("reading a text value without closing delimiter")
}
#[cold]
fn validate_string_control_character(
&mut self,
byte: u8,
index: usize,
allow_unescaped_newlines: bool,
) -> IonParseResult<'top, ()> {
if byte == b'\n' && !allow_unescaped_newlines {
return self
.slice_to_end(index)
.invalid("unescaped newlines are not allowed in short string literals")
.context("reading a string")
.cut();
}
if !WHITESPACE_BYTES.contains(&byte) {
return self
.slice_to_end(index)
.invalid(format!("unescaped control characters are not allowed in text literals; byte {byte:02X}"))
.context("reading a string")
.cut();
}
Ok(())
}
fn match_text_until_unescaped_str(
&mut self,
delimiter: &str,
) -> IonParseResult<'top, (Self, bool)> {
let delimiter_head = delimiter.as_bytes()[0];
let mut contained_escapes = false;
let mut remaining = *self;
loop {
#[allow(unused_variables)]
let (matched_input_buffer, segment_contained_escapes) =
remaining.match_text_until_unescaped(delimiter_head, true)?;
contained_escapes |= segment_contained_escapes;
if remaining.bytes().starts_with(delimiter.as_bytes()) {
let relative_match_end = remaining.offset() - self.offset();
let matched_input = self.slice(0, relative_match_end);
self.consume(relative_match_end);
return Ok((matched_input, contained_escapes));
} else {
remaining.consume(1);
}
}
}
fn match_any_digit(&mut self) -> IonParseResult<'top, std::primitive::u8> {
one_of(|b: u8| b.is_ascii_digit()).parse_next(self)
}
#[inline]
pub fn match_timestamp(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
#[inline(never)]
pub fn full_match_timestamp<'t>(
input: &mut TextBuffer<'t>,
) -> IonParseResult<'t, MatchedTimestamp> {
alt((
TextBuffer::match_timestamp_y,
TextBuffer::match_timestamp_ym,
TextBuffer::match_timestamp_ymd,
TextBuffer::match_timestamp_ymd_hm,
TextBuffer::match_timestamp_ymd_hms,
TextBuffer::match_timestamp_ymd_hms_fractional,
))
.parse_next(input)
}
match self.bytes().first() {
Some(byte) if byte.is_ascii_digit() => full_match_timestamp(self),
Some(_) => self.unrecognized().backtrack(),
None => self.incomplete("a timestamp"),
}
}
fn match_timestamp_y(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
terminated(Self::match_timestamp_year, ("T", Self::peek_stop_character))
.map(|_year| MatchedTimestamp::new(TimestampPrecision::Year))
.parse_next(self)
}
fn match_timestamp_ym(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
terminated(
(Self::match_timestamp_year, Self::match_timestamp_month),
("T", Self::peek_stop_character),
)
.map(|(_year, _month)| MatchedTimestamp::new(TimestampPrecision::Month))
.parse_next(self)
}
fn match_timestamp_ymd(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
terminated(
(
Self::match_timestamp_year,
Self::match_timestamp_month,
Self::match_timestamp_day,
),
(opt("T"), Self::peek_stop_character),
)
.map(|_| MatchedTimestamp::new(TimestampPrecision::Day))
.parse_next(self)
}
fn match_timestamp_ymd_hm(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
terminated(
(
Self::match_timestamp_year,
Self::match_timestamp_month,
Self::match_timestamp_day,
Self::match_timestamp_hour_and_minute,
Self::match_timestamp_offset,
),
Self::peek_stop_character,
)
.map(|(_y, _m, _d, _hm, offset)| {
MatchedTimestamp::new(TimestampPrecision::HourAndMinute).with_offset(offset)
})
.parse_next(self)
}
fn match_timestamp_ymd_hms(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
terminated(
(
Self::match_timestamp_year,
Self::match_timestamp_month,
Self::match_timestamp_day,
Self::match_timestamp_hour_and_minute,
Self::match_timestamp_seconds,
Self::match_timestamp_offset,
),
Self::peek_stop_character,
)
.map(|(_y, _m, _d, _hm, _s, offset)| {
MatchedTimestamp::new(TimestampPrecision::Second).with_offset(offset)
})
.parse_next(self)
}
fn match_timestamp_ymd_hms_fractional(&mut self) -> IonParseResult<'top, MatchedTimestamp> {
terminated(
(
Self::match_timestamp_year,
Self::match_timestamp_month,
Self::match_timestamp_day,
Self::match_timestamp_hour_and_minute,
Self::match_timestamp_seconds,
Self::match_timestamp_fractional_seconds,
Self::match_timestamp_offset,
),
Self::peek_stop_character,
)
.map(|(_y, _m, _d, _hm, _s, _f, offset)| {
MatchedTimestamp::new(TimestampPrecision::Second).with_offset(offset)
})
.parse_next(self)
}
fn match_timestamp_year(&mut self) -> IonMatchResult<'top> {
n_times(4, one_of(|c: u8| c.is_ascii_digit())).parse_next(self)
}
fn match_timestamp_month(&mut self) -> IonMatchResult<'top> {
preceded(
"-",
alt((("0", one_of(b"123456789")), ("1", one_of(b"012")))).take(),
)
.parse_next(self)
}
fn match_timestamp_day(&mut self) -> IonMatchResult<'top> {
preceded(
"-",
alt((
(b"0", one_of(b"123456789")),
(one_of(b"12".as_slice()).take(), Self::match_any_digit),
(b"3", one_of(b"01")),
))
.take(),
)
.parse_next(self)
}
fn match_timestamp_hour_and_minute(
&mut self,
) -> IonParseResult<'top, (TextBuffer<'top>, TextBuffer<'top>)> {
preceded(
"T",
separated_pair(
alt((
(one_of(b"01").take(), Self::match_any_digit),
("2", one_of(b"0123")),
))
.take(),
":",
(one_of(b"012345"), Self::match_any_digit).take(),
),
)
.parse_next(self)
}
fn match_timestamp_seconds(&mut self) -> IonMatchResult<'top> {
preceded(":", (one_of(b"012345"), Self::match_any_digit).take()).parse_next(self)
}
fn match_timestamp_fractional_seconds(&mut self) -> IonMatchResult<'top> {
preceded(".", digit1).parse_next(self)
}
fn match_timestamp_offset(&mut self) -> IonParseResult<'top, MatchedTimestampOffset> {
alt((
"Z".value(MatchedTimestampOffset::Zulu),
"+00:00".value(MatchedTimestampOffset::Zulu),
"-00:00".value(MatchedTimestampOffset::Unknown),
(
one_of(b"-+"),
Self::match_timestamp_offset_hours_and_minutes,
)
.map(|(sign, (_hours, _minutes))| {
if sign == b'-' {
MatchedTimestampOffset::NegativeHoursAndMinutes
} else {
MatchedTimestampOffset::PositiveHoursAndMinutes
}
}),
))
.parse_next(self)
}
fn match_timestamp_offset_hours_and_minutes(&mut self) -> IonParseResult<'top, (Self, Self)> {
separated_pair(
alt((
(one_of(b"01").take(), Self::match_any_digit),
("2", one_of(b"0123")),
))
.take(),
":",
(one_of(b"012345"), Self::match_any_digit).take(),
)
.parse_next(self)
}
pub fn match_blob(&mut self) -> IonParseResult<'top, MatchedBlob> {
let initial_offset = self.offset();
delimited(
"{{",
Self::match_base64_content,
(Self::match_whitespace0, "}}"),
)
.map(|base64_data| {
MatchedBlob::new(base64_data.offset() - initial_offset, base64_data.len())
})
.parse_next(self)
}
pub fn match_clob(&mut self) -> IonParseResult<'top, MatchedClob> {
delimited(
"{{",
preceded(
Self::match_whitespace0,
alt((
Self::match_short_clob_body.value(MatchedClob::Short),
Self::match_long_clob_body.value(MatchedClob::Long),
)),
),
preceded(Self::match_whitespace0, "}}"),
)
.parse_next(self)
}
fn match_short_clob_body(&mut self) -> IonMatchResult<'top> {
let (_matched_string, body) = Self::match_short_string.with_taken().parse_next(self)?;
body.validate_clob_text()?;
Ok(body)
}
fn match_long_clob_body(&mut self) -> IonMatchResult<'top> {
one_or_more(preceded(
Self::match_whitespace0,
Self::match_long_clob_body_segment,
))
.take()
.parse_next(self)
}
fn match_long_clob_body_segment(&mut self) -> IonMatchResult<'top> {
let (_matched_string, body) = Self::match_long_string_segment
.with_taken()
.parse_next(self)?;
body.validate_clob_text()?;
Ok(body)
}
fn validate_clob_text(&self) -> IonParseResult<'top, ()> {
for byte in self.bytes().iter().copied() {
if !Self::byte_is_legal_clob_ascii(byte) {
let message = format!("found an illegal byte '{byte:0x}' in clob");
return self.invalid(message).context("reading a clob").cut();
}
}
Ok(())
}
fn byte_is_legal_clob_ascii(b: u8) -> bool {
b.is_ascii() && (u32::from(b) >= 0x20 || WHITESPACE_BYTES.contains(&b))
}
fn match_base64_content(&mut self) -> IonMatchResult<'top> {
(
zero_or_more((
Self::match_whitespace0,
alt((alphanumeric1, one_of(b"+/").take())),
)),
opt(preceded(Self::match_whitespace0, alt(("==", "=")))),
)
.take()
.parse_next(self)
}
pub fn is_final_data(&self) -> bool {
self.is_final_data
}
}
pub trait IonParser<'top, O>: Parser<TextBuffer<'top>, O, IonParseError<'top>> {
}
impl<'top, Output, P> IonParser<'top, Output> for P where
P: Parser<TextBuffer<'top>, Output, IonParseError<'top>>
{
}
impl SliceLen for TextBuffer<'_> {
fn slice_len(&self) -> usize {
self.len()
}
}
impl<'data> Stream for TextBuffer<'data> {
type Token = u8;
type Slice = Self;
type IterOffsets = <&'data [u8] as Stream>::IterOffsets;
type Checkpoint = Self;
fn iter_offsets(&self) -> Self::IterOffsets {
self.bytes().iter_offsets()
}
fn eof_offset(&self) -> usize {
self.bytes().eof_offset()
}
fn next_token(&mut self) -> Option<Self::Token> {
let byte = *self.bytes().first()?;
self.consume(1);
Some(byte)
}
fn offset_for<P>(&self, predicate: P) -> Option<usize>
where
P: Fn(Self::Token) -> bool,
{
self.bytes().offset_for(predicate)
}
fn offset_at(&self, tokens: usize) -> Result<usize, Needed> {
self.bytes().offset_at(tokens)
}
fn next_slice(&mut self, offset: usize) -> Self::Slice {
let head = self.slice(0, offset);
self.consume(offset);
head
}
fn checkpoint(&self) -> Self::Checkpoint {
*self
}
fn reset(&mut self, checkpoint: &Self::Checkpoint) {
*self = *checkpoint;
}
fn raw(&self) -> &dyn Debug {
&self.input_span
}
}
impl StreamIsPartial for TextBuffer<'_> {
type PartialState = ();
fn complete(&mut self) -> Self::PartialState {}
fn restore_partial(&mut self, _state: Self::PartialState) {
}
fn is_partial_supported() -> bool {
true
}
fn is_partial(&self) -> bool {
!self.is_final_data
}
}
impl<'a> winnow::stream::Compare<&'a str> for TextBuffer<'_> {
fn compare(&self, t: &'a str) -> CompareResult {
self.bytes().compare(t.as_bytes())
}
}
impl<'a> winnow::stream::Compare<&'a [u8]> for TextBuffer<'_> {
fn compare(&self, t: &'a [u8]) -> CompareResult {
self.bytes().compare(t)
}
}
impl<'a, const N: usize> winnow::stream::Compare<&'a [u8; N]> for TextBuffer<'_> {
fn compare(&self, t: &'a [u8; N]) -> CompareResult {
self.bytes().compare(t.as_slice())
}
}
impl winnow::stream::Offset for TextBuffer<'_> {
fn offset_from(&self, start: &Self) -> usize {
self.offset() - start.offset()
}
}
impl FindSlice<&str> for TextBuffer<'_> {
fn find_slice(&self, substr: &str) -> Option<Range<usize>> {
self.bytes().find_slice(substr)
}
}
impl Location for TextBuffer<'_> {
fn location(&self) -> usize {
self.offset()
}
}
pub fn whitespace_and_then<'data, P, Output>(
parser: P,
) -> impl Parser<TextBuffer<'data>, Output, IonParseError<'data>>
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
preceded(TextBuffer::match_optional_comments_and_whitespace, parser)
}
pub fn zero_or_more<'data, P, Output>(
parser: P,
) -> impl Parser<TextBuffer<'data>, TextBuffer<'data>, IonParseError<'data>>
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
repeat::<_, _, (), _, _>(.., parser).take()
}
pub fn one_or_more<'data, P, Output>(
parser: P,
) -> impl Parser<TextBuffer<'data>, TextBuffer<'data>, IonParseError<'data>>
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
repeat::<_, _, (), _, _>(1.., parser).take()
}
pub fn n_times<'data, P, Output>(
n: usize,
parser: P,
) -> impl Parser<TextBuffer<'data>, TextBuffer<'data>, IonParseError<'data>>
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
repeat::<_, _, (), _, _>(n, parser).take()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lazy::any_encoding::IonVersion;
use crate::lazy::expanded::compiler::TemplateCompiler;
use crate::lazy::expanded::template::{ParameterCardinality, ParameterEncoding};
use crate::lazy::expanded::EncodingContext;
use crate::{AnyEncoding, Reader};
use rstest::rstest;
fn match_length<'data, P, Output>(
parser: P,
) -> impl Parser<TextBuffer<'data>, usize, IonParseError<'data>>
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
parser
.with_span()
.map(|(_output, match_range)| match_range.len())
}
struct MatchTest {
input: String,
context: EncodingContext,
}
impl MatchTest {
fn new(input: &str) -> Self {
MatchTest {
input: input.to_string(),
context: EncodingContext::for_ion_version(IonVersion::v1_1),
}
}
fn new_1_0(input: &str) -> Self {
MatchTest {
input: input.to_string(),
context: EncodingContext::for_ion_version(IonVersion::v1_0),
}
}
fn register_macro(&mut self, text: &str) -> &mut Self {
let new_macro =
TemplateCompiler::compile_from_source(self.context.macro_table(), text).unwrap();
self.context
.macro_table_mut()
.add_template_macro(new_macro)
.unwrap();
self
}
fn try_match<'data, P, Output>(
&'data self,
parser: P,
) -> IonParseResult<'data, (TextBuffer<'data>, usize)>
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
let mut buffer = TextBuffer::new(self.context.get_ref(), self.input.as_bytes());
let matched_length = match_length(parser).parse_next(&mut buffer)?;
Ok((buffer, matched_length))
}
fn expect_match<'data, P, Output>(&'data self, parser: P)
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
let result = self.try_match(parser).unwrap_or_else(|e| {
panic!("Unexpected parse fail for input <{}>\n{e}", self.input)
});
let match_length = result.1;
assert_eq!(
match_length,
self.input.len(),
"\nInput: '{}'\nMatched: '{}'\n",
self.input,
&self.input[..match_length]
);
}
fn expect_mismatch<'data, P, Output>(&'data self, parser: P)
where
P: Parser<TextBuffer<'data>, Output, IonParseError<'data>>,
{
let result = self.try_match(parser);
match result {
Ok((_, match_length)) => {
assert_ne!(
match_length,
self.input.len(),
"parser unexpectedly matched the complete input: {:?}\nResult: {:?}",
self.input,
result
);
}
Err(e) if e.is_incomplete() => {
panic!(
"parser reported an incomplete match rather than a mismatch: {}",
self.input
)
}
Err(_) => {}
}
}
}
macro_rules! matcher_tests {
($parser:ident $($expect:ident: [$($input:literal),+$(,)?]),+$(,)?) => {
matcher_tests!($parser => TextBuffer::$parser, $($expect: [$($input),+]),+,);
};
($mod_name:ident => $parser:expr, $($expect:ident: [$($input:literal),+$(,)?]),+$(,)?) => {
mod $mod_name {
use super::*;
$(
#[test]
fn $expect() {
$(MatchTest::new_1_0($input.trim()).$expect(match_length($parser) );)
+
}
)+
}
}
}
macro_rules! matcher_tests_with_macro {
($mod_name:ident => $parser:expr, $macro_src:literal $($expect:ident: [$($input:literal),+$(,)?]),+$(,)?) => {
mod $mod_name {
use super::*;
$(
#[test]
fn $expect() {
$(MatchTest::new($input.trim()).register_macro($macro_src).$expect(match_length($parser));)
+
}
)+
}
};
}
#[test]
fn test_match_stop_char() {
MatchTest::new(" ").expect_match(match_length(TextBuffer::match_stop_character));
MatchTest::new("").expect_match(match_length(TextBuffer::match_stop_character));
}
#[test]
fn test_match_ivm() {
fn match_ivm(input: &str) {
MatchTest::new(input)
.expect_match(match_length(TextBuffer::match_ivm::<TextEncoding_1_0>));
}
fn mismatch_ivm(input: &str) {
MatchTest::new(input)
.expect_mismatch(match_length(TextBuffer::match_ivm::<TextEncoding_1_1>));
}
match_ivm("$ion_1_0");
match_ivm("$ion_1_1");
match_ivm("$ion_1_2");
match_ivm("$ion_124_17");
mismatch_ivm("ion_1_0");
mismatch_ivm("$ion__1_0");
mismatch_ivm("$ion_1_0_0");
mismatch_ivm("$$ion_1_0");
mismatch_ivm("$ion_FF_FF");
}
matcher_tests! {
match_bool
expect_match: [
"true",
"false"
],
expect_mismatch: [
"True",
"tru",
"TRUE",
"False",
"FALSE",
"fals",
"potato",
"42"
],
}
matcher_tests! {
match_null
expect_match:[
"null",
"null.null",
"null.bool",
"null.int",
"null.float",
"null.decimal",
"null.timestamp",
"null.symbol",
"null.string",
"null.clob",
"null.blob",
"null.list",
"null.sexp",
"null.struct",
],
expect_mismatch: [
"-1",
"null.hello",
"nullnull",
"nullify",
"null..int",
"string.null",
"null.timestam",
"null.strin",
"null.nul"
],
}
matcher_tests! {
match_int
expect_match: [
"0b0",
"0B0",
"0b1",
"0B1",
"0b0001",
"0B1111",
"0b1111_1111",
"0B1010_1010",
"0",
"13",
"942",
"7_216",
"1_000_000",
"9_999_999",
"0x0",
"0x20",
"0x0A",
"0xcafe",
"0xCAFE",
"0XcaFE",
"0xC_A_F_E",
"0Xca_FE",
],
expect_mismatch: [
"00", "0123", "--5", "+5", "1__000__000", "_123", "0x0x5", "0xx5", "0x", "0b", ],
}
matcher_tests! {
match_float
expect_match: [
"0.0e0", "0E0", "0e0", "305e1", "305e+1", "305e-1", "305e100", "305e-100", "305e+100",
"305.0e1", "0.279e3", "0.279e-3", "279e0", "279.5e0", "279.5E0",
],
expect_mismatch: [
"305", ".305e", "305e0.5", "305e-0.5", "0305e1", "+305e1", "--305e1", "305e", ]
}
matcher_tests! {
match_timestamp
expect_match: [
"2023T",
"2023-08T",
"2023-08-13", "2023-08-13T",
"2023-08-13T14:18Z",
"2023-08-13T14:18+05:00",
"2023-08-13T14:18-05:00",
"2023-08-13T14:18:35-05:00",
"2023-08-13T14:18:35.994-05:00",
],
expect_mismatch: [
"20233T", "2023-13T", "2023-08-41T", "2023-08+18T", "2023-08-18T25:00Z", "2023-08-18T14:62", "2023-08-18T14:35:61", "2023-08-18T14:35:52.Z", "2023-08-18T14:35:52.000+24:30", "2023-08-18T14:35:52.000+00:60", "2023", "2023-08", "2023-08-18T14:00", ]
}
matcher_tests! {
match_string
expect_match: [
r#"
"hello"
"#,
r#"
"😀😀😀"
"#,
r#"
"this has an escaped quote \" right in the middle"
"#,
r#" '''hi''' "#,
r#"
'''foo'''
'''bar'''
'''baz'''
"#,
r#"
'''hello,''' /*comment*/ ''' world!'''
"#,
r#"
''''''
"#, r#"
'''''' ''''''
"#, ],
expect_mismatch: [
r#"
hello"
"#,
r#"
"hello
"#,
r#"
"hello\"
"#,
],
}
matcher_tests! {
match_symbol
expect_match: [
"''",
"'hello'",
"'😀😀😀'",
"'this has an escaped quote \\' right in the middle'",
"$308",
"$0",
"foo",
"name",
"$bar",
"_baz_quux",
],
expect_mismatch: [
"$-8", "'hello", "'hello\\'", ],
}
matcher_tests! {
match_annotated_value => TextBuffer::match_annotated_value::<TextEncoding_1_1>,
expect_match: [
"foo::5",
"foo::bar::5",
"foo :: 5",
"foo::bar::baz::5",
"foo :: /*comment*/ bar /*comment*/ :: baz :: 5",
"foo::bar::baz::quux::quuz::5",
"foo::'bar'::baz::$10::5",
],
expect_mismatch: ["foo::", "foo:bar", "foo:::bar"],
}
matcher_tests! {
match_decimal
expect_match: [
"5.", "-5.", "5.0", "-5.0", "5d0", "5.d0", "5.0d0", "-5.0d0", "5.0D0", "-5.0D0",
"5.0d+1", "-5.0d-1",
],
expect_mismatch: [
"123._456", "5", "05d", "-5.0+0",
"5d",
"-5d",
"5.d",
"-5.d",
"5.D",
"-5.D",
"5.1d", "-5.1d",
"5.1D", "-5.1D",
]
}
matcher_tests! {
match_sexp_1_0 => TextEncoding_1_0::sexp_matcher(),
expect_match: [
"()",
"(1)",
"(1 2)",
"(a)",
"(a b)",
"(a++)",
"(++a)",
"(a+=b)",
"(())",
"((()))",
"(1 (2 (3 4) 5) 6)",
],
expect_mismatch: ["foo", "1", "(", "(1 2 (3 4 5)"]
}
matcher_tests_with_macro! {
parsing_sexps => TextEncoding_1_1::sexp_matcher(),
"(macro foo (x*) null)"
expect_match: [
"()",
"(1)",
"(1 2)",
"(a)",
"(a b)",
"(a++)",
"(++a)",
"(a+=b)",
"(())",
"((()))",
"(1 (2 (3 4) 5) 6)",
"(1 (:foo 2 3))",
],
expect_mismatch: ["foo", "1", "(", "(1 2 (3 4 5)"]
}
matcher_tests! {
match_list_1_0 => TextEncoding_1_0::list_matcher(),
expect_match: [
"[]", "[1]", "[1, 2]", "[[]]", "[([])]",
],
expect_mismatch: [
"foo",
"1",
"[",
"[1, 2, [3, 4]",
]
}
matcher_tests_with_macro! {
match_list_1_1 => TextEncoding_1_1::list_matcher(),
"(macro foo (x*) null)"
expect_match: [
"[]", "[1]", "[1, 2]", "[[]]", "[([])]", "[1, (:foo 2 3)]"
],
expect_mismatch: [
"foo", "1",
"[", "[1, 2, [3, 4]"
]
}
matcher_tests! {
match_struct_1_0 => TextEncoding_1_0::struct_matcher(),
expect_match: [
"{}",
"{$0:$0}",
"{'':''}",
r#"{"":""}"#,
"{foo:bar}",
"{foo: bar, baz: quux}",
"{'foo': bar, 'baz': quux}",
r#"{foo: bar, "baz": quux}"#,
r#"{'foo': bar, "baz": quux}"#,
"{_:_}",
"{foo: [1, 2, 3]}",
"{foo: foo, foo: foo}",
"{foo: foo::foo::foo, foo: foo::foo}"
],
expect_mismatch: [
"{", "{foo: bar",
"{1: bar}",
"{foo: bar baz: quux}",
"{foo: bar,, baz: quux}",
"{foo:: bar, baz: quux}",
"{, foo: bar, baz: quux}",
"{,}"
]
}
matcher_tests_with_macro! {
match_struct_1_1 => TextEncoding_1_1::struct_matcher(),
"(macro foo (x*) {quux: quuz})"
expect_match: [
"{}", "{$0:$0}", "{'':''}", r#"{"":""}"#, "{foo:bar}",
"{foo: bar, baz: quux}", "{'foo': bar, 'baz': quux}",
r#"{foo: bar, "baz": quux}"#, r#"{'foo': bar, "baz": quux}"#,
"{_:_}", "{foo: [1, 2, 3]}", "{foo: foo, foo: foo}",
"{a: (:foo 1 2 3)}",
"{(:foo)}",
"{ (:foo)}",
"{(:foo) }",
"{(:foo), (:foo)}",
"{ (:foo) , (:foo) }",
"{ a : (:foo 1 2 3) , b : (:foo 4 5 6) }",
"{a:(:foo 1 2 3),b:(:foo 4 5 6)}", "{(:foo), (:foo)}",
"{a: (:foo 1 2 3), b: (:foo 4 5 6)}"
],
expect_mismatch: [
"{", "{foo: bar",
"{1: bar}",
"{foo: bar baz: quux}",
"{foo: bar,, baz: quux}",
"{foo:: bar, baz: quux}",
"{, foo: bar, baz: quux}",
"{,}",
"{(:foo}",
"{(:foo]}",
"{[:foo}",
"{(foo)}",
"{(:foo): bar}",
"{bar: (:foo}",
"{bar: (:foo) baz: quux}",
]
}
matcher_tests_with_macro! {
parsing_eexps => TextBuffer::match_e_expression,
"(macro foo (x*) null)"
expect_match: [
"(:foo)",
"(:foo 1)",
"(:foo 1 2 3)",
"(:foo (1 2 3))",
"(:foo \"foo\")",
"(:foo foo)",
"(:5)",
"(:5 1)",
"(:5 1 2 3)",
"(:5 (1 2 3))",
"(:5 \"foo\")",
"(:5 foo)",
"(:005 foo)", ],
expect_mismatch: [
"foo", "(foo)", "(5", "(5)", "(:0x5)", "(:5_000)", "(:foo", "(:5" ]
}
matcher_tests_with_macro! {
allow_omitting_trailing_optionals => TextBuffer::match_e_expression,
"(macro foo (a b+ c? d*) null)"
expect_match: [
"(:foo 1 2)",
"(:foo 1 2 3)",
"(:foo 1 2 3 4)",
"(:foo 1 2 3 4 5 6)", "(:foo 1 2 3 (::))", "(:foo 1 2 (::) 4)",
"(:foo 1 2 (::) (::))",
],
expect_mismatch: [
"(:foo 1)",
"(:foo)",
]
}
#[rstest]
#[case::empty("(::)")]
#[case::empty_with_extra_spacing("(:: )")]
#[case::single_value("(:: 1)")]
#[case::multiple_values("(:: 1 2 3)")]
#[case::eexp("(::foo 1 2 3)")]
#[case::eexp_with_sexp("(::(foo 1 2 3))")]
#[case::eexp_with_mixed_values("(:: 1 2 3 {quux: [1, 2, 3]} 4 bar::5 baz::6)")]
fn match_eexp_arg_group(#[case] input: &str) {
let parameter = Parameter::new(
"x",
ParameterEncoding::Tagged,
ParameterCardinality::ZeroOrMore,
RestSyntaxPolicy::NotAllowed,
);
MatchTest::new(input)
.register_macro("(macro foo (x*) null)")
.expect_match(match_length(TextBuffer::parser_with_arg(
TextBuffer::match_explicit_arg_group,
¶meter,
)))
}
#[rstest]
#[case::simple_e_exp("(:foo)")]
#[case::e_exp_in_e_exp("(:foo (bar 1))")]
#[case::e_exp_in_list("[a, b, (:foo 1)]")]
#[case::e_exp_in_sexp("(a (:foo 1) c)")]
#[case::e_exp_in_struct_field("{a:(:foo)}")]
#[case::e_exp_in_struct_field_with_comma("{a:(:foo),}")]
#[case::e_exp_in_struct_field_with_comma_and_second_field("{a:(:foo), b:2}")]
#[case::e_exp_in_struct_field_with_space_before("{ a:(:foo)}")]
#[case::e_exp_in_struct_field_with_space_after("{a:(:foo) }")]
#[case::e_exp_in_list_in_struct_field("{ a: [(:foo)] }")]
#[case::e_exp_in_sexp_in_struct_field("{ a: ((:foo)) }")]
#[case::e_exp_in_sexp_in_list("[a, b, ((:foo 1))]")]
#[case::e_exp_in_sexp_in_sexp("(a ((:foo 1)) c)")]
#[case::e_exp_in_list_in_list("[a, b, [(:foo 1)]]")]
#[case::e_exp_in_list_in_sexp("(a [(:foo 1)] c)")]
fn test_match_macro_invocation_in_context(#[case] input: &str) {
MatchTest::new(input)
.register_macro("(macro foo (x*) null)")
.expect_match(match_length(TextBuffer::match_top_level_item_1_1));
}
matcher_tests! {
match_blob
expect_match: [
"{{}}",
"{{ }}",
"{{\n\t}}",
"{{aGVsbG8=}}",
"{{ aGVsbG8=}}",
"{{aGVsbG8= }}",
"{{\taGVsbG8=\n\n}}",
"{{aG Vs bG 8 =}}",
r#"{{
aG Vs
bG 8=
}}"#,
"{{aGVsbG8h}}",
"{{ aGVsbG8h}}",
"{{aGVsbG8h }}",
"{{ aGVsbG8h }}",
"{{cmF6emxlIGRhenpsZSByb290IGJlZXI=}}",
"{{\ncmF6emxlIGRhenpsZSByb290IGJlZXI=\r}}",
],
expect_mismatch: [
"{{$aGVsbG8=}}",
r#"{{
// Here's the data:
aGVsbG8=
}}"#,
"{{=aGVsbG8}}",
"{{aGVsbG8===}}",
"{{aGVsbG8h",
"{{aGVsbG8h}"
]
}
matcher_tests! {
match_clob
expect_match: [
r#"{{""}}"#,
r#"{{''''''}}"#,
r#"{{"foo"}}"#,
r#"{{ "foo"}}"#,
r#"{{ "foo" }}"#,
r#"{{"foo" }}"#,
r#"{{'''foo'''}}"#,
r#"{{"foobar"}}"#,
r#"{{'''foo''' '''bar'''}}"#,
r#"{{
'''foo'''
'''bar'''
'''baz'''
}}"#,
],
expect_mismatch: [
r#"{{foo}}"#, r#"{{'''foo''' /*hi!*/ '''bar'''}}"#, r#"{{'''foo''' "bar"}}"#, r#"{{"😎🙂🙃"}}"#, r#"{{"foo}}"#, r#"{{"foo"}"#, r#"{{'''foo'''}"#, ],
}
#[test]
fn test_match_text_until_unescaped_str() {
let empty_context = EncodingContext::empty();
let context = empty_context.get_ref();
let mut input = TextBuffer::new(context, r" foo bar \''' baz''' quux ".as_bytes());
let (matched, contains_escapes) = input.match_text_until_unescaped_str(r#"'''"#).unwrap();
assert_eq!(matched.as_text().unwrap(), " foo bar \\''' baz");
assert!(contains_escapes);
}
#[test]
fn expect_foo() {
MatchTest::new_1_0("\"hello\"").expect_match(match_length(TextBuffer::match_string));
}
#[test]
fn expect_long_foo() {
MatchTest::new_1_0("'''long hello'''").expect_match(match_length(TextBuffer::match_string));
}
#[test]
fn expect_bootstrap() -> IonResult<()> {
let mut reader = Reader::new(AnyEncoding, "()")?;
let value = reader.expect_next()?;
let _ = value.read()?.expect_sexp().unwrap();
Ok(())
}
#[test]
fn expect_clob() {
MatchTest::new_1_0(r#"{{''''''}}"#).expect_match(match_length(TextBuffer::match_clob));
}
}