Skip to content

Commit

Permalink
feat: compiled bytecode (#307)
Browse files Browse the repository at this point in the history
* feat: compiled bytecode

* optimize bytecode

* add compiled expression struct

* add fetch fast and better abstraction

* fix

* rename evaluate_with for Expression<Unary>
  • Loading branch information
stefan-gorules authored Feb 10, 2025
1 parent 019fc6e commit ae40aff
Show file tree
Hide file tree
Showing 22 changed files with 679 additions and 281 deletions.
3 changes: 2 additions & 1 deletion bindings/python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ crate-type = ["cdylib"]
[dependencies]
anyhow = { workspace = true }
either = "1.13"
pyo3 = { version = "0.23", features = ["anyhow", "serde"] }
pyo3 = { version = "0.23", features = ["anyhow", "serde", "either"] }
pyo3-async-runtimes = { version = "0.23", features = ["tokio-runtime", "attributes"] }
pythonize = "0.23"
json_dotpath = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
rust_decimal = { workspace = true, features = ["maths-nopanic"] }
tokio-util = { version = "0.7", features = ["rt"] }
zen-engine = { path = "../../core/engine" }
zen-expression = { path = "../../core/expression" }
Expand Down
16 changes: 8 additions & 8 deletions bindings/python/src/decision.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use std::sync::Arc;

use crate::custom_node::PyCustomNode;
use crate::engine::PyZenEvaluateOptions;
use crate::loader::PyDecisionLoader;
use crate::mt::worker_pool;
use crate::value::PyValue;
use anyhow::{anyhow, Context};
use pyo3::types::PyDict;
use pyo3::{pyclass, pymethods, Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python};
Expand All @@ -9,12 +14,7 @@ use pyo3_async_runtimes::tokio::re_exports::runtime::Runtime;
use pythonize::depythonize;
use serde_json::Value;
use zen_engine::{Decision, EvaluationOptions};

use crate::custom_node::PyCustomNode;
use crate::engine::PyZenEvaluateOptions;
use crate::loader::PyDecisionLoader;
use crate::mt::worker_pool;
use crate::value::PyValue;
use zen_expression::Variable;

#[pyclass]
#[pyo3(name = "ZenDecision")]
Expand All @@ -35,7 +35,7 @@ impl PyZenDecision {
ctx: &Bound<'_, PyDict>,
opts: Option<&Bound<'_, PyDict>>,
) -> PyResult<Py<PyAny>> {
let context: Value = depythonize(ctx).context("Failed to convert dict")?;
let context: Variable = depythonize(ctx).context("Failed to convert dict")?;
let options: PyZenEvaluateOptions = if let Some(op) = opts {
depythonize(op).context("Failed to convert dict")?
} else {
Expand All @@ -47,7 +47,7 @@ impl PyZenDecision {
let rt = Runtime::new()?;
let result = rt
.block_on(decision.evaluate_with_opts(
context.into(),
context,
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
Expand Down
16 changes: 8 additions & 8 deletions bindings/python/src/engine.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use std::sync::Arc;

use crate::custom_node::PyCustomNode;
use crate::decision::PyZenDecision;
use crate::loader::PyDecisionLoader;
use crate::mt::{block_on, worker_pool};
use crate::value::PyValue;
use anyhow::{anyhow, Context};
use pyo3::prelude::PyDictMethods;
use pyo3::types::PyDict;
Expand All @@ -11,12 +16,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use zen_engine::model::DecisionContent;
use zen_engine::{DecisionEngine, EvaluationOptions};

use crate::custom_node::PyCustomNode;
use crate::decision::PyZenDecision;
use crate::loader::PyDecisionLoader;
use crate::mt::{block_on, worker_pool};
use crate::value::PyValue;
use zen_expression::Variable;

#[pyclass]
#[pyo3(name = "ZenEngine")]
Expand Down Expand Up @@ -90,7 +90,7 @@ impl PyZenEngine {
ctx: &Bound<'_, PyDict>,
opts: Option<&Bound<'_, PyDict>>,
) -> PyResult<Py<PyAny>> {
let context: Value = depythonize(ctx).context("Failed to convert dict")?;
let context: Variable = depythonize(ctx).context("Failed to convert dict")?;
let options: PyZenEvaluateOptions = if let Some(op) = opts {
depythonize(op).context("Failed to convert dict")?
} else {
Expand All @@ -99,7 +99,7 @@ impl PyZenEngine {

let result = block_on(self.engine.evaluate_with_opts(
key,
context.into(),
context,
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
Expand Down
114 changes: 102 additions & 12 deletions bindings/python/src/expression.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
use crate::variable::PyVariable;
use anyhow::{anyhow, Context};
use pyo3::types::PyDict;
use pyo3::{pyfunction, Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python};
use either::Either;
use pyo3::types::{PyDict, PyList};
use pyo3::{
pyclass, pyfunction, pymethods, Bound, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr,
PyResult, Python,
};
use pythonize::depythonize;
use serde_json::Value;
use zen_expression::expression::{Standard, Unary};
use zen_expression::vm::VM;
use zen_expression::{Expression, Variable};

use crate::value::PyValue;
#[pyfunction]
pub fn compile_expression(expression: String) -> PyResult<PyExpression> {
let expr = zen_expression::compile_expression(expression.as_str())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

Ok(PyExpression {
expression: Either::Left(expr),
})
}

#[pyfunction]
pub fn compile_unary_expression(expression: String) -> PyResult<PyExpression> {
let expr = zen_expression::compile_unary_expression(expression.as_str())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

Ok(PyExpression {
expression: Either::Right(expr),
})
}

#[pyfunction]
#[pyo3(signature = (expression, ctx=None))]
Expand All @@ -17,19 +42,19 @@ pub fn evaluate_expression(
.map(|ctx| depythonize(ctx))
.transpose()
.context("Failed to convert context")?
.unwrap_or(Value::Null);
.unwrap_or(Variable::Null);

let result = zen_expression::evaluate_expression(expression.as_str(), context.into())
let result = zen_expression::evaluate_expression(expression.as_str(), context)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

PyValue(result.to_value()).into_py_any(py)
PyVariable(result).into_py_any(py)
}

#[pyfunction]
pub fn evaluate_unary_expression(expression: String, ctx: &Bound<'_, PyDict>) -> PyResult<bool> {
let context: Value = depythonize(ctx).context("Failed to convert context")?;
let context: Variable = depythonize(ctx).context("Failed to convert context")?;

let result = zen_expression::evaluate_unary_expression(expression.as_str(), context.into())
let result = zen_expression::evaluate_unary_expression(expression.as_str(), context)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

Ok(result)
Expand All @@ -41,10 +66,75 @@ pub fn render_template(
template: String,
ctx: &Bound<'_, PyDict>,
) -> PyResult<Py<PyAny>> {
let context: Value = depythonize(ctx).context("Failed to convert context")?;
let context: Variable = depythonize(ctx)
.context("Failed to convert context")
.unwrap_or(Variable::Null);

let result = zen_tmpl::render(template.as_str(), context.into())
let result = zen_tmpl::render(template.as_str(), context)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

PyValue(result.to_value()).into_py_any(py)
PyVariable(result).into_py_any(py)
}

#[pyclass]
pub struct PyExpression {
expression: Either<Expression<Standard>, Expression<Unary>>,
}
#[pymethods]
impl PyExpression {
#[pyo3(signature = (ctx=None))]
pub fn evaluate(&self, py: Python, ctx: Option<&Bound<'_, PyDict>>) -> PyResult<Py<PyAny>> {
let context = ctx
.map(|ctx| depythonize(ctx))
.transpose()
.context("Failed to convert context")?
.unwrap_or(Variable::Null);

let maybe_result = match &self.expression {
Either::Left(standard) => standard.evaluate(context),
Either::Right(unary) => unary.evaluate(context).map(Variable::Bool),
};

let result = maybe_result
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

PyVariable(result).into_py_any(py)
}

pub fn evaluate_many(&self, py: Python, ctx: &Bound<'_, PyList>) -> PyResult<Py<PyAny>> {
let contexts: Vec<Variable> = depythonize(ctx).context("Failed to convert contexts")?;

let mut vm = VM::new();
let results: Vec<_> = contexts
.into_iter()
.map(|context| {
let result = match &self.expression {
Either::Left(standard) => standard.evaluate_with(context, &mut vm),
Either::Right(unary) => {
unary.evaluate_with(context, &mut vm).map(Variable::Bool)
}
};

match result {
Ok(ok) => Either::Left(PyVariable(ok)),
Err(err) => Either::Right(PyExpressionError(err.to_string())),
}
})
.collect();

results.into_py_any(py)
}
}

struct PyExpressionError(String);

impl<'py> IntoPyObject<'py> for PyExpressionError {
type Target = PyAny;
type Output = Bound<'py, PyAny>;
type Error = PyErr;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let err = pyo3::exceptions::PyException::new_err(self.0);
err.into_bound_py_any(py)
}
}
9 changes: 8 additions & 1 deletion bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::decision::PyZenDecision;
use crate::engine::PyZenEngine;
use crate::expression::{evaluate_expression, evaluate_unary_expression, render_template};
use crate::expression::{
compile_expression, compile_unary_expression, evaluate_expression, evaluate_unary_expression,
render_template, PyExpression,
};
use pyo3::prelude::PyModuleMethods;
use pyo3::types::PyModule;
use pyo3::{pymodule, wrap_pyfunction, Bound, PyResult, Python};
Expand All @@ -13,14 +16,18 @@ mod loader;
mod mt;
mod types;
mod value;
mod variable;

#[pymodule]
fn zen(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyZenEngine>()?;
m.add_class::<PyZenDecision>()?;
m.add_class::<PyExpression>()?;
m.add_function(wrap_pyfunction!(evaluate_expression, m)?)?;
m.add_function(wrap_pyfunction!(evaluate_unary_expression, m)?)?;
m.add_function(wrap_pyfunction!(render_template, m)?)?;
m.add_function(wrap_pyfunction!(compile_expression, m)?)?;
m.add_function(wrap_pyfunction!(compile_unary_expression, m)?)?;

Ok(())
}
3 changes: 2 additions & 1 deletion bindings/python/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde_json::Value;
use std::sync::Arc;

use crate::value::{value_to_object, PyValue};
use crate::variable::PyVariable;
use zen_engine::handler::custom_node_adapter::{
CustomDecisionNode as BaseCustomDecisionNode, CustomNodeRequest,
};
Expand Down Expand Up @@ -74,7 +75,7 @@ impl PyNodeRequest {
let template_value = zen_tmpl::render(template.as_str(), Variable::from(&self.inner_input))
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;

PyValue(template_value.to_value()).into_py_any(py)
PyVariable(template_value).into_py_any(py)
}

fn get_field_raw(&self, py: Python, path: String) -> PyResult<Py<PyAny>> {
Expand Down
51 changes: 51 additions & 0 deletions bindings/python/src/variable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use pyo3::prelude::{PyDictMethods, PyListMethods};
use pyo3::types::{PyDict, PyList};
use pyo3::{Bound, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python};
use rust_decimal::prelude::ToPrimitive;
use zen_expression::Variable;

#[repr(transparent)]
#[derive(Clone, Debug)]
pub struct PyVariable(pub Variable);

pub fn variable_to_object<'py>(py: Python<'py>, val: &Variable) -> PyResult<Bound<'py, PyAny>> {
match val {
Variable::Null => py.None().into_bound_py_any(py),
Variable::Bool(b) => b.into_bound_py_any(py),
Variable::Number(n) => {
let of64 = n.to_f64().map(|i| i.into_bound_py_any(py));
let oi64 = n.to_i64().map(|i| i.into_bound_py_any(py));
let ou64 = n.to_u64().map(|i| i.into_bound_py_any(py));
of64.or(oi64).or(ou64).expect("number too large")
}
Variable::String(s) => s.into_bound_py_any(py),
Variable::Array(v) => {
let list = PyList::empty(py);
let b = v.borrow();
for item in b.iter() {
list.append(variable_to_object(py, item)?)?;
}

list.into_bound_py_any(py)
}
Variable::Object(m) => {
let dict = PyDict::new(py);
let b = m.borrow();
for (key, value) in b.iter() {
dict.set_item(key, variable_to_object(py, value)?)?;
}

dict.into_bound_py_any(py)
}
}
}

impl<'py> IntoPyObject<'py> for PyVariable {
type Target = PyAny;
type Output = Bound<'py, PyAny>;
type Error = PyErr;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
variable_to_object(py, &self.0)
}
}
Loading

0 comments on commit ae40aff

Please sign in to comment.