use alloc::string::String;
use brink_format::Value;
use crate::error::RuntimeError;
use crate::story::Flow;
pub(crate) fn char_at(flow: &mut Flow) -> Result<(), RuntimeError> {
let index = flow.pop_value()?;
let s = flow.pop_value()?;
let Value::String(text) = &s else {
return Err(RuntimeError::NotIndexable(type_name(&s)));
};
let Value::Int(i) = index else {
return Err(RuntimeError::CharAtIndexNotInt(type_name(&index)));
};
let nth = if i >= 0 {
#[expect(
clippy::cast_sign_loss,
reason = "i >= 0 just checked; the `else` branch below handles negative i"
)]
let idx = i as usize;
text.chars().nth(idx)
} else {
None
};
match nth {
Some(c) => {
let mut out = String::new();
out.push(c);
flow.value_stack.push(Value::String(out.into()));
Ok(())
}
None => Err(RuntimeError::CharAtOutOfBounds {
index: i,
len: text.chars().count(),
}),
}
}
pub(crate) fn str_find(flow: &mut Flow) -> Result<(), RuntimeError> {
let sub = flow.pop_value()?;
let s = flow.pop_value()?;
let Value::String(text) = &s else {
return Err(RuntimeError::StdlibWrongType {
verb: "find",
expected: "a string",
found: type_name(&s),
});
};
let Value::String(needle) = &sub else {
return Err(RuntimeError::StdlibWrongType {
verb: "find",
expected: "a string",
found: type_name(&sub),
});
};
#[expect(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let result = text.find(needle.as_ref()).map_or_else(Value::none, |byte| {
Value::some(Value::Int(text[..byte].chars().count() as i32))
});
flow.value_stack.push(result);
Ok(())
}
fn type_name(v: &Value) -> &'static str {
match v {
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Bool(_) => "bool",
Value::String(_) => "string",
Value::List(_) => "list",
Value::DivertTarget(_) => "divert_target",
Value::VariablePointer(_) => "var_pointer",
Value::TempPointer { .. } => "temp_pointer",
Value::Null => "null",
Value::FragmentRef(_) => "fragment_ref",
Value::Array(_) => "array",
Value::Map(_) => "map",
Value::Record { .. } => "record",
Value::FnRef(_) | Value::Closure(_) => "fn",
Value::Handle { .. } => "handle",
Value::Projection(_) => "projection",
Value::OptionVal(_) => "option",
Value::Range { .. } => "range",
Value::Vec2(_) => "vec2",
Value::Vec3(_) => "vec3",
Value::Vec4(_) => "vec4",
Value::Quat(_) => "quat",
Value::Mat2(_) => "mat2",
Value::Mat3(_) => "mat3",
Value::Mat4(_) => "mat4",
Value::Weighted(_) => "weighted",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::OutputBuffer;
use crate::story::PendingTerminal;
fn test_flow() -> Flow {
Flow {
threads: alloc::vec::Vec::new(),
value_stack: alloc::vec::Vec::new(),
output: OutputBuffer::new(),
pending_choices: alloc::vec::Vec::new(),
current_tags: alloc::vec::Vec::new(),
in_tag: false,
skipping_choice: false,
did_safe_exit: false,
did_unsafe_yield: false,
ran_out_of_content_cause: crate::RanOutOfContentCause::default(),
line_delivered_this_turn: false,
exec_mode: crate::story::ExecMode::default(),
pure_callback: crate::story::PureCallbackState::default(),
next_block_id: 0,
pending_terminal: PendingTerminal::default(),
warnings: Vec::new(),
}
}
fn push_args(flow: &mut Flow, args: [Value; 2]) {
for v in args {
flow.value_stack.push(v);
}
}
#[test]
fn char_at_first_char() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hello"), Value::Int(0)]);
char_at(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::String("h".into()));
}
#[test]
fn char_at_last_char() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hello"), Value::Int(4)]);
char_at(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::String("o".into()));
}
#[test]
fn char_at_counts_unicode_scalar_values_not_bytes() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("café"), Value::Int(3)]);
char_at(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::String("é".into()));
}
#[test]
fn char_at_index_equal_to_len_faults() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hi"), Value::Int(2)]);
let err = char_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::CharAtOutOfBounds { index: 2, len: 2 });
}
#[test]
fn char_at_negative_index_faults() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hi"), Value::Int(-1)]);
let err = char_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::CharAtOutOfBounds { index: -1, len: 2 });
}
#[test]
fn char_at_out_of_bounds_reports_char_count_not_byte_length() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("café"), Value::Int(4)]);
let err = char_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::CharAtOutOfBounds { index: 4, len: 4 });
}
#[test]
fn char_at_non_string_faults() {
let mut flow = test_flow();
push_args(&mut flow, [Value::Int(5), Value::Int(0)]);
let err = char_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::NotIndexable("int"));
}
#[test]
fn char_at_non_int_index_faults() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hi"), Value::from("nope")]);
let err = char_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::CharAtIndexNotInt("string"));
}
#[test]
fn char_at_empty_string_faults() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from(""), Value::Int(0)]);
let err = char_at(&mut flow).unwrap_err();
assert_eq!(err, RuntimeError::CharAtOutOfBounds { index: 0, len: 0 });
}
#[test]
fn find_present_returns_some_index_absent_returns_none() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hello"), Value::from("ll")]);
str_find(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(2)));
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hello"), Value::from("xyz")]);
str_find(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::none());
}
#[test]
fn find_counts_unicode_scalar_values_not_bytes() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("café au lait"), Value::from("au")]);
str_find(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(5)));
}
#[test]
fn find_empty_substring_is_index_zero() {
let mut flow = test_flow();
push_args(&mut flow, [Value::from("hi"), Value::from("")]);
str_find(&mut flow).unwrap();
assert_eq!(flow.pop_value().unwrap(), Value::some(Value::Int(0)));
}
#[test]
fn find_non_string_arguments_fault() {
let mut flow = test_flow();
push_args(&mut flow, [Value::Int(3), Value::from("x")]);
let err = str_find(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "find",
expected: "a string",
found: "int",
}
);
let mut flow = test_flow();
push_args(&mut flow, [Value::from("x"), Value::Int(3)]);
let err = str_find(&mut flow).unwrap_err();
assert_eq!(
err,
RuntimeError::StdlibWrongType {
verb: "find",
expected: "a string",
found: "int",
}
);
}
}