Implement string interpolation.
This commit is contained in:
parent
ab0ea87f9c
commit
e6c878edf3
@ -19,6 +19,7 @@ Breaking changes
|
|||||||
New features
|
New features
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
* String interpolation support is added via the `` `... ${`` ... ``} ...` `` syntax.
|
||||||
* `FileModuleResolver` resolves relative paths under the parent path (i.e. the path holding the script that does the loading). This allows seamless cross-loading of scripts from a directory hierarchy instead of having all relative paths load from the current working directory.
|
* `FileModuleResolver` resolves relative paths under the parent path (i.e. the path holding the script that does the loading). This allows seamless cross-loading of scripts from a directory hierarchy instead of having all relative paths load from the current working directory.
|
||||||
|
|
||||||
|
|
||||||
|
38
src/ast.rs
38
src/ast.rs
@ -8,6 +8,7 @@ use crate::stdlib::{
|
|||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
fmt,
|
fmt,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
|
iter::empty,
|
||||||
num::NonZeroUsize,
|
num::NonZeroUsize,
|
||||||
ops::{Add, AddAssign},
|
ops::{Add, AddAssign},
|
||||||
string::String,
|
string::String,
|
||||||
@ -15,6 +16,7 @@ use crate::stdlib::{
|
|||||||
vec::Vec,
|
vec::Vec,
|
||||||
};
|
};
|
||||||
use crate::token::Token;
|
use crate::token::Token;
|
||||||
|
use crate::utils::calc_fn_hash;
|
||||||
use crate::{
|
use crate::{
|
||||||
Dynamic, FnNamespace, FnPtr, Identifier, ImmutableString, Module, Position, Shared, StaticVec,
|
Dynamic, FnNamespace, FnPtr, Identifier, ImmutableString, Module, Position, Shared, StaticVec,
|
||||||
INT,
|
INT,
|
||||||
@ -844,10 +846,11 @@ impl StmtBlock {
|
|||||||
impl fmt::Debug for StmtBlock {
|
impl fmt::Debug for StmtBlock {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Debug::fmt(&self.statements, f)?;
|
||||||
if !self.pos.is_none() {
|
if !self.pos.is_none() {
|
||||||
write!(f, "{} @ ", self.pos)?;
|
write!(f, " @ {:?}", self.pos)?;
|
||||||
}
|
}
|
||||||
fmt::Debug::fmt(&self.statements, f)
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1293,6 +1296,18 @@ pub struct OpAssignment {
|
|||||||
pub op: &'static str,
|
pub op: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OpAssignment {
|
||||||
|
pub fn new(op: &'static str) -> Self {
|
||||||
|
let op2 = &op[..op.len() - 1]; // extract operator without =
|
||||||
|
|
||||||
|
Self {
|
||||||
|
hash_op_assign: calc_fn_hash(empty(), op, 2),
|
||||||
|
hash_op: calc_fn_hash(empty(), op2, 2),
|
||||||
|
op,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// _(INTERNALS)_ An set of function call hashes.
|
/// _(INTERNALS)_ An set of function call hashes.
|
||||||
/// Exported under the `internals` feature only.
|
/// Exported under the `internals` feature only.
|
||||||
///
|
///
|
||||||
@ -1548,6 +1563,8 @@ pub enum Expr {
|
|||||||
StringConstant(ImmutableString, Position),
|
StringConstant(ImmutableString, Position),
|
||||||
/// [`FnPtr`] constant.
|
/// [`FnPtr`] constant.
|
||||||
FnPointer(ImmutableString, Position),
|
FnPointer(ImmutableString, Position),
|
||||||
|
/// An interpolated [string][ImmutableString].
|
||||||
|
InterpolatedString(Box<StaticVec<Expr>>),
|
||||||
/// [ expr, ... ]
|
/// [ expr, ... ]
|
||||||
Array(Box<StaticVec<Expr>>, Position),
|
Array(Box<StaticVec<Expr>>, Position),
|
||||||
/// #{ name:expr, ... }
|
/// #{ name:expr, ... }
|
||||||
@ -1608,7 +1625,7 @@ impl Expr {
|
|||||||
Self::Array(x, _) if self.is_constant() => {
|
Self::Array(x, _) if self.is_constant() => {
|
||||||
let mut arr = Array::with_capacity(x.len());
|
let mut arr = Array::with_capacity(x.len());
|
||||||
arr.extend(x.iter().map(|v| v.get_constant_value().unwrap()));
|
arr.extend(x.iter().map(|v| v.get_constant_value().unwrap()));
|
||||||
Dynamic(Union::Array(Box::new(arr), AccessMode::ReadOnly))
|
arr.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "no_object"))]
|
#[cfg(not(feature = "no_object"))]
|
||||||
@ -1617,7 +1634,7 @@ impl Expr {
|
|||||||
x.0.iter().for_each(|(k, v)| {
|
x.0.iter().for_each(|(k, v)| {
|
||||||
*map.get_mut(k.name.as_str()).unwrap() = v.get_constant_value().unwrap()
|
*map.get_mut(k.name.as_str()).unwrap() = v.get_constant_value().unwrap()
|
||||||
});
|
});
|
||||||
Dynamic(Union::Map(Box::new(map), AccessMode::ReadOnly))
|
map.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => return None,
|
_ => return None,
|
||||||
@ -1643,6 +1660,7 @@ impl Expr {
|
|||||||
Self::IntegerConstant(_, pos) => *pos,
|
Self::IntegerConstant(_, pos) => *pos,
|
||||||
Self::CharConstant(_, pos) => *pos,
|
Self::CharConstant(_, pos) => *pos,
|
||||||
Self::StringConstant(_, pos) => *pos,
|
Self::StringConstant(_, pos) => *pos,
|
||||||
|
Self::InterpolatedString(x) => x.first().unwrap().position(),
|
||||||
Self::FnPointer(_, pos) => *pos,
|
Self::FnPointer(_, pos) => *pos,
|
||||||
Self::Array(_, pos) => *pos,
|
Self::Array(_, pos) => *pos,
|
||||||
Self::Map(_, pos) => *pos,
|
Self::Map(_, pos) => *pos,
|
||||||
@ -1672,6 +1690,9 @@ impl Expr {
|
|||||||
Self::IntegerConstant(_, pos) => *pos = new_pos,
|
Self::IntegerConstant(_, pos) => *pos = new_pos,
|
||||||
Self::CharConstant(_, pos) => *pos = new_pos,
|
Self::CharConstant(_, pos) => *pos = new_pos,
|
||||||
Self::StringConstant(_, pos) => *pos = new_pos,
|
Self::StringConstant(_, pos) => *pos = new_pos,
|
||||||
|
Self::InterpolatedString(x) => {
|
||||||
|
x.first_mut().unwrap().set_position(new_pos);
|
||||||
|
}
|
||||||
Self::FnPointer(_, pos) => *pos = new_pos,
|
Self::FnPointer(_, pos) => *pos = new_pos,
|
||||||
Self::Array(_, pos) => *pos = new_pos,
|
Self::Array(_, pos) => *pos = new_pos,
|
||||||
Self::Map(_, pos) => *pos = new_pos,
|
Self::Map(_, pos) => *pos = new_pos,
|
||||||
@ -1693,7 +1714,7 @@ impl Expr {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_pure(&self) -> bool {
|
pub fn is_pure(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Array(x, _) => x.iter().all(Self::is_pure),
|
Self::InterpolatedString(x) | Self::Array(x, _) => x.iter().all(Self::is_pure),
|
||||||
|
|
||||||
Self::Map(x, _) => x.0.iter().map(|(_, v)| v).all(Self::is_pure),
|
Self::Map(x, _) => x.0.iter().map(|(_, v)| v).all(Self::is_pure),
|
||||||
|
|
||||||
@ -1731,10 +1752,8 @@ impl Expr {
|
|||||||
| Self::FnPointer(_, _)
|
| Self::FnPointer(_, _)
|
||||||
| Self::Unit(_) => true,
|
| Self::Unit(_) => true,
|
||||||
|
|
||||||
// An array literal is constant if all items are constant
|
Self::InterpolatedString(x) | Self::Array(x, _) => x.iter().all(Self::is_constant),
|
||||||
Self::Array(x, _) => x.iter().all(Self::is_constant),
|
|
||||||
|
|
||||||
// An map literal is constant if all items are constant
|
|
||||||
Self::Map(x, _) => x.0.iter().map(|(_, expr)| expr).all(Self::is_constant),
|
Self::Map(x, _) => x.0.iter().map(|(_, expr)| expr).all(Self::is_constant),
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
@ -1763,6 +1782,7 @@ impl Expr {
|
|||||||
| Self::Unit(_) => false,
|
| Self::Unit(_) => false,
|
||||||
|
|
||||||
Self::StringConstant(_, _)
|
Self::StringConstant(_, _)
|
||||||
|
| Self::InterpolatedString(_)
|
||||||
| Self::FnCall(_, _)
|
| Self::FnCall(_, _)
|
||||||
| Self::Stmt(_)
|
| Self::Stmt(_)
|
||||||
| Self::Dot(_, _)
|
| Self::Dot(_, _)
|
||||||
@ -1814,7 +1834,7 @@ impl Expr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Array(x, _) => {
|
Self::InterpolatedString(x) | Self::Array(x, _) => {
|
||||||
for e in x.as_ref() {
|
for e in x.as_ref() {
|
||||||
if !e.walk(path, on_node) {
|
if !e.walk(path, on_node) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -25,8 +25,8 @@ use crate::stdlib::{
|
|||||||
use crate::syntax::CustomSyntax;
|
use crate::syntax::CustomSyntax;
|
||||||
use crate::utils::get_hasher;
|
use crate::utils::get_hasher;
|
||||||
use crate::{
|
use crate::{
|
||||||
Dynamic, EvalAltResult, FnPtr, Identifier, Module, Position, RhaiResult, Scope, Shared,
|
Dynamic, EvalAltResult, FnPtr, Identifier, ImmutableString, Module, Position, RhaiResult,
|
||||||
StaticVec,
|
Scope, Shared, StaticVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "no_index"))]
|
#[cfg(not(feature = "no_index"))]
|
||||||
@ -200,6 +200,9 @@ pub const FN_ANONYMOUS: &str = "anon$";
|
|||||||
/// Standard equality comparison operator.
|
/// Standard equality comparison operator.
|
||||||
pub const OP_EQUALS: &str = "==";
|
pub const OP_EQUALS: &str = "==";
|
||||||
|
|
||||||
|
/// Standard concatenation operator.
|
||||||
|
pub const OP_CONCAT: &str = "+=";
|
||||||
|
|
||||||
/// Standard method function for containment testing.
|
/// Standard method function for containment testing.
|
||||||
///
|
///
|
||||||
/// The `in` operator is implemented as a call to this method.
|
/// The `in` operator is implemented as a call to this method.
|
||||||
@ -410,7 +413,7 @@ impl<'a> Target<'a> {
|
|||||||
Self::Value(_) => panic!("cannot update a value"),
|
Self::Value(_) => panic!("cannot update a value"),
|
||||||
#[cfg(not(feature = "no_index"))]
|
#[cfg(not(feature = "no_index"))]
|
||||||
Self::StringChar(s, index, _) => {
|
Self::StringChar(s, index, _) => {
|
||||||
let s = &mut *s.write_lock::<crate::ImmutableString>().unwrap();
|
let s = &mut *s.write_lock::<ImmutableString>().unwrap();
|
||||||
|
|
||||||
// Replace the character at the specified index position
|
// Replace the character at the specified index position
|
||||||
let new_ch = new_val.as_char().map_err(|err| {
|
let new_ch = new_val.as_char().map_err(|err| {
|
||||||
@ -591,7 +594,7 @@ pub struct Limits {
|
|||||||
/// Not available under `no_module`.
|
/// Not available under `no_module`.
|
||||||
#[cfg(not(feature = "no_module"))]
|
#[cfg(not(feature = "no_module"))]
|
||||||
pub max_modules: usize,
|
pub max_modules: usize,
|
||||||
/// Maximum length of a [string][crate::ImmutableString].
|
/// Maximum length of a [string][ImmutableString].
|
||||||
pub max_string_size: Option<NonZeroUsize>,
|
pub max_string_size: Option<NonZeroUsize>,
|
||||||
/// Maximum length of an [array][Array].
|
/// Maximum length of an [array][Array].
|
||||||
///
|
///
|
||||||
@ -714,6 +717,9 @@ pub struct Engine {
|
|||||||
/// A map mapping type names to pretty-print names.
|
/// A map mapping type names to pretty-print names.
|
||||||
pub(crate) type_names: BTreeMap<Identifier, Identifier>,
|
pub(crate) type_names: BTreeMap<Identifier, Identifier>,
|
||||||
|
|
||||||
|
/// An empty [`ImmutableString`] for cloning purposes.
|
||||||
|
pub(crate) empty_string: ImmutableString,
|
||||||
|
|
||||||
/// A set of symbols to disable.
|
/// A set of symbols to disable.
|
||||||
pub(crate) disabled_symbols: BTreeSet<Identifier>,
|
pub(crate) disabled_symbols: BTreeSet<Identifier>,
|
||||||
/// A map containing custom keywords and precedence to recognize.
|
/// A map containing custom keywords and precedence to recognize.
|
||||||
@ -815,6 +821,7 @@ impl Engine {
|
|||||||
module_resolver: Box::new(crate::module::resolvers::DummyModuleResolver::new()),
|
module_resolver: Box::new(crate::module::resolvers::DummyModuleResolver::new()),
|
||||||
|
|
||||||
type_names: Default::default(),
|
type_names: Default::default(),
|
||||||
|
empty_string: Default::default(),
|
||||||
disabled_symbols: Default::default(),
|
disabled_symbols: Default::default(),
|
||||||
custom_keywords: Default::default(),
|
custom_keywords: Default::default(),
|
||||||
custom_syntax: Default::default(),
|
custom_syntax: Default::default(),
|
||||||
@ -875,6 +882,7 @@ impl Engine {
|
|||||||
module_resolver: Box::new(crate::module::resolvers::DummyModuleResolver::new()),
|
module_resolver: Box::new(crate::module::resolvers::DummyModuleResolver::new()),
|
||||||
|
|
||||||
type_names: Default::default(),
|
type_names: Default::default(),
|
||||||
|
empty_string: Default::default(),
|
||||||
disabled_symbols: Default::default(),
|
disabled_symbols: Default::default(),
|
||||||
custom_keywords: Default::default(),
|
custom_keywords: Default::default(),
|
||||||
custom_syntax: Default::default(),
|
custom_syntax: Default::default(),
|
||||||
@ -1587,8 +1595,8 @@ impl Engine {
|
|||||||
#[cfg(not(feature = "no_object"))]
|
#[cfg(not(feature = "no_object"))]
|
||||||
Dynamic(Union::Map(map, _)) => {
|
Dynamic(Union::Map(map, _)) => {
|
||||||
// val_map[idx]
|
// val_map[idx]
|
||||||
let index = &*idx.read_lock::<crate::ImmutableString>().ok_or_else(|| {
|
let index = &*idx.read_lock::<ImmutableString>().ok_or_else(|| {
|
||||||
self.make_type_mismatch_err::<crate::ImmutableString>(idx.type_name(), idx_pos)
|
self.make_type_mismatch_err::<ImmutableString>(idx.type_name(), idx_pos)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if _create && !map.contains_key(index.as_str()) {
|
if _create && !map.contains_key(index.as_str()) {
|
||||||
@ -1698,6 +1706,34 @@ impl Engine {
|
|||||||
self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None)
|
self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `... ${...} ...`
|
||||||
|
Expr::InterpolatedString(x) => {
|
||||||
|
let mut pos = expr.position();
|
||||||
|
let mut result: Dynamic = self.empty_string.clone().into();
|
||||||
|
|
||||||
|
for expr in x.iter() {
|
||||||
|
let item = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?;
|
||||||
|
self.eval_op_assignment(
|
||||||
|
mods,
|
||||||
|
state,
|
||||||
|
lib,
|
||||||
|
Some(OpAssignment::new(OP_CONCAT)),
|
||||||
|
pos,
|
||||||
|
(&mut result).into(),
|
||||||
|
item,
|
||||||
|
expr.position(),
|
||||||
|
)?;
|
||||||
|
pos = expr.position();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is::<ImmutableString>(),
|
||||||
|
"interpolated string must be a string"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "no_index"))]
|
#[cfg(not(feature = "no_index"))]
|
||||||
Expr::Array(x, _) => {
|
Expr::Array(x, _) => {
|
||||||
let mut arr = Array::with_capacity(x.len());
|
let mut arr = Array::with_capacity(x.len());
|
||||||
@ -1707,7 +1743,7 @@ impl Engine {
|
|||||||
.flatten(),
|
.flatten(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(Dynamic(Union::Array(Box::new(arr), AccessMode::ReadWrite)))
|
Ok(arr.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "no_object"))]
|
#[cfg(not(feature = "no_object"))]
|
||||||
@ -1718,7 +1754,7 @@ impl Engine {
|
|||||||
.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?
|
.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?
|
||||||
.flatten();
|
.flatten();
|
||||||
}
|
}
|
||||||
Ok(Dynamic(Union::Map(Box::new(map), AccessMode::ReadWrite)))
|
Ok(map.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal function call
|
// Normal function call
|
||||||
@ -2445,7 +2481,7 @@ impl Engine {
|
|||||||
|
|
||||||
if let Some(path) = self
|
if let Some(path) = self
|
||||||
.eval_expr(scope, mods, state, lib, this_ptr, &expr, level)?
|
.eval_expr(scope, mods, state, lib, this_ptr, &expr, level)?
|
||||||
.try_cast::<crate::ImmutableString>()
|
.try_cast::<ImmutableString>()
|
||||||
{
|
{
|
||||||
use crate::ModuleResolver;
|
use crate::ModuleResolver;
|
||||||
|
|
||||||
@ -2481,7 +2517,7 @@ impl Engine {
|
|||||||
|
|
||||||
Ok(Dynamic::UNIT)
|
Ok(Dynamic::UNIT)
|
||||||
} else {
|
} else {
|
||||||
Err(self.make_type_mismatch_err::<crate::ImmutableString>("", expr.position()))
|
Err(self.make_type_mismatch_err::<ImmutableString>("", expr.position()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,11 @@ use crate::engine::{EvalContext, Imports, State};
|
|||||||
use crate::fn_native::{FnCallArgs, SendSync};
|
use crate::fn_native::{FnCallArgs, SendSync};
|
||||||
use crate::fn_register::RegisterNativeFunction;
|
use crate::fn_register::RegisterNativeFunction;
|
||||||
use crate::optimize::OptimizationLevel;
|
use crate::optimize::OptimizationLevel;
|
||||||
|
use crate::parser::ParseState;
|
||||||
use crate::stdlib::{
|
use crate::stdlib::{
|
||||||
any::{type_name, TypeId},
|
any::{type_name, TypeId},
|
||||||
boxed::Box,
|
boxed::Box,
|
||||||
|
num::NonZeroUsize,
|
||||||
string::String,
|
string::String,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -1156,8 +1158,22 @@ impl Engine {
|
|||||||
scripts: &[&str],
|
scripts: &[&str],
|
||||||
optimization_level: OptimizationLevel,
|
optimization_level: OptimizationLevel,
|
||||||
) -> Result<AST, ParseError> {
|
) -> Result<AST, ParseError> {
|
||||||
let stream = self.lex_raw(scripts, None);
|
let (stream, buffer) = self.lex_raw(scripts, None);
|
||||||
self.parse(&mut stream.peekable(), scope, optimization_level)
|
let mut state = ParseState::new(
|
||||||
|
self,
|
||||||
|
buffer,
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
NonZeroUsize::new(self.max_expr_depth()),
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
#[cfg(not(feature = "no_function"))]
|
||||||
|
NonZeroUsize::new(self.max_function_expr_depth()),
|
||||||
|
);
|
||||||
|
self.parse(
|
||||||
|
&mut stream.peekable(),
|
||||||
|
&mut state,
|
||||||
|
scope,
|
||||||
|
optimization_level,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/// Read the contents of a file into a string.
|
/// Read the contents of a file into a string.
|
||||||
#[cfg(not(feature = "no_std"))]
|
#[cfg(not(feature = "no_std"))]
|
||||||
@ -1331,7 +1347,7 @@ impl Engine {
|
|||||||
.into());
|
.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream = self.lex_raw(
|
let (stream, buffer) = self.lex_raw(
|
||||||
&scripts,
|
&scripts,
|
||||||
Some(if has_null {
|
Some(if has_null {
|
||||||
|token| match token {
|
|token| match token {
|
||||||
@ -1344,8 +1360,22 @@ impl Engine {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let ast =
|
let mut state = ParseState::new(
|
||||||
self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?;
|
self,
|
||||||
|
buffer,
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
NonZeroUsize::new(self.max_expr_depth()),
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
#[cfg(not(feature = "no_function"))]
|
||||||
|
NonZeroUsize::new(self.max_function_expr_depth()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ast = self.parse_global_expr(
|
||||||
|
&mut stream.peekable(),
|
||||||
|
&mut state,
|
||||||
|
&scope,
|
||||||
|
OptimizationLevel::None,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Handle null - map to ()
|
// Handle null - map to ()
|
||||||
if has_null {
|
if has_null {
|
||||||
@ -1424,10 +1454,19 @@ impl Engine {
|
|||||||
script: &str,
|
script: &str,
|
||||||
) -> Result<AST, ParseError> {
|
) -> Result<AST, ParseError> {
|
||||||
let scripts = [script];
|
let scripts = [script];
|
||||||
let stream = self.lex_raw(&scripts, None);
|
let (stream, buffer) = self.lex_raw(&scripts, None);
|
||||||
|
|
||||||
let mut peekable = stream.peekable();
|
let mut peekable = stream.peekable();
|
||||||
self.parse_global_expr(&mut peekable, scope, self.optimization_level)
|
let mut state = ParseState::new(
|
||||||
|
self,
|
||||||
|
buffer,
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
NonZeroUsize::new(self.max_expr_depth()),
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
#[cfg(not(feature = "no_function"))]
|
||||||
|
NonZeroUsize::new(self.max_function_expr_depth()),
|
||||||
|
);
|
||||||
|
self.parse_global_expr(&mut peekable, &mut state, scope, self.optimization_level)
|
||||||
}
|
}
|
||||||
/// Evaluate a script file.
|
/// Evaluate a script file.
|
||||||
///
|
///
|
||||||
@ -1585,10 +1624,24 @@ impl Engine {
|
|||||||
script: &str,
|
script: &str,
|
||||||
) -> Result<T, Box<EvalAltResult>> {
|
) -> Result<T, Box<EvalAltResult>> {
|
||||||
let scripts = [script];
|
let scripts = [script];
|
||||||
let stream = self.lex_raw(&scripts, None);
|
let (stream, buffer) = self.lex_raw(&scripts, None);
|
||||||
|
let mut state = ParseState::new(
|
||||||
|
self,
|
||||||
|
buffer,
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
NonZeroUsize::new(self.max_expr_depth()),
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
#[cfg(not(feature = "no_function"))]
|
||||||
|
NonZeroUsize::new(self.max_function_expr_depth()),
|
||||||
|
);
|
||||||
|
|
||||||
// No need to optimize a lone expression
|
// No need to optimize a lone expression
|
||||||
let ast = self.parse_global_expr(&mut stream.peekable(), scope, OptimizationLevel::None)?;
|
let ast = self.parse_global_expr(
|
||||||
|
&mut stream.peekable(),
|
||||||
|
&mut state,
|
||||||
|
scope,
|
||||||
|
OptimizationLevel::None,
|
||||||
|
)?;
|
||||||
|
|
||||||
self.eval_ast_with_scope(scope, &ast)
|
self.eval_ast_with_scope(scope, &ast)
|
||||||
}
|
}
|
||||||
@ -1726,8 +1779,24 @@ impl Engine {
|
|||||||
script: &str,
|
script: &str,
|
||||||
) -> Result<(), Box<EvalAltResult>> {
|
) -> Result<(), Box<EvalAltResult>> {
|
||||||
let scripts = [script];
|
let scripts = [script];
|
||||||
let stream = self.lex_raw(&scripts, None);
|
let (stream, buffer) = self.lex_raw(&scripts, None);
|
||||||
let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?;
|
let mut state = ParseState::new(
|
||||||
|
self,
|
||||||
|
buffer,
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
NonZeroUsize::new(self.max_expr_depth()),
|
||||||
|
#[cfg(not(feature = "unchecked"))]
|
||||||
|
#[cfg(not(feature = "no_function"))]
|
||||||
|
NonZeroUsize::new(self.max_function_expr_depth()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ast = self.parse(
|
||||||
|
&mut stream.peekable(),
|
||||||
|
&mut state,
|
||||||
|
scope,
|
||||||
|
self.optimization_level,
|
||||||
|
)?;
|
||||||
|
|
||||||
self.consume_ast_with_scope(scope, &ast)
|
self.consume_ast_with_scope(scope, &ast)
|
||||||
}
|
}
|
||||||
/// Evaluate an AST, but throw away the result and only return error (if any).
|
/// Evaluate an AST, but throw away the result and only return error (if any).
|
||||||
|
@ -609,6 +609,16 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) {
|
|||||||
match expr {
|
match expr {
|
||||||
// {}
|
// {}
|
||||||
Expr::Stmt(x) if x.statements.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.pos) }
|
Expr::Stmt(x) if x.statements.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.pos) }
|
||||||
|
// { Stmt(Expr) }
|
||||||
|
Expr::Stmt(x) if x.statements.len() == 1 && x.statements[0].is_pure() && matches!(x.statements[0], Stmt::Expr(_)) =>
|
||||||
|
{
|
||||||
|
state.set_dirty();
|
||||||
|
if let Stmt::Expr(e) = mem::take(&mut x.statements[0]) {
|
||||||
|
*expr = e;
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
// { stmt; ... } - do not count promotion as dirty because it gets turned back into an array
|
// { stmt; ... } - do not count promotion as dirty because it gets turned back into an array
|
||||||
Expr::Stmt(x) => x.statements = optimize_stmt_block(mem::take(&mut x.statements).into_vec(), state, true, true, false).into(),
|
Expr::Stmt(x) => x.statements = optimize_stmt_block(mem::take(&mut x.statements).into_vec(), state, true, true, false).into(),
|
||||||
// lhs.rhs
|
// lhs.rhs
|
||||||
@ -664,6 +674,59 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) {
|
|||||||
// lhs[rhs]
|
// lhs[rhs]
|
||||||
(lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); }
|
(lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); }
|
||||||
},
|
},
|
||||||
|
// ``
|
||||||
|
Expr::InterpolatedString(x) if x.is_empty() => {
|
||||||
|
state.set_dirty();
|
||||||
|
*expr = Expr::StringConstant(state.engine.empty_string.clone(), Position::NONE);
|
||||||
|
}
|
||||||
|
// `...`
|
||||||
|
Expr::InterpolatedString(x) if x.len() == 1 && matches!(x[0], Expr::StringConstant(_, _)) => {
|
||||||
|
state.set_dirty();
|
||||||
|
*expr = mem::take(&mut x[0]);
|
||||||
|
}
|
||||||
|
// `... ${ ... } ...`
|
||||||
|
Expr::InterpolatedString(x) => {
|
||||||
|
x.iter_mut().for_each(|expr| optimize_expr(expr, state));
|
||||||
|
|
||||||
|
let mut n= 0;
|
||||||
|
|
||||||
|
// Merge consecutive strings
|
||||||
|
while n < x.len()-1 {
|
||||||
|
match (mem::take(&mut x[n]), mem::take(&mut x[n+1])) {
|
||||||
|
(Expr::StringConstant(mut s1, pos), Expr::StringConstant(s2, _)) => {
|
||||||
|
s1 += s2;
|
||||||
|
x[n] = Expr::StringConstant(s1, pos);
|
||||||
|
x.remove(n+1);
|
||||||
|
state.set_dirty();
|
||||||
|
}
|
||||||
|
(expr1, Expr::Unit(_)) => {
|
||||||
|
x[n] = expr1;
|
||||||
|
x.remove(n+1);
|
||||||
|
state.set_dirty();
|
||||||
|
}
|
||||||
|
(Expr::Unit(_), expr2) => {
|
||||||
|
x[n+1] = expr2;
|
||||||
|
x.remove(n);
|
||||||
|
state.set_dirty();
|
||||||
|
}
|
||||||
|
(expr1, Expr::StringConstant(s, _)) if s.is_empty() => {
|
||||||
|
x[n] = expr1;
|
||||||
|
x.remove(n+1);
|
||||||
|
state.set_dirty();
|
||||||
|
}
|
||||||
|
(Expr::StringConstant(s, _), expr2) if s.is_empty()=> {
|
||||||
|
x[n+1] = expr2;
|
||||||
|
x.remove(n);
|
||||||
|
state.set_dirty();
|
||||||
|
}
|
||||||
|
(expr1, expr2) => {
|
||||||
|
x[n] = expr1;
|
||||||
|
x[n+1] = expr2;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// [ constant .. ]
|
// [ constant .. ]
|
||||||
#[cfg(not(feature = "no_index"))]
|
#[cfg(not(feature = "no_index"))]
|
||||||
Expr::Array(_, _) if expr.is_constant() => {
|
Expr::Array(_, _) if expr.is_constant() => {
|
||||||
|
@ -23,18 +23,49 @@ mod string_functions {
|
|||||||
use crate::ImmutableString;
|
use crate::ImmutableString;
|
||||||
|
|
||||||
#[rhai_fn(name = "+", name = "append")]
|
#[rhai_fn(name = "+", name = "append")]
|
||||||
pub fn add_append(ctx: NativeCallContext, string: &str, mut item: Dynamic) -> ImmutableString {
|
pub fn add_append(
|
||||||
|
ctx: NativeCallContext,
|
||||||
|
string: ImmutableString,
|
||||||
|
mut item: Dynamic,
|
||||||
|
) -> ImmutableString {
|
||||||
let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item);
|
let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item);
|
||||||
|
|
||||||
|
if s.is_empty() {
|
||||||
|
string
|
||||||
|
} else {
|
||||||
format!("{}{}", string, s).into()
|
format!("{}{}", string, s).into()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#[rhai_fn(name = "+=")]
|
||||||
|
pub fn append(ctx: NativeCallContext, string: &mut ImmutableString, mut item: Dynamic) {
|
||||||
|
let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item);
|
||||||
|
|
||||||
|
if !s.is_empty() {
|
||||||
|
string.make_mut().push_str(&s);
|
||||||
|
}
|
||||||
|
}
|
||||||
#[rhai_fn(name = "+", pure)]
|
#[rhai_fn(name = "+", pure)]
|
||||||
pub fn add_prepend(
|
pub fn add_prepend(
|
||||||
ctx: NativeCallContext,
|
ctx: NativeCallContext,
|
||||||
item: &mut Dynamic,
|
item: &mut Dynamic,
|
||||||
string: &str,
|
string: &str,
|
||||||
) -> ImmutableString {
|
) -> ImmutableString {
|
||||||
let s = print_with_func(FUNC_TO_STRING, &ctx, item);
|
let mut s = print_with_func(FUNC_TO_STRING, &ctx, item);
|
||||||
format!("{}{}", s, string).into()
|
|
||||||
|
if string.is_empty() {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
s.make_mut().push_str(string);
|
||||||
|
s.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[rhai_fn(name = "+=")]
|
||||||
|
pub fn prepend(ctx: NativeCallContext, item: &mut Dynamic, string: &str) {
|
||||||
|
let mut s = print_with_func(FUNC_TO_STRING, &ctx, item);
|
||||||
|
|
||||||
|
if !string.is_empty() {
|
||||||
|
s.make_mut().push_str(string);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rhai_fn(name = "+")]
|
#[rhai_fn(name = "+")]
|
||||||
@ -137,13 +168,18 @@ mod string_functions {
|
|||||||
.unwrap_or(-1 as INT)
|
.unwrap_or(-1 as INT)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sub_string(string: &str, start: INT, len: INT) -> ImmutableString {
|
pub fn sub_string(
|
||||||
|
ctx: NativeCallContext,
|
||||||
|
string: &str,
|
||||||
|
start: INT,
|
||||||
|
len: INT,
|
||||||
|
) -> ImmutableString {
|
||||||
let offset = if string.is_empty() || len <= 0 {
|
let offset = if string.is_empty() || len <= 0 {
|
||||||
return "".to_string().into();
|
return ctx.engine().empty_string.clone().into();
|
||||||
} else if start < 0 {
|
} else if start < 0 {
|
||||||
0
|
0
|
||||||
} else if start as usize >= string.chars().count() {
|
} else if start as usize >= string.chars().count() {
|
||||||
return "".to_string().into();
|
return ctx.engine().empty_string.clone().into();
|
||||||
} else {
|
} else {
|
||||||
start as usize
|
start as usize
|
||||||
};
|
};
|
||||||
@ -165,9 +201,13 @@ mod string_functions {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
#[rhai_fn(name = "sub_string")]
|
#[rhai_fn(name = "sub_string")]
|
||||||
pub fn sub_string_starting_from(string: &str, start: INT) -> ImmutableString {
|
pub fn sub_string_starting_from(
|
||||||
|
ctx: NativeCallContext,
|
||||||
|
string: &str,
|
||||||
|
start: INT,
|
||||||
|
) -> ImmutableString {
|
||||||
let len = string.len() as INT;
|
let len = string.len() as INT;
|
||||||
sub_string(string, start, len)
|
sub_string(ctx, string, start, len)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rhai_fn(name = "crop")]
|
#[rhai_fn(name = "crop")]
|
||||||
@ -341,9 +381,9 @@ mod string_functions {
|
|||||||
string.chars().map(Into::<Dynamic>::into).collect()
|
string.chars().map(Into::<Dynamic>::into).collect()
|
||||||
}
|
}
|
||||||
#[rhai_fn(name = "split")]
|
#[rhai_fn(name = "split")]
|
||||||
pub fn split_at(string: ImmutableString, start: INT) -> Array {
|
pub fn split_at(ctx: NativeCallContext, string: ImmutableString, start: INT) -> Array {
|
||||||
if start <= 0 {
|
if start <= 0 {
|
||||||
vec!["".into(), string.into()]
|
vec![ctx.engine().empty_string.clone().into(), string.into()]
|
||||||
} else {
|
} else {
|
||||||
let prefix: String = string.chars().take(start as usize).collect();
|
let prefix: String = string.chars().take(start as usize).collect();
|
||||||
let prefix_len = prefix.len();
|
let prefix_len = prefix.len();
|
||||||
|
117
src/parser.rs
117
src/parser.rs
@ -11,6 +11,7 @@ use crate::optimize::optimize_into_ast;
|
|||||||
use crate::optimize::OptimizationLevel;
|
use crate::optimize::OptimizationLevel;
|
||||||
use crate::stdlib::{
|
use crate::stdlib::{
|
||||||
boxed::Box,
|
boxed::Box,
|
||||||
|
cell::Cell,
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
format,
|
format,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
@ -40,9 +41,11 @@ type FunctionsLib = BTreeMap<u64, Shared<ScriptFnDef>>;
|
|||||||
|
|
||||||
/// A type that encapsulates the current state of the parser.
|
/// A type that encapsulates the current state of the parser.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ParseState<'e> {
|
pub struct ParseState<'e> {
|
||||||
/// Reference to the scripting [`Engine`].
|
/// Reference to the scripting [`Engine`].
|
||||||
engine: &'e Engine,
|
engine: &'e Engine,
|
||||||
|
/// Input stream buffer containing the next character to read.
|
||||||
|
buffer: Shared<Cell<Option<char>>>,
|
||||||
/// Interned strings.
|
/// Interned strings.
|
||||||
interned_strings: IdentifierBuilder,
|
interned_strings: IdentifierBuilder,
|
||||||
/// Encapsulates a local stack with variable names to simulate an actual runtime scope.
|
/// Encapsulates a local stack with variable names to simulate an actual runtime scope.
|
||||||
@ -75,6 +78,7 @@ impl<'e> ParseState<'e> {
|
|||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
engine: &'e Engine,
|
engine: &'e Engine,
|
||||||
|
buffer: Shared<Cell<Option<char>>>,
|
||||||
#[cfg(not(feature = "unchecked"))] max_expr_depth: Option<NonZeroUsize>,
|
#[cfg(not(feature = "unchecked"))] max_expr_depth: Option<NonZeroUsize>,
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
#[cfg(not(feature = "no_function"))]
|
#[cfg(not(feature = "no_function"))]
|
||||||
@ -82,6 +86,7 @@ impl<'e> ParseState<'e> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
engine,
|
engine,
|
||||||
|
buffer,
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
max_expr_depth,
|
max_expr_depth,
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
@ -458,7 +463,7 @@ fn parse_index_chain(
|
|||||||
.into_err(*pos))
|
.into_err(*pos))
|
||||||
}
|
}
|
||||||
Expr::IntegerConstant(_, pos) => match lhs {
|
Expr::IntegerConstant(_, pos) => match lhs {
|
||||||
Expr::Array(_, _) | Expr::StringConstant(_, _) => (),
|
Expr::Array(_, _) | Expr::StringConstant(_, _) | Expr::InterpolatedString(_) => (),
|
||||||
|
|
||||||
Expr::Map(_, _) => {
|
Expr::Map(_, _) => {
|
||||||
return Err(PERR::MalformedIndexExpr(
|
return Err(PERR::MalformedIndexExpr(
|
||||||
@ -490,14 +495,14 @@ fn parse_index_chain(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// lhs[string]
|
// lhs[string]
|
||||||
Expr::StringConstant(_, pos) => match lhs {
|
Expr::StringConstant(_, _) | Expr::InterpolatedString(_) => match lhs {
|
||||||
Expr::Map(_, _) => (),
|
Expr::Map(_, _) => (),
|
||||||
|
|
||||||
Expr::Array(_, _) | Expr::StringConstant(_, _) => {
|
Expr::Array(_, _) | Expr::StringConstant(_, _) | Expr::InterpolatedString(_) => {
|
||||||
return Err(PERR::MalformedIndexExpr(
|
return Err(PERR::MalformedIndexExpr(
|
||||||
"Array or string expects numeric index, not a string".into(),
|
"Array or string expects numeric index, not a string".into(),
|
||||||
)
|
)
|
||||||
.into_err(*pos))
|
.into_err(idx_expr.position()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "no_float"))]
|
#[cfg(not(feature = "no_float"))]
|
||||||
@ -979,6 +984,7 @@ fn parse_primary(
|
|||||||
Token::Pipe | Token::Or if settings.allow_anonymous_fn => {
|
Token::Pipe | Token::Or if settings.allow_anonymous_fn => {
|
||||||
let mut new_state = ParseState::new(
|
let mut new_state = ParseState::new(
|
||||||
state.engine,
|
state.engine,
|
||||||
|
state.buffer.clone(),
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
state.max_function_expr_depth,
|
state.max_function_expr_depth,
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
@ -1010,6 +1016,50 @@ fn parse_primary(
|
|||||||
expr
|
expr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interpolated string
|
||||||
|
Token::InterpolatedString(_) => {
|
||||||
|
let mut segments: StaticVec<Expr> = Default::default();
|
||||||
|
|
||||||
|
if let (Token::InterpolatedString(s), pos) = input.next().unwrap() {
|
||||||
|
segments.push(Expr::StringConstant(s.into(), pos));
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let expr = match parse_block(input, state, lib, settings.level_up())? {
|
||||||
|
block @ Stmt::Block(_, _) => Expr::Stmt(Box::new(block.into())),
|
||||||
|
stmt => unreachable!("expecting Stmt::Block, but gets {:?}", stmt),
|
||||||
|
};
|
||||||
|
segments.push(expr);
|
||||||
|
|
||||||
|
// Make sure to parse the following as text
|
||||||
|
state.buffer.set(Some('`'));
|
||||||
|
|
||||||
|
match input.next().unwrap() {
|
||||||
|
(Token::StringConstant(s), pos) => {
|
||||||
|
if !s.is_empty() {
|
||||||
|
segments.push(Expr::StringConstant(s.into(), pos));
|
||||||
|
}
|
||||||
|
// End the interpolated string if it is terminated by a back-tick.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(Token::InterpolatedString(s), pos) => {
|
||||||
|
if !s.is_empty() {
|
||||||
|
segments.push(Expr::StringConstant(s.into(), pos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(token, _) => unreachable!(
|
||||||
|
"expected a string within an interpolated string literal, but gets {:?}",
|
||||||
|
token
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Interpolated string: {:?}", segments);
|
||||||
|
Expr::InterpolatedString(Box::new(segments))
|
||||||
|
}
|
||||||
|
|
||||||
// Array literal
|
// Array literal
|
||||||
#[cfg(not(feature = "no_index"))]
|
#[cfg(not(feature = "no_index"))]
|
||||||
Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?,
|
Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?,
|
||||||
@ -1020,8 +1070,8 @@ fn parse_primary(
|
|||||||
|
|
||||||
// Identifier
|
// Identifier
|
||||||
Token::Identifier(_) => {
|
Token::Identifier(_) => {
|
||||||
let s = match input.next().unwrap().0 {
|
let s = match input.next().unwrap() {
|
||||||
Token::Identifier(s) => s,
|
(Token::Identifier(s), _) => s,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1067,8 +1117,8 @@ fn parse_primary(
|
|||||||
|
|
||||||
// Reserved keyword or symbol
|
// Reserved keyword or symbol
|
||||||
Token::Reserved(_) => {
|
Token::Reserved(_) => {
|
||||||
let s = match input.next().unwrap().0 {
|
let s = match input.next().unwrap() {
|
||||||
Token::Reserved(s) => s,
|
(Token::Reserved(s), _) => s,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1101,14 +1151,10 @@ fn parse_primary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token::LexError(_) => {
|
Token::LexError(_) => match input.next().unwrap() {
|
||||||
let err = match input.next().unwrap().0 {
|
(Token::LexError(err), _) => return Err(err.into_err(settings.pos)),
|
||||||
Token::LexError(err) => err,
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
},
|
||||||
|
|
||||||
return Err(err.into_err(settings.pos));
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(LexError::UnexpectedInput(token.syntax().to_string()).into_err(settings.pos))
|
return Err(LexError::UnexpectedInput(token.syntax().to_string()).into_err(settings.pos))
|
||||||
@ -1374,13 +1420,7 @@ fn make_assignment_stmt<'a>(
|
|||||||
let op_info = if op.is_empty() {
|
let op_info = if op.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let op2 = &op[..op.len() - 1]; // extract operator without =
|
Some(OpAssignment::new(op))
|
||||||
|
|
||||||
Some(OpAssignment {
|
|
||||||
hash_op_assign: calc_fn_hash(empty(), &op, 2),
|
|
||||||
hash_op: calc_fn_hash(empty(), op2, 2),
|
|
||||||
op,
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &lhs {
|
match &lhs {
|
||||||
@ -1460,7 +1500,7 @@ fn parse_op_assignment_stmt(
|
|||||||
settings.pos = *token_pos;
|
settings.pos = *token_pos;
|
||||||
|
|
||||||
let op = match token {
|
let op = match token {
|
||||||
Token::Equals => "".into(),
|
Token::Equals => "",
|
||||||
|
|
||||||
Token::PlusAssign
|
Token::PlusAssign
|
||||||
| Token::MinusAssign
|
| Token::MinusAssign
|
||||||
@ -1797,9 +1837,10 @@ fn parse_custom_syntax(
|
|||||||
// Add enough empty variable names to the stack.
|
// Add enough empty variable names to the stack.
|
||||||
// Empty variable names act as a barrier so earlier variables will not be matched.
|
// Empty variable names act as a barrier so earlier variables will not be matched.
|
||||||
// Variable searches stop at the first empty variable name.
|
// Variable searches stop at the first empty variable name.
|
||||||
|
let empty = state.get_identifier("");
|
||||||
state.stack.resize(
|
state.stack.resize(
|
||||||
state.stack.len() + delta as usize,
|
state.stack.len() + delta as usize,
|
||||||
("".into(), AccessMode::ReadWrite),
|
(empty, AccessMode::ReadWrite),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
delta if delta < 0 && state.stack.len() <= delta.abs() as usize => state.stack.clear(),
|
delta if delta < 0 && state.stack.len() <= delta.abs() as usize => state.stack.clear(),
|
||||||
@ -2502,6 +2543,7 @@ fn parse_stmt(
|
|||||||
(Token::Fn, pos) => {
|
(Token::Fn, pos) => {
|
||||||
let mut new_state = ParseState::new(
|
let mut new_state = ParseState::new(
|
||||||
state.engine,
|
state.engine,
|
||||||
|
state.buffer.clone(),
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
state.max_function_expr_depth,
|
state.max_function_expr_depth,
|
||||||
#[cfg(not(feature = "unchecked"))]
|
#[cfg(not(feature = "unchecked"))]
|
||||||
@ -2930,18 +2972,11 @@ impl Engine {
|
|||||||
pub(crate) fn parse_global_expr(
|
pub(crate) fn parse_global_expr(
|
||||||
&self,
|
&self,
|
||||||
input: &mut TokenStream,
|
input: &mut TokenStream,
|
||||||
|
state: &mut ParseState,
|
||||||
scope: &Scope,
|
scope: &Scope,
|
||||||
optimization_level: OptimizationLevel,
|
optimization_level: OptimizationLevel,
|
||||||
) -> Result<AST, ParseError> {
|
) -> Result<AST, ParseError> {
|
||||||
let mut functions = Default::default();
|
let mut functions = Default::default();
|
||||||
let mut state = ParseState::new(
|
|
||||||
self,
|
|
||||||
#[cfg(not(feature = "unchecked"))]
|
|
||||||
NonZeroUsize::new(self.max_expr_depth()),
|
|
||||||
#[cfg(not(feature = "unchecked"))]
|
|
||||||
#[cfg(not(feature = "no_function"))]
|
|
||||||
NonZeroUsize::new(self.max_function_expr_depth()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let settings = ParseSettings {
|
let settings = ParseSettings {
|
||||||
allow_if_expr: false,
|
allow_if_expr: false,
|
||||||
@ -2954,7 +2989,7 @@ impl Engine {
|
|||||||
level: 0,
|
level: 0,
|
||||||
pos: Position::NONE,
|
pos: Position::NONE,
|
||||||
};
|
};
|
||||||
let expr = parse_expr(input, &mut state, &mut functions, settings)?;
|
let expr = parse_expr(input, state, &mut functions, settings)?;
|
||||||
|
|
||||||
assert!(functions.is_empty());
|
assert!(functions.is_empty());
|
||||||
|
|
||||||
@ -2978,17 +3013,10 @@ impl Engine {
|
|||||||
fn parse_global_level(
|
fn parse_global_level(
|
||||||
&self,
|
&self,
|
||||||
input: &mut TokenStream,
|
input: &mut TokenStream,
|
||||||
|
state: &mut ParseState,
|
||||||
) -> Result<(Vec<Stmt>, Vec<Shared<ScriptFnDef>>), ParseError> {
|
) -> Result<(Vec<Stmt>, Vec<Shared<ScriptFnDef>>), ParseError> {
|
||||||
let mut statements = Vec::with_capacity(16);
|
let mut statements = Vec::with_capacity(16);
|
||||||
let mut functions = BTreeMap::new();
|
let mut functions = BTreeMap::new();
|
||||||
let mut state = ParseState::new(
|
|
||||||
self,
|
|
||||||
#[cfg(not(feature = "unchecked"))]
|
|
||||||
NonZeroUsize::new(self.max_expr_depth()),
|
|
||||||
#[cfg(not(feature = "unchecked"))]
|
|
||||||
#[cfg(not(feature = "no_function"))]
|
|
||||||
NonZeroUsize::new(self.max_function_expr_depth()),
|
|
||||||
);
|
|
||||||
|
|
||||||
while !input.peek().unwrap().0.is_eof() {
|
while !input.peek().unwrap().0.is_eof() {
|
||||||
let settings = ParseSettings {
|
let settings = ParseSettings {
|
||||||
@ -3003,7 +3031,7 @@ impl Engine {
|
|||||||
pos: Position::NONE,
|
pos: Position::NONE,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stmt = parse_stmt(input, &mut state, &mut functions, settings)?;
|
let stmt = parse_stmt(input, state, &mut functions, settings)?;
|
||||||
|
|
||||||
if stmt.is_noop() {
|
if stmt.is_noop() {
|
||||||
continue;
|
continue;
|
||||||
@ -3046,10 +3074,11 @@ impl Engine {
|
|||||||
pub(crate) fn parse(
|
pub(crate) fn parse(
|
||||||
&self,
|
&self,
|
||||||
input: &mut TokenStream,
|
input: &mut TokenStream,
|
||||||
|
state: &mut ParseState,
|
||||||
scope: &Scope,
|
scope: &Scope,
|
||||||
optimization_level: OptimizationLevel,
|
optimization_level: OptimizationLevel,
|
||||||
) -> Result<AST, ParseError> {
|
) -> Result<AST, ParseError> {
|
||||||
let (statements, lib) = self.parse_global_level(input)?;
|
let (statements, lib) = self.parse_global_level(input, state)?;
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
// Optimize AST
|
// Optimize AST
|
||||||
|
74
src/token.rs
74
src/token.rs
@ -1,11 +1,14 @@
|
|||||||
//! Main module defining the lexer and parser.
|
//! Main module defining the lexer and parser.
|
||||||
|
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
use crate::engine::{
|
use crate::engine::{
|
||||||
Precedence, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL,
|
Precedence, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL,
|
||||||
KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF,
|
KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF,
|
||||||
};
|
};
|
||||||
use crate::stdlib::{
|
use crate::stdlib::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
cell::Cell,
|
||||||
char, fmt, format,
|
char, fmt, format,
|
||||||
iter::Peekable,
|
iter::Peekable,
|
||||||
num::NonZeroUsize,
|
num::NonZeroUsize,
|
||||||
@ -13,7 +16,7 @@ use crate::stdlib::{
|
|||||||
str::{Chars, FromStr},
|
str::{Chars, FromStr},
|
||||||
string::{String, ToString},
|
string::{String, ToString},
|
||||||
};
|
};
|
||||||
use crate::{Engine, LexError, StaticVec, INT};
|
use crate::{Engine, LexError, Shared, StaticVec, INT};
|
||||||
|
|
||||||
#[cfg(not(feature = "no_float"))]
|
#[cfg(not(feature = "no_float"))]
|
||||||
use crate::ast::FloatWrapper;
|
use crate::ast::FloatWrapper;
|
||||||
@ -209,6 +212,8 @@ pub enum Token {
|
|||||||
CharConstant(char),
|
CharConstant(char),
|
||||||
/// A string constant.
|
/// A string constant.
|
||||||
StringConstant(String),
|
StringConstant(String),
|
||||||
|
/// An interpolated string.
|
||||||
|
InterpolatedString(String),
|
||||||
/// `{`
|
/// `{`
|
||||||
LeftBrace,
|
LeftBrace,
|
||||||
/// `}`
|
/// `}`
|
||||||
@ -485,6 +490,7 @@ impl Token {
|
|||||||
#[cfg(feature = "decimal")]
|
#[cfg(feature = "decimal")]
|
||||||
DecimalConstant(d) => d.to_string().into(),
|
DecimalConstant(d) => d.to_string().into(),
|
||||||
StringConstant(_) => "string".into(),
|
StringConstant(_) => "string".into(),
|
||||||
|
InterpolatedString(_) => "string".into(),
|
||||||
CharConstant(c) => c.to_string().into(),
|
CharConstant(c) => c.to_string().into(),
|
||||||
Identifier(s) => s.clone().into(),
|
Identifier(s) => s.clone().into(),
|
||||||
Reserved(s) => s.clone().into(),
|
Reserved(s) => s.clone().into(),
|
||||||
@ -855,18 +861,30 @@ pub fn parse_string_literal(
|
|||||||
termination_char: char,
|
termination_char: char,
|
||||||
continuation: bool,
|
continuation: bool,
|
||||||
verbatim: bool,
|
verbatim: bool,
|
||||||
) -> Result<String, (LexError, Position)> {
|
allow_interpolation: bool,
|
||||||
|
) -> Result<(String, bool), (LexError, Position)> {
|
||||||
let mut result: smallvec::SmallVec<[char; 16]> = Default::default();
|
let mut result: smallvec::SmallVec<[char; 16]> = Default::default();
|
||||||
let mut escape: smallvec::SmallVec<[char; 12]> = Default::default();
|
let mut escape: smallvec::SmallVec<[char; 12]> = Default::default();
|
||||||
|
|
||||||
let start = *pos;
|
let start = *pos;
|
||||||
let mut skip_whitespace_until = 0;
|
let mut skip_whitespace_until = 0;
|
||||||
|
let mut interpolated = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let next_char = stream.get_next().ok_or((LERR::UnterminatedString, start))?;
|
let next_char = stream.get_next().ok_or((LERR::UnterminatedString, start))?;
|
||||||
|
|
||||||
pos.advance();
|
pos.advance();
|
||||||
|
|
||||||
|
// String interpolation?
|
||||||
|
if allow_interpolation
|
||||||
|
&& next_char == '$'
|
||||||
|
&& escape.is_empty()
|
||||||
|
&& stream.peek_next().map(|ch| ch == '{').unwrap_or(false)
|
||||||
|
{
|
||||||
|
interpolated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(max) = state.max_string_size {
|
if let Some(max) = state.max_string_size {
|
||||||
if result.len() > max.get() {
|
if result.len() > max.get() {
|
||||||
return Err((LexError::StringTooLong(max.get()), *pos));
|
return Err((LexError::StringTooLong(max.get()), *pos));
|
||||||
@ -1000,7 +1018,7 @@ pub fn parse_string_literal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(s)
|
Ok((s, interpolated))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume the next character.
|
/// Consume the next character.
|
||||||
@ -1296,9 +1314,10 @@ fn get_next_token_inner(
|
|||||||
|
|
||||||
// " - string literal
|
// " - string literal
|
||||||
('"', _) => {
|
('"', _) => {
|
||||||
return parse_string_literal(stream, state, pos, c, true, false).map_or_else(
|
return parse_string_literal(stream, state, pos, c, true, false, false)
|
||||||
|
.map_or_else(
|
||||||
|err| Some((Token::LexError(err.0), err.1)),
|
|err| Some((Token::LexError(err.0), err.1)),
|
||||||
|out| Some((Token::StringConstant(out), start_pos)),
|
|(result, _)| Some((Token::StringConstant(result), start_pos)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// ` - string literal
|
// ` - string literal
|
||||||
@ -1320,9 +1339,15 @@ fn get_next_token_inner(
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
return parse_string_literal(stream, state, pos, c, false, true).map_or_else(
|
return parse_string_literal(stream, state, pos, c, false, true, true).map_or_else(
|
||||||
|err| Some((Token::LexError(err.0), err.1)),
|
|err| Some((Token::LexError(err.0), err.1)),
|
||||||
|out| Some((Token::StringConstant(out), start_pos)),
|
|(result, interpolated)| {
|
||||||
|
if interpolated {
|
||||||
|
Some((Token::InterpolatedString(result), start_pos))
|
||||||
|
} else {
|
||||||
|
Some((Token::StringConstant(result), start_pos))
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1335,9 +1360,9 @@ fn get_next_token_inner(
|
|||||||
}
|
}
|
||||||
('\'', _) => {
|
('\'', _) => {
|
||||||
return Some(
|
return Some(
|
||||||
parse_string_literal(stream, state, pos, c, false, false).map_or_else(
|
parse_string_literal(stream, state, pos, c, false, false, false).map_or_else(
|
||||||
|err| (Token::LexError(err.0), err.1),
|
|err| (Token::LexError(err.0), err.1),
|
||||||
|result| {
|
|(result, _)| {
|
||||||
let mut chars = result.chars();
|
let mut chars = result.chars();
|
||||||
let first = chars.next().unwrap();
|
let first = chars.next().unwrap();
|
||||||
|
|
||||||
@ -1765,6 +1790,10 @@ pub struct MultiInputsStream<'a> {
|
|||||||
impl InputStream for MultiInputsStream<'_> {
|
impl InputStream for MultiInputsStream<'_> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn unget(&mut self, ch: char) {
|
fn unget(&mut self, ch: char) {
|
||||||
|
if self.buf.is_some() {
|
||||||
|
panic!("cannot unget two characters in a row");
|
||||||
|
}
|
||||||
|
|
||||||
self.buf = Some(ch);
|
self.buf = Some(ch);
|
||||||
}
|
}
|
||||||
fn get_next(&mut self) -> Option<char> {
|
fn get_next(&mut self) -> Option<char> {
|
||||||
@ -1813,6 +1842,8 @@ pub struct TokenIterator<'a> {
|
|||||||
state: TokenizeState,
|
state: TokenizeState,
|
||||||
/// Current position.
|
/// Current position.
|
||||||
pos: Position,
|
pos: Position,
|
||||||
|
/// Buffer containing the next character to read, if any.
|
||||||
|
buffer: Shared<Cell<Option<char>>>,
|
||||||
/// Input character stream.
|
/// Input character stream.
|
||||||
stream: MultiInputsStream<'a>,
|
stream: MultiInputsStream<'a>,
|
||||||
/// A processor function that maps a token to another.
|
/// A processor function that maps a token to another.
|
||||||
@ -1823,6 +1854,11 @@ impl<'a> Iterator for TokenIterator<'a> {
|
|||||||
type Item = (Token, Position);
|
type Item = (Token, Position);
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if let Some(ch) = self.buffer.take() {
|
||||||
|
self.stream.unget(ch);
|
||||||
|
self.pos.rewind();
|
||||||
|
}
|
||||||
|
|
||||||
let (token, pos) = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) {
|
let (token, pos) = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) {
|
||||||
// {EOF}
|
// {EOF}
|
||||||
None => return None,
|
None => return None,
|
||||||
@ -1901,12 +1937,17 @@ impl<'a> Iterator for TokenIterator<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FusedIterator for TokenIterator<'_> {}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
/// _(INTERNALS)_ Tokenize an input text stream.
|
/// _(INTERNALS)_ Tokenize an input text stream.
|
||||||
/// Exported under the `internals` feature only.
|
/// Exported under the `internals` feature only.
|
||||||
#[cfg(feature = "internals")]
|
#[cfg(feature = "internals")]
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn lex<'a>(&'a self, input: impl IntoIterator<Item = &'a &'a str>) -> TokenIterator<'a> {
|
pub fn lex<'a>(
|
||||||
|
&'a self,
|
||||||
|
input: impl IntoIterator<Item = &'a &'a str>,
|
||||||
|
) -> (TokenIterator<'a>, Shared<Cell<Option<char>>>) {
|
||||||
self.lex_raw(input, None)
|
self.lex_raw(input, None)
|
||||||
}
|
}
|
||||||
/// _(INTERNALS)_ Tokenize an input text stream with a mapping function.
|
/// _(INTERNALS)_ Tokenize an input text stream with a mapping function.
|
||||||
@ -1917,7 +1958,7 @@ impl Engine {
|
|||||||
&'a self,
|
&'a self,
|
||||||
input: impl IntoIterator<Item = &'a &'a str>,
|
input: impl IntoIterator<Item = &'a &'a str>,
|
||||||
map: fn(Token) -> Token,
|
map: fn(Token) -> Token,
|
||||||
) -> TokenIterator<'a> {
|
) -> (TokenIterator<'a>, Shared<Cell<Option<char>>>) {
|
||||||
self.lex_raw(input, Some(map))
|
self.lex_raw(input, Some(map))
|
||||||
}
|
}
|
||||||
/// Tokenize an input text stream with an optional mapping function.
|
/// Tokenize an input text stream with an optional mapping function.
|
||||||
@ -1926,7 +1967,11 @@ impl Engine {
|
|||||||
&'a self,
|
&'a self,
|
||||||
input: impl IntoIterator<Item = &'a &'a str>,
|
input: impl IntoIterator<Item = &'a &'a str>,
|
||||||
map: Option<fn(Token) -> Token>,
|
map: Option<fn(Token) -> Token>,
|
||||||
) -> TokenIterator<'a> {
|
) -> (TokenIterator<'a>, Shared<Cell<Option<char>>>) {
|
||||||
|
let buffer: Shared<Cell<Option<char>>> = Cell::new(None).into();
|
||||||
|
let buffer2 = buffer.clone();
|
||||||
|
|
||||||
|
(
|
||||||
TokenIterator {
|
TokenIterator {
|
||||||
engine: self,
|
engine: self,
|
||||||
state: TokenizeState {
|
state: TokenizeState {
|
||||||
@ -1941,12 +1986,15 @@ impl Engine {
|
|||||||
disable_doc_comments: self.disable_doc_comments,
|
disable_doc_comments: self.disable_doc_comments,
|
||||||
},
|
},
|
||||||
pos: Position::new(1, 0),
|
pos: Position::new(1, 0),
|
||||||
|
buffer,
|
||||||
stream: MultiInputsStream {
|
stream: MultiInputsStream {
|
||||||
buf: None,
|
buf: None,
|
||||||
streams: input.into_iter().map(|s| s.chars().peekable()).collect(),
|
streams: input.into_iter().map(|s| s.chars().peekable()).collect(),
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
map,
|
map,
|
||||||
}
|
},
|
||||||
|
buffer2,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -310,3 +310,62 @@ fn test_string_split() -> Result<(), Box<EvalAltResult>> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_string_interpolated() -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let engine = Engine::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<String>(
|
||||||
|
r"
|
||||||
|
let x = 40;
|
||||||
|
`hello ${x+2} worlds!`
|
||||||
|
"
|
||||||
|
)?,
|
||||||
|
"hello 42 worlds!"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<String>(
|
||||||
|
r"
|
||||||
|
const x = 42;
|
||||||
|
`hello ${x} worlds!`
|
||||||
|
"
|
||||||
|
)?,
|
||||||
|
"hello 42 worlds!"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(engine.eval::<String>("`hello ${}world!`")?, "hello world!");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<String>(
|
||||||
|
r"
|
||||||
|
const x = 42;
|
||||||
|
`${x} worlds!`
|
||||||
|
"
|
||||||
|
)?,
|
||||||
|
"42 worlds!"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<String>(
|
||||||
|
r"
|
||||||
|
const x = 42;
|
||||||
|
`hello ${x}`
|
||||||
|
"
|
||||||
|
)?,
|
||||||
|
"hello 42"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
engine.eval::<String>(
|
||||||
|
r"
|
||||||
|
const x = 20;
|
||||||
|
`hello ${let y = x + 1; `${y * 2}`} worlds!`
|
||||||
|
"
|
||||||
|
)?,
|
||||||
|
"hello 42 worlds!"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user