diff --git a/src/py/api.rs b/src/py/api.rs index 8f453341..ea5bf29b 100644 --- a/src/py/api.rs +++ b/src/py/api.rs @@ -1,24 +1,104 @@ +use std::cell::RefMut; + use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyDict}; use pyo3::wrap_pyfunction; +use crate::allocator::Allocator; +use crate::core_ops::*; +use crate::err_utils::err; +use crate::more_ops::*; +use crate::node::Node; +use crate::serialize::node_to_bytes; + +use super::arena::Arena; +use super::dialect::{Dialect, PyMultiOpFn}; +use super::f_table::OpFn; use super::lazy_node::LazyNode; +use super::native_op::NativeOp; use super::run_program::{ __pyo3_get_function_deserialize_and_run_program, - __pyo3_get_function_deserialize_and_run_program2, - __pyo3_get_function_serialize_and_run_program, __pyo3_get_function_serialized_length, + __pyo3_get_function_deserialize_and_run_program2, __pyo3_get_function_serialized_length, STRICT_MODE, }; +#[pyfunction] +pub fn native_opcodes_dict(py: Python) -> PyResult { + let opcode_lookup: [(OpFn, &str); 30] = [ + (op_if, "op_if"), + (op_cons, "op_cons"), + (op_first, "op_first"), + (op_rest, "op_rest"), + (op_listp, "op_listp"), + (op_raise, "op_raise"), + (op_eq, "op_eq"), + (op_sha256, "op_sha256"), + (op_add, "op_add"), + (op_subtract, "op_subtract"), + (op_multiply, "op_multiply"), + (op_divmod, "op_divmod"), + (op_substr, "op_substr"), + (op_strlen, "op_strlen"), + (op_point_add, "op_point_add"), + (op_pubkey_for_exp, "op_pubkey_for_exp"), + (op_concat, "op_concat"), + (op_gr, "op_gr"), + (op_gr_bytes, "op_gr_bytes"), + (op_logand, "op_logand"), + (op_logior, "op_logior"), + (op_logxor, "op_logxor"), + (op_lognot, "op_lognot"), + (op_ash, "op_ash"), + (op_lsh, "op_lsh"), + (op_not, "op_not"), + (op_any, "op_any"), + (op_all, "op_all"), + (op_softfork, "op_softfork"), + (op_div, "op_div"), + ]; + let r = PyDict::new(py); + for (f, name) in opcode_lookup.iter() { + r.set_item(name, PyCell::new(py, NativeOp::new(*f))?)?; + } + Ok(r.to_object(py)) +} + +#[pyfunction] +fn serialize_to_bytes<'p>(py: Python<'p>, sexp: &PyAny) -> PyResult<&'p PyBytes> { + let arena_cell = Arena::new_cell(py)?; + let arena = arena_cell.borrow(); + let mut allocator_refcell: RefMut = arena.allocator(); + let allocator: &mut Allocator = &mut allocator_refcell as &mut Allocator; + + let ptr = arena.as_native(py, allocator, sexp)?; + + let node = Node::new(allocator, ptr); + let s: Vec = node_to_bytes(&node)?; + Ok(PyBytes::new(py, &s)) +} + /// This module is a python module implemented in Rust. #[pymodule] fn clvm_rs(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(serialize_and_run_program, m)?)?; - m.add_function(wrap_pyfunction!(deserialize_and_run_program, m)?)?; - m.add_function(wrap_pyfunction!(deserialize_and_run_program2, m)?)?; - m.add("STRICT_MODE", STRICT_MODE)?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; + m.add_function(wrap_pyfunction!(native_opcodes_dict, m)?)?; m.add_function(wrap_pyfunction!(serialized_length, m)?)?; + m.add( + "NATIVE_OP_UNKNOWN_STRICT", + PyMultiOpFn::new(|_a, b, _op, _d| err(b, "unimplemented operator")), + )?; + + m.add("NATIVE_OP_UNKNOWN_NON_STRICT", PyMultiOpFn::new(op_unknown))?; + + m.add_function(wrap_pyfunction!(serialize_to_bytes, m)?)?; + + m.add_function(wrap_pyfunction!(deserialize_and_run_program, m)?)?; + m.add_function(wrap_pyfunction!(deserialize_and_run_program2, m)?)?; + m.add("STRICT_MODE", STRICT_MODE)?; + Ok(()) } diff --git a/src/py/arena.rs b/src/py/arena.rs new file mode 100644 index 00000000..ebcecd40 --- /dev/null +++ b/src/py/arena.rs @@ -0,0 +1,86 @@ +/// An `Arena` is a collection of objects representing a program and +/// its arguments, and intermediate values reached while running +/// a program. Objects can be created in an `Arena` but are never +/// dropped until the `Arena` is dropped. +use std::cell::{RefCell, RefMut}; + +use pyo3::prelude::pyclass; +use pyo3::prelude::*; + +use super::bridge_cache::BridgeCache; +use crate::allocator::{Allocator, NodePtr}; +use crate::serialize::node_from_bytes; + +#[pyclass(subclass, unsendable)] +pub struct Arena { + arena: RefCell, + cache: BridgeCache, +} + +#[pymethods] +impl Arena { + #[new] + pub fn new(py: Python, new_obj_f: PyObject) -> PyResult { + Ok(Arena { + arena: RefCell::new(Allocator::default()), + cache: BridgeCache::new(py, new_obj_f)?, + }) + } + + /// deserialize `bytes` into an object in this `Arena` + pub fn deserialize<'p>(&self, py: Python<'p>, blob: &[u8]) -> PyResult<&'p PyAny> { + let allocator: &mut Allocator = &mut self.allocator() as &mut Allocator; + let ptr = node_from_bytes(allocator, blob)?; + self.as_python(py, allocator, ptr) + } + + /// copy this python object into this `Arena` if it's not yet in the cache + /// (otherwise it returns the previously cached object) + pub fn include<'p>(&self, py: Python<'p>, obj: &'p PyAny) -> PyResult<&'p PyAny> { + let allocator = &mut self.allocator(); + let ptr = self.as_native(py, allocator, obj)?; + self.as_python(py, allocator, ptr) + } + + /// copy this python object into this `Arena` if it's not yet in the cache + /// (otherwise it returns the previously cached object) + pub fn ptr_for_obj(&self, py: Python, obj: &PyAny) -> PyResult { + self.as_native(py, &mut self.allocator(), obj) + } +} + +impl Arena { + pub fn new_cell_obj(py: Python, new_obj_f: PyObject) -> PyResult<&PyCell> { + PyCell::new(py, Arena::new(py, new_obj_f)?) + } + + pub fn new_cell(py: Python) -> PyResult<&PyCell> { + Self::new_cell_obj(py, py.eval("lambda sexp: sexp", None, None)?.to_object(py)) + } + + pub fn obj_for_ptr<'p>(&self, py: Python<'p>, ptr: i32) -> PyResult<&'p PyAny> { + self.as_python(py, &mut self.allocator(), ptr) + } + + pub fn allocator(&self) -> RefMut { + self.arena.borrow_mut() + } + + pub fn as_native( + &self, + py: Python, + allocator: &mut Allocator, + obj: &PyAny, + ) -> PyResult { + self.cache.as_native(py, allocator, obj) + } + + pub fn as_python<'p>( + &self, + py: Python<'p>, + allocator: &mut Allocator, + ptr: NodePtr, + ) -> PyResult<&'p PyAny> { + self.cache.as_python(py, allocator, ptr) + } +} diff --git a/src/py/bridge_cache.rs b/src/py/bridge_cache.rs new file mode 100644 index 00000000..d4fa4c8b --- /dev/null +++ b/src/py/bridge_cache.rs @@ -0,0 +1,236 @@ +use std::collections::HashSet; + +use pyo3::prelude::*; +use pyo3::types::{IntoPyDict, PyBytes, PyTuple}; + +use crate::allocator::{Allocator, NodePtr, SExp}; + +enum PySExp<'p> { + Atom(&'p PyBytes), + Pair(&'p PyAny, &'p PyAny), +} + +pub struct BridgeCache { + /// this cache is a python `dict` that keeps a mapping + /// from `i32` to python objects and vice-versa + cache: PyObject, + /// `to_python`, a python callable, is called whenever a `python` + /// object needs to be created from a native one. It's called with + /// either a `bytes` or `tuple` of two elements, each of which + /// either came from python or has had `to_python` called on them + to_python: PyObject, +} + +/// yield a corresponding `PySExp` for a python object based +/// on the standard way of looking in `.atom` for a `PyBytes` object +/// and then in `.pair` for a `PyTuple`. + +fn sexp_for_obj(obj: &PyAny) -> PyResult { + let r: PyResult<&PyBytes> = obj.getattr("atom")?.extract(); + if let Ok(bytes) = r { + return Ok(PySExp::Atom(bytes)); + } + let pair: &PyTuple = obj.getattr("pair")?.extract()?; + let p0: &PyAny = pair.get_item(0); + let p1: &PyAny = pair.get_item(1); + Ok(PySExp::Pair(p0, p1)) +} + +impl BridgeCache { + pub fn new(py: Python, new_obj_f: PyObject) -> PyResult { + Ok(BridgeCache { + cache: py.eval("dict()", None, None)?.to_object(py), + to_python: new_obj_f, + }) + } + + /// add a python object <-> native object mapping + /// to the cache, in both directions + fn add(&self, py: Python, obj: &PyAny, ptr: NodePtr) -> PyResult<()> { + let locals = [ + ("cache", self.cache.clone()), + ("obj", obj.to_object(py)), + ("ptr", ptr.to_object(py)), + ] + .into_py_dict(py); + + py.run("cache[ptr] = obj; cache[id(obj)] = ptr", None, Some(locals)) + } + + // py to native methods + + fn native_from_cache<'p>(&self, py: Python<'p>, obj: &PyAny) -> PyResult { + let locals = [("cache", self.cache.clone()), ("key", obj.to_object(py))].into_py_dict(py); + py.eval("cache[id(key)]", None, Some(locals))?.extract() + } + + /// copy this python object into this `Arena` if it's not yet in the cache + /// (otherwise it returns the previously cached object) + fn populate_native( + &self, + py: Python, + obj: &PyAny, + allocator: &mut Allocator, + ) -> PyResult { + // items in `pending` are already in the stack of things to be converted + // if they appear again, we have an illegal cycle and must fail + + let mut pending: HashSet = HashSet::new(); + + apply_to_tree(obj, move |obj| { + // is it in cache yet? + if self.native_from_cache(py, obj).is_ok() { + // yep, we're done + return Ok(None); + } + + // it's not in the cache + + match sexp_for_obj(obj)? { + PySExp::Atom(atom) => { + let blob: &[u8] = atom.extract()?; + let ptr = allocator.new_atom(blob).unwrap(); + self.add(py, obj, ptr)?; + + Ok(None) + } + PySExp::Pair(p0, p1) => { + let ptr_0: PyResult = self.native_from_cache(py, p0); + let ptr_1: PyResult = self.native_from_cache(py, p1); + + let as_obj = id_for_pyany(py, obj)?; + + if let (Ok(ptr_0), Ok(ptr_1)) = (ptr_0, ptr_1) { + let ptr = allocator.new_pair(ptr_0, ptr_1).unwrap(); + self.add(py, obj, ptr)?; + + pending.remove(&as_obj); + Ok(None) + } else { + if pending.contains(&as_obj) { + let locals = Some([("obj", obj)].into_py_dict(py)); + py.run( + "raise ValueError(f'illegal clvm object loop {obj}')", + None, + locals, + )?; + panic!(); + } + pending.insert(as_obj); + + Ok(Some((p0, p1))) + } + } + } + })?; + + self.native_from_cache(py, obj) + } + + pub fn as_native( + &self, + py: Python, + allocator: &mut Allocator, + obj: &PyAny, + ) -> PyResult { + self.native_from_cache(py, obj) + .or_else(|_err| self.populate_native(py, obj, allocator)) + } + + // native to py methods + + fn python_from_cache<'p>(&self, py: Python<'p>, ptr: NodePtr) -> PyResult<&'p PyAny> { + let locals = [("cache", self.cache.clone()), ("key", ptr.to_object(py))].into_py_dict(py); + py.eval("cache[key]", None, Some(locals))?.extract() + } + + fn populate_python<'p>( + &self, + py: Python<'p>, + ptr: NodePtr, + allocator: &mut Allocator, + ) -> PyResult<&'p PyAny> { + apply_to_tree(ptr, move |ptr| { + // is it in cache yet? + if self.python_from_cache(py, ptr).is_ok() { + // yep, we're done + return Ok(None); + } + + // it's not in the cache + + match allocator.sexp(ptr) { + SExp::Atom(a) => { + // it's an atom, so we just populate cache directly + let blob = allocator.buf(&a); + let py_bytes = PyBytes::new(py, blob); + self.add(py, self.to_python.as_ref(py).call1((py_bytes,))?, ptr)?; + Ok(None) + } + SExp::Pair(ptr_1, ptr_2) => { + // we can only create this if the children are in the cache + // Let's find out + let locals = [ + ("cache", self.cache.clone()), + ("p1", ptr_1.to_object(py)), + ("p2", ptr_2.to_object(py)), + ] + .into_py_dict(py); + + let pair: PyResult<&PyAny> = + py.eval("(cache[p1], cache[p2])", None, Some(locals)); + + match pair { + // the children aren't in the cache, keep drilling down + Err(_) => Ok(Some((ptr_1, ptr_2))), + + // the children are in the cache, create new node & populate cache with it + Ok(tuple) => { + let (_p1, _p2): (&PyAny, &PyAny) = tuple.extract()?; + self.add(py, self.to_python.as_ref(py).call1((tuple,))?, ptr)?; + Ok(None) + } + } + } + } + })?; + + self.python_from_cache(py, ptr) + } + + pub fn as_python<'p>( + &self, + py: Python<'p>, + allocator: &mut Allocator, + ptr: NodePtr, + ) -> PyResult<&'p PyAny> { + self.python_from_cache(py, ptr) + .or_else(|_err| self.populate_python(py, ptr, allocator)) + } +} + +fn id_for_pyany(py: Python, obj: &PyAny) -> PyResult { + let locals = Some([("obj", obj)].into_py_dict(py)); + py.eval("id(obj)", None, locals)?.extract() +} + +fn apply_to_tree(node: T, mut apply: F) -> PyResult<()> +where + F: FnMut(T) -> PyResult>, + T: Clone, +{ + let mut items = vec![node]; + loop { + let t = items.pop(); + if let Some(obj) = t { + if let Some((p0, p1)) = apply(obj.clone())? { + items.push(obj); + items.push(p0); + items.push(p1); + } + } else { + break; + } + } + Ok(()) +} diff --git a/src/py/dialect.rs b/src/py/dialect.rs new file mode 100644 index 00000000..5da4e8de --- /dev/null +++ b/src/py/dialect.rs @@ -0,0 +1,331 @@ +use std::cell::RefMut; +use std::collections::HashMap; + +use pyo3::prelude::{pyclass, pymethods}; + +use pyo3::types::{PyString, PyTuple}; +use pyo3::{FromPyObject, PyAny, PyObject, PyRef, PyResult, Python, ToPyObject}; + +use crate::allocator::{Allocator, NodePtr}; +use crate::cost::Cost; +use crate::reduction::EvalErr; +use crate::reduction::Reduction; +use crate::reduction::Response; +use crate::run_program::{OperatorHandler, PostEval, PreEval}; +use crate::serialize::node_from_bytes; + +use super::arena::Arena; +use super::error_bridge::{eval_err_for_pyerr, raise_eval_error, unwrap_or_eval_err}; +use super::f_table::FLookup; +use super::f_table::OpFn; +use super::native_op::NativeOp; + +type MultiOpFn = fn(&mut Allocator, NodePtr, NodePtr, Cost) -> Response; + +#[pyclass] +#[derive(Clone)] +pub struct PyMultiOpFn { + op: MultiOpFn, +} + +impl PyMultiOpFn { + pub fn new(op: MultiOpFn) -> Self { + Self { op } + } +} + +#[derive(Clone)] +pub enum MultiOpFnE { + Python(PyObject), + Rust(MultiOpFn), +} + +impl MultiOpFnE { + pub fn invoke( + &self, + allocator: &mut Allocator, + o: NodePtr, + args: NodePtr, + max_cost: Cost, + ) -> Response { + match self { + Self::Python(_o) => { + todo!() + } + Self::Rust(f) => f(allocator, o, args, max_cost), + } + } +} + +impl<'source> FromPyObject<'source> for MultiOpFnE { + fn extract(obj: &'source pyo3::PyAny) -> PyResult { + let v: PyResult> = obj.extract(); + if let Ok(v) = v { + Ok(Self::Rust(v.op)) + } else { + Ok(Self::Python(obj.into())) + } + } +} + +#[pyclass] +pub struct Dialect { + quote_kw: Vec, + apply_kw: Vec, + u8_lookup: FLookup, + python_u8_lookup: HashMap, PyObject>, + native_u8_lookup: HashMap, OpFn>, + unknown_op_callback: MultiOpFnE, + to_python: PyObject, +} + +#[pymethods] +impl Dialect { + #[new] + pub fn new( + quote_kw: Vec, + apply_kw: Vec, + unknown_op_callback: MultiOpFnE, + to_python: PyObject, + ) -> PyResult { + let u8_lookup = [None; 256]; + let python_u8_lookup = HashMap::new(); + let native_u8_lookup = HashMap::new(); + Ok(Self { + quote_kw, + apply_kw, + u8_lookup, + python_u8_lookup, + native_u8_lookup, + unknown_op_callback, + to_python, + }) + } + + pub fn update(&mut self, py: Python, d: HashMap, PyObject>) -> PyResult<()> { + for (op, fn_obj) in d.iter() { + let r: PyResult> = fn_obj.extract(py); + if let Ok(native_op) = r { + if op.len() == 1 { + let index = op[0] as usize; + self.u8_lookup[index] = Some(native_op.op); + } else { + self.native_u8_lookup.insert(op.to_owned(), native_op.op); + } + } else { + self.python_u8_lookup.insert(op.to_owned(), fn_obj.clone()); + } + } + Ok(()) + } + + pub fn run_program<'p>( + &self, + py: Python<'p>, + program: &PyAny, + args: &PyAny, + max_cost: Cost, + pre_eval_f: &PyAny, + ) -> PyResult<(Cost, PyObject)> { + let arena_cell = Arena::new_cell_obj(py, self.to_python.clone())?; + let arena: PyRef = arena_cell.borrow(); + + let program = arena.ptr_for_obj(py, program)?; + let args = arena.ptr_for_obj(py, args)?; + + let (cost, r) = self.run_program_ptr(py, arena, program, args, max_cost, pre_eval_f)?; + Ok((cost, r.to_object(py))) + } + + pub fn deserialize_and_run_program<'p>( + &self, + py: Python<'p>, + program_blob: &[u8], + args_blob: &[u8], + max_cost: Cost, + pre_eval: &'p PyAny, + ) -> PyResult<(Cost, &'p PyAny)> { + let arena_cell = Arena::new_cell_obj(py, self.to_python.clone())?; + let arena = arena_cell.borrow(); + let (program, args) = { + let mut allocator_refcell: RefMut = arena.allocator(); + let allocator: &mut Allocator = &mut allocator_refcell as &mut Allocator; + + let program = node_from_bytes(allocator, program_blob)?; + let args = node_from_bytes(allocator, args_blob)?; + (program, args) + }; + self.run_program_ptr(py, arena, program, args, max_cost, pre_eval) + } +} + +fn pre_eval_callback( + py: Python, + arena: &Arena, + pre_eval_obj: PyObject, + allocator: &mut Allocator, + program: NodePtr, + args: NodePtr, +) -> PyResult { + // call the python `pre_eval` object and return the python object yielded + let program_obj = arena.as_python(py, allocator, program)?; + let args_obj = arena.as_python(py, allocator, args)?; + let post_eval_obj = pre_eval_obj + .call1(py, (program_obj, args_obj))? + .to_object(py); + Ok(post_eval_obj) +} + +impl Dialect { + pub fn run_program_ptr<'p>( + &self, + py: Python<'p>, + arena: PyRef<'p, Arena>, + program: i32, + args: i32, + max_cost: Cost, + pre_eval: &'p PyAny, + ) -> PyResult<(Cost, &'p PyAny)> { + let mut allocator_refcell: RefMut = arena.allocator(); + let allocator: &mut Allocator = &mut allocator_refcell as &mut Allocator; + + let drc = DialectRunningContext { + dialect: self, + arena: &arena, + }; + + // we convert `pre_eval` from a python object to a `PreEval` + // this should be factored out to a standalone function, but + // lifetimes make it tough! + let pre_eval_obj = pre_eval.to_object(py); + let pre_eval_f = { + if pre_eval.is_none() { + None + } else { + let local_pre_eval: PreEval = Box::new(|allocator, program, args| { + if let Ok(post_eval_obj) = pre_eval_callback( + py, + &arena, + pre_eval_obj.clone(), + allocator, + program, + args, + ) { + let local_arena = &arena; + let post_eval: Box = + Box::new(move |allocator: &mut Allocator, result_ptr: i32| { + if let Ok(r) = local_arena.as_python(py, allocator, result_ptr) { + // invoke the python `PostEval` callback + let _r = post_eval_obj.call1(py, (r.to_object(py),)); + } + }); + Ok(Some(post_eval)) + } else { + Ok(None) + } + }); + Some(local_pre_eval) + } + }; + + let r: Result = crate::run_program::run_program( + allocator, + program, + args, + &self.quote_kw, + &self.apply_kw, + max_cost, + &drc, + pre_eval_f, + ); + + match r { + Ok(reduction) => { + let r = arena.as_python(py, allocator, reduction.1)?; + Ok((reduction.0, r)) + } + Err(eval_err) => { + let node: PyObject = arena.as_python(py, allocator, eval_err.0)?.to_object(py); + let s: String = eval_err.1; + let s1: &str = &s; + let msg: &PyString = PyString::new(py, s1); + match raise_eval_error(py, &msg, node) { + Err(x) => Err(x), + _ => panic!(), + } + } + } + } +} + +struct DialectRunningContext<'a> { + dialect: &'a Dialect, + arena: &'a PyRef<'a, Arena>, +} + +impl DialectRunningContext<'_> { + pub fn invoke_py_obj( + &self, + obj: &PyObject, + allocator: &mut Allocator, + args: NodePtr, + max_cost: Cost, + ) -> Response { + Python::with_gil(|py| { + let r = unwrap_or_eval_err( + self.arena.as_python(py, allocator, args), + args, + "can't uncache", + )?; + let r1 = obj.call1(py, (r.to_object(py), max_cost)); + match r1 { + Err(pyerr) => { + let eval_err: PyResult = + eval_err_for_pyerr(py, &pyerr, &self.arena, allocator); + let r: EvalErr = unwrap_or_eval_err(eval_err, args, "unexpected exception")?; + Err(r) + } + Ok(o) => { + let pair: &PyTuple = unwrap_or_eval_err(o.extract(py), args, "expected tuple")?; + + let i0: u32 = + unwrap_or_eval_err(pair.get_item(0).extract(), args, "expected u32")?; + + let clvm_object: &PyAny = + unwrap_or_eval_err(pair.get_item(1).extract(), args, "expected node")?; + + let r = self.arena.as_native(py, allocator, clvm_object); + let node: i32 = unwrap_or_eval_err(r, args, "can't find in int allocator")?; + Ok(Reduction(i0 as Cost, node)) + } + } + }) + } +} + +impl OperatorHandler for DialectRunningContext<'_> { + fn op( + &self, + allocator: &mut Allocator, + o: NodePtr, + argument_list: NodePtr, + max_cost: Cost, + ) -> Response { + let op = &allocator.atom(o); + if op.len() == 1 { + if let Some(f) = self.dialect.u8_lookup[op[0] as usize] { + return f(allocator, argument_list, max_cost); + } + } + let op = op.to_owned(); + if let Some(op_fn) = self.dialect.native_u8_lookup.get(op) { + op_fn(allocator, argument_list, max_cost) + } else if let Some(op_fn) = self.dialect.python_u8_lookup.get(op) { + self.invoke_py_obj(op_fn, allocator, argument_list, max_cost) + } else { + self.dialect + .unknown_op_callback + .invoke(allocator, o, argument_list, max_cost) + } + } +} diff --git a/src/py/error_bridge.rs b/src/py/error_bridge.rs new file mode 100644 index 00000000..d1e16dae --- /dev/null +++ b/src/py/error_bridge.rs @@ -0,0 +1,44 @@ +use crate::allocator::{Allocator, NodePtr}; +use crate::reduction::EvalErr; +use pyo3::types::{PyDict, PyString, PyTuple}; +use pyo3::{PyAny, PyErr, PyObject, PyRef, PyResult, Python}; + +use super::arena::Arena; + +/// turn a `PyErr` into an `EvalErr` if at all possible +/// otherwise, return a `PyErr` +pub fn eval_err_for_pyerr<'p>( + py: Python<'p>, + pyerr: &PyErr, + arena: &'p PyRef, + allocator: &mut Allocator, +) -> PyResult { + let args: &PyTuple = pyerr.pvalue(py).getattr("args")?.extract()?; + let arg0: &PyString = args.get_item(0).extract()?; + let sexp: &PyAny = pyerr.pvalue(py).getattr("_sexp")?.extract()?; + let node: i32 = arena.as_native(py, allocator, sexp)?; + let s: String = arg0.to_str()?.to_string(); + Ok(EvalErr(node, s)) +} + +pub fn unwrap_or_eval_err(obj: PyResult, err_node: NodePtr, msg: &str) -> Result { + match obj { + Err(_py_err) => Err(EvalErr(err_node, msg.to_string())), + Ok(o) => Ok(o), + } +} + +pub fn raise_eval_error(py: Python, msg: &PyString, sexp: PyObject) -> PyResult { + let ctx: &PyDict = PyDict::new(py); + ctx.set_item("msg", msg)?; + ctx.set_item("sexp", sexp)?; + let r = py.run( + "from clvm.EvalError import EvalError; raise EvalError(msg, sexp)", + None, + Some(ctx), + ); + match r { + Err(x) => Err(x), + Ok(_) => Ok(ctx.into()), + } +} diff --git a/src/py/f_table.rs b/src/py/f_table.rs index 2ae4fc11..b30d59ef 100644 --- a/src/py/f_table.rs +++ b/src/py/f_table.rs @@ -10,7 +10,7 @@ use crate::more_ops::{ }; use crate::reduction::Response; -type OpFn = fn(&mut Allocator, NodePtr, Cost) -> Response; +pub type OpFn = fn(&mut Allocator, NodePtr, Cost) -> Response; pub type FLookup = [Option; 256]; diff --git a/src/py/mod.rs b/src/py/mod.rs index 7a356f08..fd9f3d8b 100644 --- a/src/py/mod.rs +++ b/src/py/mod.rs @@ -1,4 +1,9 @@ pub mod api; +pub mod arena; +pub mod bridge_cache; +pub mod dialect; +pub mod error_bridge; pub mod f_table; pub mod lazy_node; +pub mod native_op; pub mod run_program; diff --git a/src/py/native_op.rs b/src/py/native_op.rs new file mode 100644 index 00000000..5093c01e --- /dev/null +++ b/src/py/native_op.rs @@ -0,0 +1,57 @@ +use pyo3::prelude::{pyclass, pymethods}; +use pyo3::types::PyString; +use pyo3::{PyAny, PyObject, PyResult, Python, ToPyObject}; + +use crate::allocator::Allocator; +use crate::cost::Cost; +use crate::reduction::Reduction; + +use super::arena::Arena; +use super::error_bridge::raise_eval_error; +use super::f_table::OpFn; + +#[pyclass] +pub struct NativeOp { + pub op: OpFn, +} + +impl NativeOp { + pub fn new(op: OpFn) -> Self { + Self { op } + } +} + +#[pymethods] +impl NativeOp { + #[call] + fn __call__<'p>( + &'p self, + py: Python<'p>, + args: &'p PyAny, + _max_cost: Cost, + ) -> PyResult<(Cost, PyObject)> { + let arena_cell = Arena::new_cell(py)?; + let arena: &Arena = &arena_cell.borrow(); + let ptr = arena.ptr_for_obj(py, args)?; + let mut allocator = arena.allocator(); + let allocator: &mut Allocator = &mut allocator; + let r = (self.op)(allocator, ptr, _max_cost); + match r { + Ok(Reduction(cost, ptr)) => { + let r = arena.as_python(py, allocator, ptr)?; + Ok((cost, r.to_object(py))) + } + Err(_err) => { + let r = arena.as_python(py, allocator, ptr)?; + match raise_eval_error( + py, + PyString::new(py, "problem in suboperator"), + r.to_object(py), + ) { + Err(e) => Err(e), + Ok(_) => panic!("oh dear"), + } + } + } + } +} diff --git a/src/py/run_program.rs b/src/py/run_program.rs index d5e3c851..96d64e1a 100644 --- a/src/py/run_program.rs +++ b/src/py/run_program.rs @@ -1,3 +1,4 @@ +use std::cell::RefMut; use std::collections::HashMap; use std::rc::Rc; @@ -6,18 +7,20 @@ use crate::cost::Cost; use crate::err_utils::err; use crate::more_ops::op_unknown; use crate::node::Node; -use crate::py::f_table::{f_lookup_for_hashmap, FLookup}; -use crate::py::lazy_node::LazyNode; use crate::reduction::Response; use crate::run_program::{run_program, OperatorHandler}; use crate::serialize::{node_from_bytes, node_to_bytes, serialized_length_from_bytes}; +use super::arena::Arena; +use super::f_table::{f_lookup_for_hashmap, FLookup}; +use super::lazy_node::LazyNode; + use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict}; pub const STRICT_MODE: u32 = 1; -struct OperatorHandlerWithMode { +pub struct OperatorHandlerWithMode { f_lookup: FLookup, strict: bool, } @@ -44,67 +47,6 @@ impl OperatorHandler for OperatorHandlerWithMode { } } -#[pyfunction] -pub fn serialize_and_run_program( - py: Python, - program: &[u8], - args: &[u8], - quote_kw: u8, - apply_kw: u8, - max_cost: Cost, - flags: u32, -) -> PyResult<(Cost, Py)> { - let mut opcode_lookup_by_name = HashMap::>::new(); - for (v, s) in [ - (4, "op_if"), - (5, "op_cons"), - (6, "op_first"), - (7, "op_rest"), - (8, "op_listp"), - (9, "op_raise"), - (10, "op_eq"), - (11, "op_sha256"), - (12, "op_add"), - (13, "op_subtract"), - (14, "op_multiply"), - (15, "op_divmod"), - (16, "op_substr"), - (17, "op_strlen"), - (18, "op_point_add"), - (19, "op_pubkey_for_exp"), - (20, "op_concat"), - (22, "op_gr"), - (23, "op_gr_bytes"), - (24, "op_logand"), - (25, "op_logior"), - (26, "op_logxor"), - (27, "op_lognot"), - (28, "op_ash"), - (29, "op_lsh"), - (30, "op_not"), - (31, "op_any"), - (32, "op_all"), - (33, "op_softfork"), - (34, "op_div"), - ] - .iter() - { - let v: Vec = vec![*v as u8]; - opcode_lookup_by_name.insert(s.to_string(), v); - } - - deserialize_and_run_program( - py, - program, - args, - quote_kw, - apply_kw, - opcode_lookup_by_name, - max_cost, - flags, - ) -} - #[allow(clippy::too_many_arguments)] #[pyfunction] pub fn deserialize_and_run_program( @@ -116,32 +58,29 @@ pub fn deserialize_and_run_program( opcode_lookup_by_name: HashMap>, max_cost: Cost, flags: u32, -) -> PyResult<(Cost, Py)> { - let mut allocator = Allocator::new(); +) -> PyResult<(Cost, PyObject)> { + let arena = Arena::new_cell(py)?; + let arena_borrowed = arena.borrow(); + let mut allocator_refcell: RefMut = arena_borrowed.allocator(); + let allocator: &mut Allocator = &mut allocator_refcell as &mut Allocator; let f_lookup = f_lookup_for_hashmap(opcode_lookup_by_name); - let strict: bool = (flags & STRICT_MODE) != 0; - let f: Box = Box::new(OperatorHandlerWithMode { f_lookup, strict }); - let program = node_from_bytes(&mut allocator, program)?; - let args = node_from_bytes(&mut allocator, args)?; +let strict: bool = (flags & STRICT_MODE) != 0; + let f = OperatorHandlerWithMode { f_lookup, strict }; + let program = node_from_bytes(allocator, program)?; + let args = node_from_bytes(allocator, args)?; let r = py.allow_threads(|| { run_program( - &mut allocator, - program, - args, - quote_kw, - apply_kw, - max_cost, - f, - None, + allocator, program, args, &[quote_kw], &[apply_kw], max_cost, &f, None, ) }); match r { - Ok(reduction) => { - let node_as_blob = node_to_bytes(&Node::new(&allocator, reduction.1))?; - let node_as_bytes: Py = PyBytes::new(py, &node_as_blob).into(); - Ok((reduction.0, node_as_bytes)) - } + Ok(reduction) => Ok(( + reduction.0, + arena_borrowed + .as_python(py, allocator, reduction.1)? + .to_object(py), + )), Err(eval_err) => { let node_as_blob = node_to_bytes(&Node::new(&allocator, eval_err.0))?; let msg = eval_err.1; @@ -187,7 +126,7 @@ pub fn deserialize_and_run_program2( let mut allocator = Allocator::new(); let f_lookup = f_lookup_for_hashmap(opcode_lookup_by_name); let strict: bool = (flags & STRICT_MODE) != 0; - let f: Box = Box::new(OperatorHandlerWithMode { f_lookup, strict }); + let f = OperatorHandlerWithMode { f_lookup, strict }; let program = node_from_bytes(&mut allocator, program)?; let args = node_from_bytes(&mut allocator, args)?; @@ -196,10 +135,10 @@ pub fn deserialize_and_run_program2( &mut allocator, program, args, - quote_kw, - apply_kw, + &[quote_kw], + &[apply_kw], max_cost, - f, + &f, None, ) }); diff --git a/src/run_program.rs b/src/run_program.rs index f1ffb1f2..08adff5d 100644 --- a/src/run_program.rs +++ b/src/run_program.rs @@ -22,10 +22,11 @@ pub trait OperatorHandler { -> Response; } -pub type PreEval = - Box Result>, EvalErr>>; +pub type PreEval<'a> = Box< + dyn Fn(&mut Allocator, NodePtr, NodePtr) -> Result>>, EvalErr> + 'a, +>; -pub type PostEval = dyn Fn(Option); +pub type PostEval<'a> = dyn Fn(&mut Allocator, NodePtr) + 'a; #[repr(u8)] enum Operation { @@ -41,11 +42,11 @@ enum Operation { pub struct RunProgramContext<'a> { allocator: &'a mut Allocator, - quote_kw: u8, - apply_kw: u8, - operator_lookup: Box, - pre_eval: Option, - posteval_stack: Vec>, + quote_kw: &'a [u8], + apply_kw: &'a [u8], + operator_lookup: &'a dyn OperatorHandler, + pre_eval: Option>, + posteval_stack: Vec>>, val_stack: Vec, op_stack: Vec, } @@ -113,7 +114,7 @@ fn traverse_path(allocator: &Allocator, node_index: &[u8], args: NodePtr) -> Res return Err(EvalErr(arg_list, "path into atom".into())); } SExp::Pair(left, right) => { - arg_list = *(if is_bit_set { &right } else { &left }); + arg_list = if is_bit_set { right } else { left }; } } if bitmask == 0x80 { @@ -142,10 +143,10 @@ fn augment_cost_errors(r: Result, max_cost: NodePtr) -> Result RunProgramContext<'a> { fn new( allocator: &'a mut Allocator, - quote_kw: u8, - apply_kw: u8, - operator_lookup: Box, - pre_eval: Option, + quote_kw: &'a [u8], + apply_kw: &'a [u8], + operator_lookup: &'a dyn OperatorHandler, + pre_eval: Option>, ) -> Self { RunProgramContext { allocator, @@ -178,10 +179,7 @@ impl<'a> RunProgramContext<'a> { } } -impl<'a> RunProgramContext<'a> -where - NodePtr: 'static, -{ +impl<'a> RunProgramContext<'a> { fn eval_op_atom( &mut self, op_buf: &AtomBuf, @@ -191,7 +189,7 @@ where ) -> Result { let op_atom = self.allocator.buf(op_buf); // special case check for quote - if op_atom.len() == 1 && op_atom[0] == self.quote_kw { + if op_atom.len() == 1 && op_atom == self.quote_kw { self.push(operand_list); Ok(QUOTE_COST) } else { @@ -282,7 +280,7 @@ where return err(operator, "internal error"); } let op_atom = self.allocator.atom(operator); - if op_atom.len() == 1 && op_atom[0] == self.apply_kw { + if op_atom.len() == 1 && op_atom == self.apply_kw { let operand_list = Node::new(self.allocator, operand_list); if operand_list.arg_count_is(2) { let new_operator = operand_list.first()?; @@ -334,7 +332,9 @@ where Operation::PostEval => { let f = self.posteval_stack.pop().unwrap(); let peek: Option = self.val_stack.last().copied(); - f(peek); + if let Some(v) = peek { + f(self.allocator, v); + } 0 } }; @@ -347,19 +347,16 @@ where } #[allow(clippy::too_many_arguments)] -pub fn run_program( - allocator: &mut Allocator, +pub fn run_program<'a>( + allocator: &'a mut Allocator, program: NodePtr, args: NodePtr, - quote_kw: u8, - apply_kw: u8, + quote_kw: &'a [u8], + apply_kw: &'a [u8], max_cost: Cost, - operator_lookup: Box, - pre_eval: Option, -) -> Response -where - NodePtr: 'static, -{ + operator_lookup: &'a dyn OperatorHandler, + pre_eval: Option>, +) -> Response { let mut rpc = RunProgramContext::new(allocator, quote_kw, apply_kw, operator_lookup, pre_eval); rpc.run_program(program, args, max_cost) } diff --git a/src/tests.rs b/src/tests.rs index 4e3e4666..25754473 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -172,9 +172,7 @@ fn node_to_hex(node: &Node) -> String { fn do_test_run_program(input_as_hex: &str, expected_as_hex: &str) -> () { let mut a = Allocator::new(); let n = node_from_hex(&a, input_as_hex); - println!("n = {:?}", n); let r = do_run_program(&n, &null); - println!("r = {:?}", r); assert_eq!(node_to_hex(&r), expected_as_hex); }