Skip to content

Commit

Permalink
Merge pull request #845 from zitsen/feat/str-index-as-slice
Browse files Browse the repository at this point in the history
feat: range indexing on strings
  • Loading branch information
schungx committed Mar 19, 2024
2 parents f44af63 + 33ac732 commit 6dc86bb
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 41 deletions.
6 changes: 6 additions & 0 deletions src/ast/expr.rs
Expand Up @@ -530,6 +530,9 @@ impl Expr {
Self::IntegerConstant(ref start, ..),
Self::IntegerConstant(ref end, ..),
) => (*start..*end).into(),
(Self::IntegerConstant(ref start, ..), Self::Unit(..)) => {
(*start..INT::MAX).into()
}
_ => return None,
},
// x..=y
Expand All @@ -538,6 +541,9 @@ impl Expr {
Self::IntegerConstant(ref start, ..),
Self::IntegerConstant(ref end, ..),
) => (*start..=*end).into(),
(Self::IntegerConstant(ref start, ..), Self::Unit(..)) => {
(*start..=INT::MAX).into()
}
_ => return None,
},
_ => return None,
Expand Down
164 changes: 123 additions & 41 deletions src/eval/chaining.rs
Expand Up @@ -4,6 +4,7 @@
use super::{Caches, GlobalRuntimeState, Target};
use crate::ast::{ASTFlags, BinaryExpr, Expr, OpAssignment};
use crate::engine::{FN_IDX_GET, FN_IDX_SET};
use crate::tokenizer::Token;

Check warning on line 7 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_index,serde,metadata,internals,debugging, sta...

