Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions bindings/c/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,13 @@ pub extern "C" fn zen_engine_evaluate(

let zen_engine = unsafe { &*(engine as *mut ZenEngine) };

let Ok(var_context) = zen_engine::Variable::try_from_value(val_context) else {
return ZenResult::error(ZenError::JsonDeserializationFailed);
};

let maybe_result = tokio_runtime().block_on(zen_engine.evaluate_with_opts(
str_key,
val_context.into(),
var_context,
options.into(),
));
let result = match maybe_result {
Expand Down Expand Up @@ -232,8 +236,10 @@ pub extern "C" fn zen_engine_evaluate_batch(
Ok(value) => {
let engine = decision_engine.clone();
BatchTask::Pending(pool.spawn_pinned(move || async move {
let value = zen_engine::Variable::try_from_value(value)
.map_err(|e| json!(format!("invalid context: {e}")))?;
engine
.evaluate_with_opts(key, value.into(), eval_options)
.evaluate_with_opts(key, value, eval_options)
.await
.map(|response| serde_json::to_value(&response).unwrap_or(Value::Null))
.map_err(|e| {
Expand Down
7 changes: 4 additions & 3 deletions bindings/nodejs/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ impl ZenDecision {
let options = opts.unwrap_or_default();

async move {
decision
.evaluate_serialized(context.into(), options.into())
.await
let context = zen_engine::Variable::try_from_value(context).map_err(
|e| serde_json::json!({ "type": "ContextError", "source": e.to_string() }),
)?;
decision.evaluate_serialized(context, options.into()).await
}
})
.await
Expand Down
10 changes: 8 additions & 2 deletions bindings/nodejs/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,11 @@ impl ZenEngine {
};

async move {
let context = zen_engine::Variable::try_from_value(context).map_err(
|e| serde_json::json!({ "type": "ContextError", "source": e.to_string() }),
)?;
graph
.evaluate_with_opts(key, context.into(), options)
.evaluate_with_opts(key, context, options)
.await
.map(|response| NodeEvalResponse::build(response, mode))
.map_err(|e| {
Expand Down Expand Up @@ -357,8 +360,11 @@ impl ZenEngine {
max_depth,
};

let context = zen_engine::Variable::try_from_value(context).map_err(
|e| serde_json::json!({ "type": "ContextError", "source": e.to_string() }),
)?;
engine
.evaluate_with_opts(key, context.into(), eval_opts)
.evaluate_with_opts(key, context, eval_opts)
.await
.map(|response| NodeEvalResponse::build(response, mode))
.map_err(|e| {
Expand Down
11 changes: 7 additions & 4 deletions bindings/nodejs/src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use serde_json::Value;

#[napi]
pub fn evaluate_expression_sync(expression: String, context: Option<Value>) -> napi::Result<Value> {
let ctx = context.unwrap_or(Value::Null);
let ctx = zen_expression::Variable::try_from_value(context.unwrap_or(Value::Null))
.map_err(|e| anyhow!(e))?;

Ok(
zen_expression::evaluate_expression(expression.as_str(), ctx.into())
zen_expression::evaluate_expression(expression.as_str(), ctx)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?
.to_value(),
)
Expand All @@ -16,16 +17,18 @@ pub fn evaluate_expression_sync(expression: String, context: Option<Value>) -> n
#[allow(dead_code)]
#[napi]
pub fn evaluate_unary_expression_sync(expression: String, context: Value) -> napi::Result<bool> {
let context = zen_expression::Variable::try_from_value(context).map_err(|e| anyhow!(e))?;
Ok(
zen_expression::evaluate_unary_expression(expression.as_str(), context.into())
zen_expression::evaluate_unary_expression(expression.as_str(), context)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?,
)
}

#[allow(dead_code)]
#[napi]
pub fn render_template_sync(template: String, context: Value) -> napi::Result<Value> {
Ok(zen_tmpl::render(template.as_str(), context.into())
let context = zen_expression::Variable::try_from_value(context).map_err(|e| anyhow!(e))?;
Ok(zen_tmpl::render(template.as_str(), context)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?
.to_value())
}
Expand Down
4 changes: 3 additions & 1 deletion bindings/python/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ impl PyZenDecision {
let result = tokio::future_into_py_with_locals(py, get_current_locals(py)?, async move {
let response = worker_pool()
.spawn_pinned(move || async move {
let context =
zen_engine::Variable::try_from_value(context).map_err(|e| anyhow!(e))?;
decision
.evaluate_with_opts(context.into(), options.into())
.evaluate_with_opts(context, options.into())
.await
.map(crate::convert::PortableResponse::build)
.map_err(|e| {
Expand Down
8 changes: 6 additions & 2 deletions bindings/python/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ impl PyZenEngine {
.map(|request| {
let engine = self.engine.clone();
worker_pool().spawn_pinned(move || async move {
let context = zen_engine::Variable::try_from_value(request.context)
.map_err(|e| Value::String(format!("invalid context: {e}")))?;
engine
.evaluate_with_opts(request.key, request.context.into(), options)
.evaluate_with_opts(request.key, context, options)
.await
.map(crate::convert::PortableResponse::build)
.map_err(|e| {
Expand Down Expand Up @@ -263,8 +265,10 @@ impl PyZenEngine {
let result = tokio::future_into_py_with_locals(py, get_current_locals(py)?, async move {
let response = worker_pool()
.spawn_pinned(move || async move {
let context =
zen_engine::Variable::try_from_value(context).map_err(|e| anyhow!(e))?;
engine
.evaluate_with_opts(key, context.into(), options.into())
.evaluate_with_opts(key, context, options.into())
.await
.map(crate::convert::PortableResponse::build)
.map_err(|e| {
Expand Down
4 changes: 3 additions & 1 deletion bindings/uniffi/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ impl ZenDecision {
let response = task::spawn_blocking(move || {
// The blocking code that uses non-Send types
Handle::current().block_on(async move {
let context = zen_engine::Variable::try_from_value(context)
.map_err(|e| ZenError::ValidationError(e.to_string()))?;
decision
.evaluate_with_opts(context.into(), options.into())
.evaluate_with_opts(context, options.into())
.await
.map(|response| ZenEngineResponse::try_from(response))
.map_err(|err| {
Expand Down
8 changes: 6 additions & 2 deletions bindings/uniffi/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ impl ZenEngine {
let response = task::spawn_blocking(move || {
// The blocking code that uses non-Send types
Handle::current().block_on(async move {
let context = zen_engine::Variable::try_from_value(context)
.map_err(|e| ZenError::ValidationError(e.to_string()))?;
engine
.evaluate_with_opts(key, context.into(), options.into())
.evaluate_with_opts(key, context, options.into())
.await
.map(|response| ZenEngineResponse::try_from(response))
.map_err(|err| {
Expand Down Expand Up @@ -115,8 +117,10 @@ impl ZenEngine {
task::spawn_blocking(move || {
Handle::current().block_on(async move {
let context: Value = request.context.try_into()?;
let context = zen_engine::Variable::try_from_value(context)
.map_err(|e| ZenError::ValidationError(e.to_string()))?;
let response = engine
.evaluate_with_opts(request.key, context.into(), options)
.evaluate_with_opts(request.key, context, options)
.await
.map_err(|err| {
ZenError::EvaluationError(
Expand Down
15 changes: 12 additions & 3 deletions core/expression/src/functions/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,10 @@ pub(crate) mod imp {

pub fn avg(args: Arguments) -> anyhow::Result<V> {
let a = __internal_number_array(&args, 0)?;
let sum = a.iter().fold(Decimal::ZERO, |acc, x| acc + x);
let sum = a
.iter()
.try_fold(Decimal::ZERO, |acc, x| acc.checked_add(*x))
.context("Number overflow")?;

Ok(V::Number(Decimal::from(
sum.checked_div(Decimal::from(a.len()))
Expand All @@ -671,7 +674,10 @@ pub(crate) mod imp {

pub fn sum(args: Arguments) -> anyhow::Result<V> {
let a = __internal_number_array(&args, 0)?;
let sum = a.iter().fold(Decimal::ZERO, |acc, v| acc + v);
let sum = a
.iter()
.try_fold(Decimal::ZERO, |acc, v| acc.checked_add(*v))
.context("Number overflow")?;

Ok(V::Number(Decimal::from(sum)))
}
Expand All @@ -688,7 +694,10 @@ pub(crate) mod imp {
let center_left = a.get(center - 1).context("Index out of bounds")?;
let center_right = a.get(center).context("Index out of bounds")?;

let median = ((*center_left) + (*center_right)) / dec!(2);
let median = center_left
.checked_add(*center_right)
.context("Number overflow")?
/ dec!(2);
Ok(V::Number(median))
}
}
Expand Down
32 changes: 28 additions & 4 deletions core/expression/src/vm/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,13 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
let a = self.pop()?;

match (a, b) {
(Number(a), Number(b)) => self.push(Number(a + b)),
(Number(a), Number(b)) => {
let result = a.checked_add(b).ok_or_else(|| OpcodeErr {
opcode: "Add".into(),
message: "Number overflow".into(),
})?;
self.push(Number(result));
}
(String(a), String(b)) => {
let mut c = StdString::with_capacity(a.len() + b.len());

Expand All @@ -489,7 +495,13 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
let a = self.pop()?;

match (a, b) {
(Number(a), Number(b)) => self.push(Number(a - b)),
(Number(a), Number(b)) => {
let result = a.checked_sub(b).ok_or_else(|| OpcodeErr {
opcode: "Subtract".into(),
message: "Number overflow".into(),
})?;
self.push(Number(result));
}
_ => {
return Err(OpcodeErr {
opcode: "Subtract".into(),
Expand All @@ -503,7 +515,13 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
let a = self.pop()?;

match (a, b) {
(Number(a), Number(b)) => self.push(Number(a * b)),
(Number(a), Number(b)) => {
let result = a.checked_mul(b).ok_or_else(|| OpcodeErr {
opcode: "Multiply".into(),
message: "Number overflow".into(),
})?;
self.push(Number(result));
}
_ => {
return Err(OpcodeErr {
opcode: "Multiply".into(),
Expand Down Expand Up @@ -535,7 +553,13 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
let a = self.pop()?;

match (a, b) {
(Number(a), Number(b)) => self.push(Number(a % b)),
(Number(a), Number(b)) => {
let result = match a.checked_rem(b) {
Some(r) => Number(r),
None => Null,
};
self.push(result);
}
_ => {
return Err(OpcodeErr {
opcode: "Modulo".into(),
Expand Down
30 changes: 30 additions & 0 deletions core/expression/tests/isolate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,3 +966,33 @@ mod test {
}
}
}

#[test]
fn arithmetic_overflow_errors_instead_of_panicking() {
let mut isolate = Isolate::new();
let max = "79228162514264337593543950335";

assert!(isolate.run_standard(&format!("{max} + 1")).is_err());
assert!(isolate.run_standard(&format!("-{max} - 1")).is_err());
assert!(isolate.run_standard(&format!("{max} * 2")).is_err());
assert!(isolate
.run_standard(&format!("sum([{max}, {max}])"))
.is_err());
assert!(isolate
.run_standard(&format!("avg([{max}, {max}])"))
.is_err());
assert!(isolate
.run_standard(&format!("median([{max}, {max}])"))
.is_err());
}

#[test]
fn division_and_modulo_by_zero_return_null() {
let mut isolate = Isolate::new();

let division = isolate.run_standard("1 / 0").unwrap();
assert_eq!(division, Variable::Null);

let modulo = isolate.run_standard("1 % 0").unwrap();
assert_eq!(modulo, Variable::Null);
}
Loading
Loading