use std::fmt;
use std::sync::Arc;
use compact_str::CompactString;
use super::instruction::{CompiledFormatOneArg, CompiledFormatPart, CompiledFormatTemplate};
use super::*;
pub(crate) async fn stringify_value_async(value: &Value) -> Result<String, RuntimeError> {
let mut output = String::new();
append_stringified_value_async(&mut output, value).await?;
Ok(output)
}
pub(crate) fn stringify_value(value: &Value) -> Result<String, RuntimeError> {
if value_contains_projected(value) {
futures_executor::block_on(stringify_value_async(value))
} else {
stringify_value_direct(value)
}
}
pub(crate) fn stringify_value_blocking(value: &Value) -> Result<String, RuntimeError> {
stringify_value(value)
}
pub(crate) fn stringify_value_direct(value: &Value) -> Result<String, RuntimeError> {
let mut output = String::new();
append_stringified_value_direct(&mut output, value)?;
Ok(output)
}
pub(crate) fn append_stringified_value_direct(
output: &mut String,
value: &Value,
) -> Result<(), RuntimeError> {
match value {
Value::String(value) => output.push_str(value),
Value::Null => output.push_str("null"),
Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }),
Value::Number(value) => {
write_number(output, *value).expect("string writes should not fail")
}
Value::Projected(_) => unreachable!("projected values require async stringification"),
Value::Image(_) | Value::Resource(_) | Value::List(_) | Value::Record(_) => {
append_direct_json(output, value)
}
}
Ok(())
}
pub(crate) fn append_stringified_value_async<'a>(
output: &'a mut String,
value: &'a Value,
) -> ProjectedFuture<'a, Result<(), RuntimeError>> {
Box::pin(async move {
match value {
Value::String(value) => output.push_str(value),
Value::Null => output.push_str("null"),
Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }),
Value::Number(value) => {
write_number(output, *value).expect("string writes should not fail")
}
Value::Projected(value) => output.push_str(&value.render().await),
Value::Image(_) | Value::Resource(_) | Value::List(_) | Value::Record(_) => {
append_runtime_json_async(output, value).await;
}
}
Ok(())
})
}
#[cfg(test)]
pub(crate) fn append_stringified_value(
output: &mut String,
value: &Value,
) -> Result<(), RuntimeError> {
futures_executor::block_on(append_stringified_value_async(output, value))
}
pub(crate) fn write_number(output: &mut impl fmt::Write, value: f64) -> fmt::Result {
if value.is_finite() && value.fract() == 0.0 {
let as_i64 = value as i64 as f64;
if as_i64 == value {
return write_i64(output, value as i64);
}
let as_u64 = value as u64 as f64;
if as_u64 == value {
return write_u64(output, value as u64);
}
}
write!(output, "{value}")
}
fn write_i64(output: &mut impl fmt::Write, value: i64) -> fmt::Result {
if value < 0 {
output.write_char('-')?;
return write_u64(output, value.unsigned_abs());
}
write_u64(output, value as u64)
}
fn write_u64(output: &mut impl fmt::Write, mut value: u64) -> fmt::Result {
let mut buffer = [0u8; 20];
let mut index = buffer.len();
loop {
index -= 1;
buffer[index] = b'0' + (value % 10) as u8;
value /= 10;
if value == 0 {
break;
}
}
let text = std::str::from_utf8(&buffer[index..]).expect("digits are valid utf-8");
output.write_str(text)
}
pub(crate) async fn apply_format_async(
template: &str,
args: &[Value],
) -> Result<String, RuntimeError> {
let mut output = String::with_capacity(template.len());
let bytes = template.as_bytes();
let mut index = 0;
let mut last_literal = 0;
let mut uses_sequential = false;
let mut uses_indexed = false;
let mut next_sequential = 0usize;
let mut used_indexed_bits = 0u64;
let mut used_indexed_args: Option<Vec<bool>> = None;
while index < bytes.len() {
match bytes[index] {
b'{' if bytes.get(index + 1) == Some(&b'{') => {
output.push_str(&template[last_literal..index]);
output.push('{');
index += 2;
last_literal = index;
continue;
}
b'{' => {
output.push_str(&template[last_literal..index]);
let mut cursor = index + 1;
while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
cursor += 1;
}
let (slot, slot_text) = if cursor >= bytes.len() {
return Err(RuntimeError::ValueError {
message: "unmatched `{` in format string".to_string(),
});
} else if bytes[cursor] != b'}' {
return Err(RuntimeError::ValueError {
message: "invalid format placeholder".to_string(),
});
} else if cursor == index + 1 {
if uses_indexed {
return Err(RuntimeError::ValueError {
message: "can't mix `{}` and indexed format placeholders".to_string(),
});
}
uses_sequential = true;
let slot = next_sequential;
next_sequential += 1;
(slot, None)
} else {
if uses_sequential {
return Err(RuntimeError::ValueError {
message: "can't mix `{}` and indexed format placeholders".to_string(),
});
}
uses_indexed = true;
let digits = &template[index + 1..cursor];
let slot = digits
.parse::<usize>()
.map_err(|_| RuntimeError::ValueError {
message: format!("bad format slot `{digits}`"),
})?;
if slot < args.len() {
if args.len() <= u64::BITS as usize {
used_indexed_bits |= 1u64 << slot;
} else {
let used_args =
used_indexed_args.get_or_insert_with(|| vec![false; args.len()]);
used_args[slot] = true;
}
}
(slot, Some(digits))
};
let value = args.get(slot).ok_or_else(|| RuntimeError::ValueError {
message: match slot_text {
Some(slot_text) => format!("format slot `{slot_text}` is out of range"),
None => "format slot `{}` is out of range".to_string(),
},
})?;
append_stringified_value_async(&mut output, value).await?;
index = cursor + 1;
last_literal = index;
continue;
}
b'}' if bytes.get(index + 1) == Some(&b'}') => {
output.push_str(&template[last_literal..index]);
output.push('}');
index += 2;
last_literal = index;
continue;
}
b'}' => {
return Err(RuntimeError::ValueError {
message: "unmatched `}` in format string".to_string(),
});
}
_ => {}
}
index += 1;
}
output.push_str(&template[last_literal..]);
let unused_index = if uses_sequential {
(next_sequential < args.len()).then_some(next_sequential)
} else if uses_indexed {
if args.len() <= u64::BITS as usize {
(0..args.len()).find(|slot| used_indexed_bits & (1u64 << slot) == 0)
} else {
used_indexed_args
.as_ref()
.and_then(|used_args| used_args.iter().position(|used| !*used))
}
} else {
(!args.is_empty()).then_some(0)
};
if let Some(unused_index) = unused_index {
return Err(RuntimeError::ValueError {
message: format!("format argument `{unused_index}` is unused"),
});
}
Ok(output)
}
#[cfg(test)]
pub(crate) fn apply_format(template: &str, args: &[Value]) -> Result<String, RuntimeError> {
futures_executor::block_on(apply_format_async(template, args))
}
pub(crate) fn compile_format_template(template: &str, argc: usize) -> CompiledFormatTemplate {
match parse_format_template(template, argc) {
Ok(parts) => {
let one_arg = compiled_format_one_arg(&parts, argc);
CompiledFormatTemplate {
parts: parts.into_boxed_slice(),
argc,
min_capacity: template.len(),
error: None,
one_arg,
}
}
Err(message) => CompiledFormatTemplate {
parts: Box::new([]),
argc,
min_capacity: template.len(),
error: Some(message),
one_arg: None,
},
}
}
fn compiled_format_one_arg(
parts: &[CompiledFormatPart],
argc: usize,
) -> Option<CompiledFormatOneArg> {
if argc != 1 {
return None;
}
match parts {
[CompiledFormatPart::Arg(0)] => Some(CompiledFormatOneArg {
prefix: None,
suffix: None,
}),
[
CompiledFormatPart::Literal(prefix),
CompiledFormatPart::Arg(0),
] => Some(CompiledFormatOneArg {
prefix: Some(prefix.clone()),
suffix: None,
}),
[
CompiledFormatPart::Arg(0),
CompiledFormatPart::Literal(suffix),
] => Some(CompiledFormatOneArg {
prefix: None,
suffix: Some(suffix.clone()),
}),
[
CompiledFormatPart::Literal(prefix),
CompiledFormatPart::Arg(0),
CompiledFormatPart::Literal(suffix),
] => Some(CompiledFormatOneArg {
prefix: Some(prefix.clone()),
suffix: Some(suffix.clone()),
}),
_ => None,
}
}
pub(crate) fn parse_format_template(
template: &str,
argc: usize,
) -> Result<Vec<CompiledFormatPart>, String> {
let mut parts = Vec::new();
let bytes = template.as_bytes();
let mut index = 0;
let mut last_literal = 0;
let mut uses_sequential = false;
let mut uses_indexed = false;
let mut next_sequential = 0usize;
let mut used_indexed_bits = 0u64;
let mut used_indexed_args: Option<Vec<bool>> = None;
while index < bytes.len() {
match bytes[index] {
b'{' if bytes.get(index + 1) == Some(&b'{') => {
push_format_literal(&mut parts, &template[last_literal..index]);
push_format_literal(&mut parts, "{");
index += 2;
last_literal = index;
continue;
}
b'{' => {
push_format_literal(&mut parts, &template[last_literal..index]);
let mut cursor = index + 1;
while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
cursor += 1;
}
let (slot, slot_text) = if cursor >= bytes.len() {
return Err("unmatched `{` in format string".to_string());
} else if bytes[cursor] != b'}' {
return Err("invalid format placeholder".to_string());
} else if cursor == index + 1 {
if uses_indexed {
return Err("can't mix `{}` and indexed format placeholders".to_string());
}
uses_sequential = true;
let slot = next_sequential;
next_sequential += 1;
(slot, None)
} else {
if uses_sequential {
return Err("can't mix `{}` and indexed format placeholders".to_string());
}
uses_indexed = true;
let digits = &template[index + 1..cursor];
let slot = digits
.parse::<usize>()
.map_err(|_| format!("bad format slot `{digits}`"))?;
if slot < argc {
if argc <= u64::BITS as usize {
used_indexed_bits |= 1u64 << slot;
} else {
let used_args =
used_indexed_args.get_or_insert_with(|| vec![false; argc]);
used_args[slot] = true;
}
}
(slot, Some(digits))
};
if slot >= argc {
return Err(match slot_text {
Some(slot_text) => format!("format slot `{slot_text}` is out of range"),
None => "format slot `{}` is out of range".to_string(),
});
}
parts.push(CompiledFormatPart::Arg(slot));
index = cursor + 1;
last_literal = index;
continue;
}
b'}' if bytes.get(index + 1) == Some(&b'}') => {
push_format_literal(&mut parts, &template[last_literal..index]);
push_format_literal(&mut parts, "}");
index += 2;
last_literal = index;
continue;
}
b'}' => {
return Err("unmatched `}` in format string".to_string());
}
_ => {}
}
index += 1;
}
push_format_literal(&mut parts, &template[last_literal..]);
let unused_index = if uses_sequential {
(next_sequential < argc).then_some(next_sequential)
} else if uses_indexed {
if argc <= u64::BITS as usize {
(0..argc).find(|slot| used_indexed_bits & (1u64 << slot) == 0)
} else {
used_indexed_args
.as_ref()
.and_then(|used_args| used_args.iter().position(|used| !*used))
}
} else {
(argc > 0).then_some(0)
};
if let Some(unused_index) = unused_index {
return Err(format!("format argument `{unused_index}` is unused"));
}
Ok(parts)
}
pub(crate) fn push_format_literal(parts: &mut Vec<CompiledFormatPart>, literal: &str) {
if !literal.is_empty() {
parts.push(CompiledFormatPart::Literal(Arc::<str>::from(literal)));
}
}
pub(crate) async fn execute_compiled_format(
template: &CompiledFormatTemplate,
args: &[Value],
) -> Result<String, RuntimeError> {
if let Some(message) = &template.error {
return Err(RuntimeError::ValueError {
message: message.clone(),
});
}
if let Some(shape) = &template.one_arg {
let value = args.first().ok_or_else(|| RuntimeError::ValueError {
message: "format slot `0` is out of range".to_string(),
})?;
let mut output = String::with_capacity(template.min_capacity);
push_compiled_one_arg_prefix(&mut output, shape);
append_stringified_value_async(&mut output, value).await?;
push_compiled_one_arg_suffix(&mut output, shape);
return Ok(output);
}
let mut output = String::with_capacity(template.min_capacity);
for part in template.parts.iter() {
match part {
CompiledFormatPart::Literal(literal) => output.push_str(literal),
CompiledFormatPart::Arg(slot) => {
let value = args.get(*slot).ok_or_else(|| RuntimeError::ValueError {
message: format!("format slot `{slot}` is out of range"),
})?;
append_stringified_value_async(&mut output, value).await?;
}
}
}
Ok(output)
}
pub(crate) fn execute_compiled_format_direct(
template: &CompiledFormatTemplate,
args: &[Value],
) -> Result<String, RuntimeError> {
if let Some(message) = &template.error {
return Err(RuntimeError::ValueError {
message: message.clone(),
});
}
if let Some(shape) = &template.one_arg {
let value = args.first().ok_or_else(|| RuntimeError::ValueError {
message: "format slot `0` is out of range".to_string(),
})?;
let mut output = String::with_capacity(template.min_capacity);
push_compiled_one_arg_prefix(&mut output, shape);
append_stringified_value_direct(&mut output, value)?;
push_compiled_one_arg_suffix(&mut output, shape);
return Ok(output);
}
let mut output = String::with_capacity(template.min_capacity);
for part in template.parts.iter() {
match part {
CompiledFormatPart::Literal(literal) => output.push_str(literal),
CompiledFormatPart::Arg(slot) => {
let value = args.get(*slot).ok_or_else(|| RuntimeError::ValueError {
message: format!("format slot `{slot}` is out of range"),
})?;
append_stringified_value_direct(&mut output, value)?;
}
}
}
Ok(output)
}
pub(crate) fn execute_compiled_format_one_number_compact_direct(
template: &CompiledFormatTemplate,
value: f64,
) -> Result<CompactString, RuntimeError> {
if let Some(message) = &template.error {
return Err(RuntimeError::ValueError {
message: message.clone(),
});
}
if let Some(shape) = &template.one_arg {
let mut output = CompactString::with_capacity(template.min_capacity);
push_compiled_one_arg_prefix(&mut output, shape);
write_number(&mut output, value).expect("string writes should not fail");
push_compiled_one_arg_suffix(&mut output, shape);
return Ok(output);
}
let mut output = CompactString::with_capacity(template.min_capacity);
for part in template.parts.iter() {
match part {
CompiledFormatPart::Literal(literal) => output.push_str(literal),
CompiledFormatPart::Arg(0) => {
write_number(&mut output, value).expect("string writes should not fail");
}
CompiledFormatPart::Arg(slot) => {
return Err(RuntimeError::ValueError {
message: format!("format slot `{slot}` is out of range"),
});
}
}
}
Ok(output)
}
fn push_compiled_one_arg_prefix(output: &mut impl fmt::Write, shape: &CompiledFormatOneArg) {
if let Some(prefix) = &shape.prefix {
output
.write_str(prefix)
.expect("string writes should not fail");
}
}
fn push_compiled_one_arg_suffix(output: &mut impl fmt::Write, shape: &CompiledFormatOneArg) {
if let Some(suffix) = &shape.suffix {
output
.write_str(suffix)
.expect("string writes should not fail");
}
}
pub(crate) fn clamp_slice_bounds(
start: Option<isize>,
end: Option<isize>,
len: usize,
) -> Option<(usize, usize)> {
let len = len as isize;
let start = normalize_slice_bound(start.unwrap_or(0), len);
let end = normalize_slice_bound(end.unwrap_or(len), len);
(start < end).then_some((start, end))
}
pub(crate) fn normalize_slice_bound(bound: isize, len: isize) -> usize {
let normalized = if bound < 0 { len + bound } else { bound };
normalized.clamp(0, len) as usize
}
pub(crate) fn slice_string(value: &str, start: Option<isize>, end: Option<isize>) -> String {
let char_count = value.chars().count();
let Some((start, end)) = clamp_slice_bounds(start, end, char_count) else {
return String::new();
};
let mut indices = value
.char_indices()
.map(|(idx, _)| idx)
.chain(std::iter::once(value.len()));
let Some(byte_start) = indices.nth(start) else {
return String::new();
};
let Some(byte_end) = indices.nth(end - start - 1) else {
return value[byte_start..].to_string();
};
value[byte_start..byte_end].to_string()
}