unused import: `crate::tokenizer::Token`
use crate::types::dynamic::Union;
use crate::{
calc_fn_hash, Dynamic, Engine, FnArgsVec, OnceCell, Position, RhaiResult, RhaiResultOf, Scope,
Expand Down Expand Up @@ -295,53 +296,127 @@ impl Engine {

#[cfg(not(feature = "no_index"))]
Dynamic(Union::Str(s, ..)) => {
// val_string[idx]
let index = idx
.as_int()
.map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, idx_pos))?;
match idx.as_int() {
Ok(index) => {
let (ch, offset) = if index >= 0 {
#[allow(clippy::absurd_extreme_comparisons)]
if index >= crate::MAX_USIZE_INT {
return Err(ERR::ErrorStringBounds(
s.chars().count(),
index,
idx_pos,
)
.into());
}

let (ch, offset) = if index >= 0 {
#[allow(clippy::absurd_extreme_comparisons)]
if index >= crate::MAX_USIZE_INT {
return Err(
ERR::ErrorStringBounds(s.chars().count(), index, idx_pos).into()
);
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let offset = index as usize;
(
s.chars().nth(offset).ok_or_else(|| {
ERR::ErrorStringBounds(s.chars().count(), index, idx_pos)
})?,
offset,
)
} else {
let abs_index = index.unsigned_abs();

#[allow(clippy::unnecessary_cast)]
if abs_index as u64 > usize::MAX as u64 {
return Err(ERR::ErrorStringBounds(
s.chars().count(),
index,
idx_pos,
)
.into());
}

#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let offset = index as usize;
(
s.chars().nth(offset).ok_or_else(|| {
ERR::ErrorStringBounds(s.chars().count(), index, idx_pos)
})?,
offset,
)
} else {
let abs_index = index.unsigned_abs();
#[allow(clippy::cast_possible_truncation)]
let offset = abs_index as usize;
(
// Count from end if negative
s.chars().rev().nth(offset - 1).ok_or_else(|| {
ERR::ErrorStringBounds(s.chars().count(), index, idx_pos)
})?,
offset,
)
};

#[allow(clippy::unnecessary_cast)]
if abs_index as u64 > usize::MAX as u64 {
return Err(
ERR::ErrorStringBounds(s.chars().count(), index, idx_pos).into()
);
Ok(Target::StringChar {
source: target,
value: ch.into(),
index: offset,
})
}
Err(typ) => {
if typ == "core::ops::range::Range<i64>" {
// val_str[range]
let idx = idx.read_lock::<core::ops::Range<i64>>().unwrap();
let range = &*idx;
let chars_count = s.chars().count();
let start = if range.start >= 0 {
range.start as usize
} else {
super::calc_index(chars_count, range.start, true, || {

Check failure on line 359 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
ERR::ErrorStringBounds(chars_count, range.start, idx_pos).into()

Check failure on line 360 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
})?
};
let end = if range.end >= 0 {
range.end as usize
} else {
super::calc_index(chars_count, range.end, true, || {

Check failure on line 366 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
ERR::ErrorStringBounds(chars_count, range.end, idx_pos).into()

Check failure on line 367 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
})
.unwrap_or(0)
};

#[allow(clippy::cast_possible_truncation)]
let offset = abs_index as usize;
(
// Count from end if negative
s.chars().rev().nth(offset - 1).ok_or_else(|| {
ERR::ErrorStringBounds(s.chars().count(), index, idx_pos)
})?,
offset,
)
};
let take = if end > start { end - start } else { 0 };

let value = s.chars().skip(start).take(take).collect::<String>();
return Ok(Target::StringSlice {
source: target,
value: value.into(),
start: start,
end: end,
exclusive: true,
});
} else if typ == "core::ops::range::RangeInclusive<i64>" {
// val_str[range]
let idx = idx.read_lock::<core::ops::RangeInclusive<i64>>().unwrap();
let range = &*idx;
let chars_count = s.chars().count();
let start = if *range.start() >= 0 {
*range.start() as usize
} else {
super::calc_index(chars_count, *range.start(), true, || {

Check failure on line 390 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
ERR::ErrorStringBounds(chars_count, *range.start(), idx_pos)

Check failure on line 391 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
.into()
})?
};
let end = if *range.end() >= 0 {
*range.end() as usize
} else {
super::calc_index(chars_count, *range.end(), true, || {

Check failure on line 398 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
ERR::ErrorStringBounds(chars_count, *range.end(), idx_pos)

Check failure on line 399 in src/eval/chaining.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --tests --features testing-environ,only_i32,serde,metadata,internals,debugg...

mismatched types
.into()
})
.unwrap_or(0)
};

Ok(Target::StringChar {
source: target,
value: ch.into(),
index: offset,
})
let take = if end > start { end - start + 1 } else { 0 };

let value = s.chars().skip(start).take(take).collect::<String>();
return Ok(Target::StringSlice {
source: target,
value: value.into(),
start,
end,
exclusive: false,
});
} else {
return Err(self.make_type_mismatch_err::<crate::INT>(typ, idx_pos));
}
}
}
}

#[cfg(not(feature = "no_closure"))]
Expand Down Expand Up @@ -399,6 +474,13 @@ impl Engine {
(_, ChainType::Indexing) if rhs.is_constant() => {
idx_values.push(rhs.get_literal_value().unwrap())
}
#[cfg(not(feature = "no_index"))]
(Expr::FnCall(fnc, _), ChainType::Indexing)
if fnc.op_token == Some(Token::InclusiveRange)
|| fnc.op_token == Some(Token::ExclusiveRange) =>
{
idx_values.push(rhs.get_literal_value().unwrap())
}
// Short-circuit for simple method call: {expr}.func()
#[cfg(not(feature = "no_object"))]
(Expr::MethodCall(x, ..), ChainType::Dotting) if x.args.is_empty() => (),
Expand Down
52 changes: 52 additions & 0 deletions src/eval/target.rs
Expand Up @@ -162,6 +162,21 @@ pub enum Target<'a> {
/// Offset index.
index: usize,
},
/// The target is a slice of a string.
/// This is necessary because directly pointing to a [`char`] inside a [`String`] is impossible.
#[cfg(not(feature = "no_index"))]
StringSlice {
/// Mutable reference to the source [`Dynamic`].
source: &'a mut Dynamic,
/// Copy of the character at the offset, as a [`Dynamic`].
value: Dynamic,
/// Offset index.
start: usize,
/// End index.
end: usize,
/// Is exclusive?
exclusive: bool,
},
}

impl<'a> Target<'a> {
Expand All @@ -174,6 +189,8 @@ impl<'a> Target<'a> {
Self::RefMut(..) => true,
#[cfg(not(feature = "no_closure"))]
Self::SharedValue { .. } => true,
#[cfg(not(feature = "no_index"))]
Self::StringSlice { .. } => true,
Self::TempValue(..) => false,
#[cfg(not(feature = "no_index"))]
Self::Bit { .. }
Expand All @@ -190,6 +207,8 @@ impl<'a> Target<'a> {
Self::RefMut(..) => false,
#[cfg(not(feature = "no_closure"))]
Self::SharedValue { .. } => false,
#[cfg(not(feature = "no_index"))]
Self::StringSlice { .. } => true,
Self::TempValue(..) => true,
#[cfg(not(feature = "no_index"))]
Self::Bit { .. }
Expand All @@ -206,6 +225,7 @@ impl<'a> Target<'a> {
return match self {
Self::RefMut(r) => r.is_shared(),
Self::SharedValue { .. } => true,
Self::StringSlice { .. } => true,

Check failure on line 228 in src/eval/target.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_index,serde,metadata,internals,debugging, sta...

no variant named `StringSlice` found for enum `Target<'a>`
Self::TempValue(value) => value.is_shared(),
#[cfg(not(feature = "no_index"))]
Self::Bit { .. }
Expand All @@ -232,6 +252,11 @@ impl<'a> Target<'a> {
Self::BlobByte { value, .. } => value, // byte is taken
#[cfg(not(feature = "no_index"))]
Self::StringChar { value, .. } => value, // char is taken
#[cfg(not(feature = "no_index"))]
Self::StringSlice { value, .. } => {
// Slice of a string is cloned
Dynamic::from(value.to_string())
}
}
}
/// Take a `&mut Dynamic` reference from the `Target`.
Expand Down Expand Up @@ -270,6 +295,8 @@ impl<'a> Target<'a> {
Self::BlobByte { source, .. } => source,
#[cfg(not(feature = "no_index"))]
Self::StringChar { source, .. } => source,
#[cfg(not(feature = "no_index"))]
Self::StringSlice { source, .. } => source,
}
}
/// Propagate a changed value back to the original source.
Expand Down Expand Up @@ -372,6 +399,27 @@ impl<'a> Target<'a> {
.map(|(i, ch)| if i == *index { new_ch } else { ch })
.collect();
}
#[cfg(not(feature = "no_index"))]
Self::StringSlice {
source,
ref start,
ref end,
ref value,
exclusive,
} => {
let s = &mut *source.write_lock::<crate::ImmutableString>().unwrap();

let n = s.chars().count();
let vs = s.chars().take(*start);
let ve = if *exclusive {
let end = if *end > n { n } else { *end };
s.chars().skip(end)
} else {
let end = if *end >= n { n - 1 } else { *end };
s.chars().skip(end + 1)
};
*s = vs.chain(value.to_string().chars()).chain(ve).collect();
}
}

Ok(())
Expand Down Expand Up @@ -405,6 +453,8 @@ impl AsRef<Dynamic> for Target<'_> {
Self::SharedValue { guard, .. } => guard,
Self::TempValue(ref value) => value,
#[cfg(not(feature = "no_index"))]
Self::StringSlice { ref value, .. } => value,
#[cfg(not(feature = "no_index"))]
Self::Bit { ref value, .. }
| Self::BitField { ref value, .. }
| Self::BlobByte { ref value, .. }
Expand All @@ -429,6 +479,8 @@ impl AsMut<Dynamic> for Target<'_> {
Self::SharedValue { guard, .. } => &mut *guard,
Self::TempValue(ref mut value) => value,
#[cfg(not(feature = "no_index"))]
Self::StringSlice { ref mut value, .. } => value,
#[cfg(not(feature = "no_index"))]
Self::Bit { ref mut value, .. }
| Self::BitField { ref mut value, .. }
| Self::BlobByte { ref mut value, .. }
Expand Down
14 changes: 14 additions & 0 deletions src/parser.rs
Expand Up @@ -1344,6 +1344,7 @@ impl Engine {
Token::False => Expr::BoolConstant(false, settings.pos),
token => unreachable!("token is {:?}", token),
},
Token::ExclusiveRange | Token::InclusiveRange => Expr::IntegerConstant(0, settings.pos),
#[cfg(not(feature = "no_float"))]
Token::FloatConstant(x) => {
let x = x.0;
Expand Down Expand Up @@ -2014,6 +2015,13 @@ impl Engine {
}
.into_fn_call_expr(pos))
}
Token::RightBracket => {
let v = state.input.peek();
match v {
Some((Token::RightBracket, _)) => Ok(Expr::Unit(settings.pos.clone())),
_ => self.parse_primary(state, settings, ChainingFlags::empty()),
}
}
// <EOF>
Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)),
// All other tokens
Expand Down Expand Up @@ -2412,6 +2420,12 @@ impl Engine {
not_base.into_fn_call_expr(pos)
}
}
Token::ExclusiveRange | Token::InclusiveRange => {
if op_base.args[1].is_unit() {
let _ = op_base.args[1].take();
}
op_base.into_fn_call_expr(pos)
}

#[cfg(not(feature = "no_custom_syntax"))]
Token::Custom(s) if self.custom_keywords.contains_key(&*s) => {
Expand Down

0 comments on commit 6dc86bb

Please sign in to comment.