use crate::core::arithmetic_engine::ArithmeticEngine;
use crate::core::environment::{expr_desc, model_to_string};
use crate::core::eval;
use crate::core::exec::ExecOutcome;
use crate::core::{AssignOp, Expr};
use crate::error::{Result, TemplateError};
use crate::template::TModel;
use std::rc::Rc;
pub struct Assignment {
pub target: String,
pub expr: Expr,
pub op: AssignOp,
pub namespace: Option<Expr>,
}
impl Assignment {
pub fn new(target: String, expr: Expr, op: AssignOp, namespace: Option<Expr>) -> Self {
Assignment {
target,
expr,
op,
namespace,
}
}
pub(crate) fn exec(&self, env: &mut crate::core::Environment) -> Result<ExecOutcome> {
exec_assign(
env,
&self.target,
&self.expr,
&self.op,
self.namespace.as_ref(),
AssignScope::Namespace,
)
}
}
pub(crate) enum AssignScope {
Namespace,
Global,
Local,
}
pub(crate) fn exec_assign(
env: &mut crate::core::Environment,
target: &str,
expr: &crate::core::Expr,
op: &AssignOp,
namespace: Option<&crate::core::Expr>,
scope: AssignScope,
) -> Result<ExecOutcome> {
let target_ns = resolve_target_ns(env, namespace)?;
let value = if *op == AssignOp::Equals {
let v = eval::eval(env, expr)?;
if v.is_nothing() {
if env.settings.classic_compatible {
TModel::from_scalar(String::new())
} else {
return Err(TemplateError::invalid_reference(expr_desc(expr)));
}
} else {
v
}
} else {
let old = match get_old_value(env, target, &target_ns, &scope)? {
Some(old) => old,
None if *op == AssignOp::PlusEq && env.settings.classic_compatible => {
TModel::from_scalar(String::new())
}
None => {
let scope_desc = match scope {
AssignScope::Namespace => "template namespace",
AssignScope::Global => "global scope",
AssignScope::Local => "local scope",
};
let mut msg = format!(
"The target variable of the assignment, \"{target}\", was null or missing in the {scope_desc}, and the \"{}\" operator must get its value from there before assigning to it.",
assign_op_str(op)
);
if target.starts_with('$') {
msg.push_str("\n\n----\nTip: Variable references must not start with \"$\", unless the \"$\" is really part of the variable name.\n----");
}
return Err(TemplateError::misc(msg));
}
};
match op {
AssignOp::PlusEq => {
let new = eval::eval(env, expr)?;
let new = if new.is_nothing() {
if env.settings.classic_compatible {
TModel::from_scalar(String::new())
} else {
return Err(TemplateError::invalid_reference(expr_desc(expr)));
}
} else {
new
};
eval_add_concat(env, &old, &new)?
}
AssignOp::PlusPlus => {
let n = old
.get_number()
.map_err(|_| assign_non_number_err(target, &old))?;
let one = crate::value::TNumber::Int(1);
let engine = crate::core::BigDecimalEngine::default();
TModel::from_number(engine.add(&n, &one)?)
}
AssignOp::MinusMinus => {
let n = old
.get_number()
.map_err(|_| assign_non_number_err(target, &old))?;
let one = crate::value::TNumber::Int(1);
let engine = crate::core::BigDecimalEngine::default();
TModel::from_number(engine.sub(&n, &one)?)
}
AssignOp::MinusEq | AssignOp::TimesEq | AssignOp::DivideEq | AssignOp::ModuloEq => {
let l = old
.get_number()
.map_err(|_| assign_non_number_err(target, &old))?;
let rm = eval::eval(env, expr)?;
if rm.is_nothing() {
return Err(TemplateError::invalid_reference(expr_desc(expr)));
}
let r = rm.get_number().map_err(|_| {
TemplateError::misc(format!(
"For \"#assign\" assignment source: Expected a number, but this has evaluated to a string: ==> {}",
assign_source_desc(expr)
))
})?;
let engine = crate::core::BigDecimalEngine::default();
TModel::from_number(match op {
AssignOp::MinusEq => engine.sub(&l, &r)?,
AssignOp::TimesEq => engine.mul(&l, &r)?,
AssignOp::DivideEq => engine.div(&l, &r)?,
AssignOp::ModuloEq => engine.mod_op(&l, &r)?,
_ => unreachable!(),
})
}
AssignOp::Equals => unreachable!(),
}
};
write_assign_value(env, target, value, &target_ns, scope)
}
fn resolve_target_ns(
env: &mut crate::core::Environment,
namespace: Option<&crate::core::Expr>,
) -> Result<Option<Rc<crate::core::environment::Namespace>>> {
match namespace {
None => Ok(None),
Some(ns_exp) => {
let m = eval::eval(env, ns_exp)?;
if m.is_nothing() {
return Err(TemplateError::invalid_reference(
crate::core::environment::expr_desc(ns_exp),
));
}
Ok(Some(env.as_namespace(&m).ok_or_else(|| {
TemplateError::misc(format!(
"For \"#assign\" namespace: Expected a namespace, but this has evaluated to a {}: ==> {}",
m.type_name,
crate::core::environment::expr_desc(ns_exp)
))
})?))
}
}
}
pub(crate) fn exec_assign_value(
env: &mut crate::core::Environment,
target: &str,
value: TModel,
namespace: Option<&crate::core::Expr>,
scope: AssignScope,
) -> Result<ExecOutcome> {
let target_ns = resolve_target_ns(env, namespace)?;
write_assign_value(env, target, value, &target_ns, scope)
}
fn write_assign_value(
env: &mut crate::core::Environment,
target: &str,
value: TModel,
target_ns: &Option<Rc<crate::core::environment::Namespace>>,
scope: AssignScope,
) -> Result<ExecOutcome> {
match scope {
AssignScope::Local => {
env.set_local_variable(target, value)?;
}
AssignScope::Global => {
env.set_global_variable(target, value);
}
AssignScope::Namespace => match target_ns {
Some(ns) => ns.put_var(target.to_string(), value),
None => env.set_variable(target, value),
},
}
Ok(ExecOutcome::Done)
}
fn get_old_value(
env: &mut crate::core::Environment,
target: &str,
target_ns: &Option<Rc<crate::core::environment::Namespace>>,
scope: &AssignScope,
) -> Result<Option<TModel>> {
match scope {
AssignScope::Local => Ok(env.get_local_variable(target)),
AssignScope::Global => Ok(env
.get_global_namespace()
.get_member(target)
.and_then(normalize_old)),
AssignScope::Namespace => match target_ns {
Some(ns) => Ok(ns.get_member(target).and_then(normalize_old)),
None => Ok(env
.get_current_namespace()
.get_member(target)
.and_then(normalize_old)),
},
}
}
fn normalize_old(m: TModel) -> Option<TModel> {
if m.is_macro() {
None
} else {
Some(m)
}
}
fn eval_add_concat(
env: &mut crate::core::Environment,
old: &TModel,
new: &TModel,
) -> Result<TModel> {
if old.is_number() && new.is_number() {
let engine = crate::core::BigDecimalEngine::default();
return Ok(TModel::from_number(
engine.add(&old.get_number()?, &new.get_number()?)?,
));
}
if let (Some(l), Some(r)) = (&old.sequence, &new.sequence) {
return Ok(concatenated_sequence_model(l.clone(), r.clone()));
}
let both_hash = old.is_hash() && new.is_hash();
match (model_to_string(env, old), model_to_string(env, new)) {
(Ok(ls), Ok(rs)) => Ok(TModel::from_scalar(ls + &rs)),
_ if both_hash => merged_hash_model(old, new),
(Err(e), _) | (_, Err(e)) => Err(e),
}
}
pub(crate) struct ConcatenatedSeq {
left: Rc<dyn crate::template::TemplateSequenceModel>,
right: Rc<dyn crate::template::TemplateSequenceModel>,
}
fn concatenated_sequence_model(
left: Rc<dyn crate::template::TemplateSequenceModel>,
right: Rc<dyn crate::template::TemplateSequenceModel>,
) -> TModel {
let inner = Rc::new(ConcatenatedSeq { left, right });
let seq: Rc<dyn crate::template::TemplateSequenceModel> = inner.clone();
let coll: Rc<dyn crate::template::TemplateCollectionModel> = inner;
TModel {
sequence: Some(seq),
collection: Some(coll),
type_name: "sequence",
kind: crate::template::ModelKind::Sequence,
..TModel::nothing()
}
}
impl crate::template::TemplateSequenceModel for ConcatenatedSeq {
fn get(&self, index: usize) -> Result<TModel> {
let l = self.left.size()?;
if index < l {
self.left.get(index)
} else {
self.right.get(index - l)
}
}
fn size(&self) -> Result<usize> {
Ok(self.left.size()? + self.right.size()?)
}
}
impl crate::template::TemplateCollectionModel for ConcatenatedSeq {
fn iterator(&self) -> Result<Box<dyn Iterator<Item = Result<TModel>>>> {
let left = self.left.clone();
let right = self.right.clone();
let l = left.size()?;
let n = l + right.size()?;
let mut idx = 0usize;
Ok(Box::new(std::iter::from_fn(move || {
if idx >= n {
return None;
}
let i = idx;
idx += 1;
Some(if i < l { left.get(i) } else { right.get(i - l) })
})))
}
}
fn merged_hash_model(left: &TModel, right: &TModel) -> Result<TModel> {
if let (Some(l), Some(r)) = (&left.hash_ex, &right.hash_ex) {
let mut m: indexmap::IndexMap<String, TModel> = indexmap::IndexMap::new();
for ex in [l, r] {
for (k, v) in ex.entries()? {
m.insert(k, v);
}
}
return Ok(TModel::from_hash(m));
}
let left_h = left.hash.clone().ok_or_else(|| {
TemplateError::misc(format!(
"Cannot concatenate a {} value with a hash",
left.type_name
))
})?;
let right_h = right.hash.clone().ok_or_else(|| {
TemplateError::misc(format!(
"Cannot concatenate a {} value with a hash",
right.type_name
))
})?;
let inner = Rc::new(CombinedHash {
left: left_h,
right: right_h,
});
let h: Rc<dyn crate::template::TemplateHashModel> = inner;
Ok(TModel {
hash: Some(h),
type_name: "hash",
kind: crate::template::ModelKind::Hash,
..TModel::nothing()
})
}
struct CombinedHash {
left: Rc<dyn crate::template::TemplateHashModel>,
right: Rc<dyn crate::template::TemplateHashModel>,
}
impl crate::template::TemplateHashModel for CombinedHash {
fn get(&self, key: &str) -> Result<Option<TModel>> {
if let Some(v) = self.right.get(key)? {
return Ok(Some(v));
}
self.left.get(key)
}
fn is_empty(&self) -> Result<bool> {
Ok(self.left.is_empty()? && self.right.is_empty()?)
}
}
fn assign_op_str(op: &AssignOp) -> &'static str {
match op {
AssignOp::Equals => "=",
AssignOp::PlusEq => "+=",
AssignOp::MinusEq => "-=",
AssignOp::TimesEq => "*=",
AssignOp::DivideEq => "/=",
AssignOp::ModuloEq => "%=",
AssignOp::PlusPlus => "++",
AssignOp::MinusMinus => "--",
}
}
fn assign_non_number_err(target: &str, old: &TModel) -> TemplateError {
TemplateError::misc(format!(
"Expected a number, but assignment target variable \"{target}\" has evaluated to a {}.",
old.type_name
))
}
fn assign_source_desc(e: &crate::core::Expr) -> String {
use crate::core::ExprKind as K;
match &e.kind {
K::Str(s) => format!("'{}'", s),
_ => expr_desc(e),
}
}