Skip to main content

grabapl_template_ffi/
lib.rs

1//! This FFI crate exposes any functionality of [`grabapl`] and the custom semantics to other languages.
2//!
3//! This example uses the [Diplomat] tool to automatically generate idiomatic FFI bindings to
4//! multiple target languages.
5//!
6//! See the main `README.md` for information on how to build this crate and integrate it
7//! into a different language project.
8//!
9//! For more inspiration on FFI crates, the other clients in the `example_clients` directory.
10//!
11//! [Diplomat]: https://github.com/rust-diplomat/diplomat/
12
13use ::syntax::custom_syntax::CustomSyntax;
14use ::syntax::interpreter::lex_then_parse;
15use grabapl::operation::builder::IntermediateState;
16use grabapl::prelude::*;
17use semantics::*;
18
19type RustOperationContext = OperationContext<TheSemantics>;
20type RustIntermediateState = IntermediateState<TheSemantics>;
21type RustConcreteGraph = ConcreteGraph<TheSemantics>;
22type RustOperationBuilder<'a> = OperationBuilder<'a, TheSemantics>;
23
24fn parse_node_value(s: &str) -> Option<NodeValue> {
25    let parser = syntax::node_value_parser();
26    lex_then_parse(s, parser).ok()
27}
28
29fn parse_edge_value(s: &str) -> Option<EdgeValue> {
30    let parser = syntax::edge_value_parser();
31    lex_then_parse(s, parser).ok()
32}
33
34fn parse_node_type(s: &str) -> Result<NodeType, String> {
35    let parser = syntax::TheCustomSyntax::get_node_type_parser();
36    lex_then_parse(s, parser).map_err(|e| e.to_string())
37}
38
39fn parse_edge_type(s: &str) -> Result<EdgeType, String> {
40    let parser = syntax::TheCustomSyntax::get_edge_type_parser();
41    lex_then_parse(s, parser).map_err(|e| e.to_string())
42}
43
44/// This module is sent to Diplomat to automatically generate FFI bindings from functions and types
45/// on both the Rust and the target language side.
46///
47/// See [The Diplomat Book] for detailed information on how and which types and functions can be
48/// exposed with Diplomat.
49///
50/// In general, we will create a `diplomat::opaque` wrapper type for every type we want to expose,
51/// which must be created with a `Box<Self>` return type.
52///
53/// [The Diplomat Book]: https://rust-diplomat.github.io/diplomat/
54#[diplomat::bridge]
55pub mod ffi {
56    use std::collections::HashMap;
57    // we need to import this to use the write! macro
58    use super::RustIntermediateState;
59    use super::RustOperationBuilder;
60    use super::RustOperationContext;
61    use super::TheSemantics;
62    use super::{OperationId, RustConcreteGraph};
63    use error_stack::fmt::ColorMode;
64    use grabapl::NodeKey;
65    use std::fmt::Write as _;
66
67    /// Holds a bunch of top-level functions.
68    #[diplomat::opaque]
69    pub struct Grabapl;
70
71    impl Grabapl {
72        /// Call this function at the beginning of your program to initialize useful
73        /// Rust panic error messages.
74        pub fn init() {
75            // NOTE: without this call, the "error: failed to find intrinsics to enable `clone_ref` function" error
76            //  may be issued by `wasm-bindgen`.
77            // Most likely a bug in `wasm-bindgen`.
78            console_error_panic_hook::set_once();
79            // change error-stack's color mode
80            error_stack::Report::set_color_mode(ColorMode::None);
81            // print something to console to indicate that the library has been initialized
82            log::info!("Grabapl FFI initialized");
83        }
84
85        /// Parses a source file.
86        pub fn parse(src: &str) -> Box<CompileResult> {
87            let raw_res = syntax::try_parse_to_op_ctx_and_map(
88                src,
89                false, /* disable colored error messages - see the online_syntax demo for how to handle them */
90            );
91            let op_ctx_and_map_res = raw_res
92                .op_ctx_and_map
93                // turn function names into owned strings
94                .map(|(op_ctx, map)| {
95                    let state_map = map
96                        .into_iter()
97                        .map(|(k, v)| (k.into(), v))
98                        .collect::<HashMap<String, _>>();
99                    (op_ctx, state_map)
100                })
101                // project away line/col spans for brevity
102                .map_err(|e| e.value);
103
104            Box::new(CompileResult {
105                op_ctx_and_map_res,
106                state_map: raw_res.state_map,
107            })
108        }
109    }
110
111    /// Represents a concrete graph, i.e., the runtime state of a program.
112    #[diplomat::opaque]
113    pub struct ConcreteGraph(RustConcreteGraph);
114
115    impl ConcreteGraph {
116        /// Creates a new empty concrete graph.
117        pub fn create() -> Box<ConcreteGraph> {
118            Box::new(ConcreteGraph(RustConcreteGraph::new()))
119        }
120
121        /// Returns the DOT representation of the concrete graph.
122        pub fn dot(&self, out: &mut DiplomatWrite) {
123            write!(out, "{}", self.0.dot()).unwrap();
124        }
125
126        /// Adds a new node to the concrete graph with the given value and returns its key.
127        pub fn add_node(&mut self, value: &str) -> Result<u32, Box<StringError>> {
128            let node_value = super::parse_node_value(value)
129                .ok_or_else(|| StringError::from_boxed(format!("Invalid node value: {}", value)))?;
130            let node_key = self.0.add_node(node_value);
131            Ok(node_key.0)
132        }
133
134        /// Adds an edge from the node with key `from` to the node with key `to` with the given value.
135        pub fn add_edge(
136            &mut self,
137            from: u32,
138            to: u32,
139            value: &str,
140        ) -> Result<(), Box<StringError>> {
141            let edge_value = super::parse_edge_value(value)
142                .ok_or_else(|| StringError::from_boxed(format!("Invalid edge value: {}", value)))?;
143            let from_key = NodeKey(from);
144            let to_key = NodeKey(to);
145            self.0.add_edge(from_key, to_key, edge_value);
146            Ok(())
147        }
148    }
149
150    /// The operation context maps operation IDs (integers) to operations.
151    #[diplomat::opaque]
152    pub struct OperationContext(RustOperationContext);
153
154    impl OperationContext {
155        /// Creates a new operation context.
156        pub fn create() -> Box<OperationContext> {
157            let op_ctx = RustOperationContext::new();
158            // here you could populate op_ctx with a bunch of default builtin operations.
159            // in general, a user will probably want to specify the "const-generic" arguments
160            // of a builtin operation, like the constant to add in `TheOperation::AddConstant`,
161            // but they cannot do that here, since we have to pick a specific constant in order
162            // to add the operation. Hence calling builtin operations will typically involve
163            // explicit construction of the builtin operation to call and bypassing operation IDs.
164            // User defined operation will always be called via operation IDs and this context, however.
165            Box::new(OperationContext(op_ctx))
166        }
167    }
168
169    /// Represents the result of compiling a source file.
170    ///
171    /// The compilation may have failed, but there may still be valid intermediate states to print.
172    /// To check for errors and access the programs, call getProgram().
173    #[diplomat::opaque]
174    pub struct CompileResult {
175        op_ctx_and_map_res: Result<(RustOperationContext, HashMap<String, OperationId>), String>,
176        state_map: HashMap<String, RustIntermediateState>,
177    }
178
179    impl CompileResult {
180        /// Returns the DOT representation of the intermediate state named `state`.
181        pub fn dot_of_state(&self, state: &str, dot_out: &mut DiplomatWrite) {
182            let Some(state) = self.state_map.get(state) else {
183                log::error!("state does not exist in state map");
184                return;
185            };
186            write!(dot_out, "{}", state.dot_with_aid()).unwrap();
187        }
188
189        /// If the compilation was successful, this returns a `Program` that can be used to run operations.
190        ///
191        /// Otherwise, this throws an error.
192        pub fn get_program(&self) -> Result<Box<Program>, Box<StringError>> {
193            match &self.op_ctx_and_map_res {
194                Ok((op_ctx, fn_map)) => {
195                    let program = Program {
196                        op_ctx: op_ctx.clone(),
197                        fn_map: fn_map.clone(),
198                    };
199                    Ok(Box::new(program))
200                }
201                Err(err) => Err(Box::new(StringError(err.to_string()))),
202            }
203        }
204    }
205
206    /// Represents a program that can be executed.
207    #[diplomat::opaque]
208    pub struct Program {
209        op_ctx: RustOperationContext,
210        fn_map: HashMap<String, OperationId>,
211    }
212
213    impl Program {
214        /// Returns a copy of the operation context.
215        pub fn op_ctx(&self) -> Box<OperationContext> {
216            Box::new(OperationContext(self.op_ctx.clone()))
217        }
218
219        /// Runs the operation with the given name on the provided concrete graph with the given arguments.
220        pub fn run_operation(
221            &self,
222            g: &mut ConcreteGraph,
223            op_name: &str,
224            args: &[u32],
225        ) -> Result<(), Box<StringError>> {
226            let op_id = self
227                .fn_map
228                .get(op_name)
229                .ok_or_else(|| StringError(format!("Operation '{}' not found", op_name)))?;
230            let args: Vec<_> = args.iter().copied().map(NodeKey).collect();
231            let res = super::run_from_concrete(&mut g.0, &self.op_ctx, *op_id, &args);
232            res.map_err(|e| Box::new(StringError(e.to_string())))
233                .map(|_| ())
234        }
235    }
236
237    /// A user defined operation that is currently being built using low-level instructions instead of
238    /// parsing via the syntax parser.
239    ///
240    /// This builder should probably be used to create an interactive interface for building user defined operations.
241    #[diplomat::opaque]
242    pub struct OperationBuilder<'a>(RustOperationBuilder<'a>);
243
244    impl<'a> OperationBuilder<'a> {
245        /// Creates a new operation builder for the given operation context and with the given self operation ID.
246        ///
247        /// The passed operation context holds the other user defined operations that can be used in the builder.
248        pub fn create(op_ctx: &'a OperationContext, self_op_id: u32) -> Box<OperationBuilder<'a>> {
249            let op_builder = RustOperationBuilder::new(&op_ctx.0, self_op_id);
250            Box::new(OperationBuilder(op_builder))
251        }
252
253        /// Adds an expected parameter node with the given name and type to the operation.
254        pub fn expect_parameter_node(
255            &mut self,
256            name: &str,
257            node_type: &str,
258        ) -> Result<(), Box<StringError>> {
259            let node_type = super::parse_node_type(node_type)
260                .map_err(|e| StringError::from_boxed(format!("Invalid node type: {}", e)))?;
261            self.0
262                .expect_parameter_node(name, node_type)
263                .map_err(|e| StringError::from_boxed(e.to_string()))
264        }
265
266        // TODO: add more of the desired builder operations here. See the main `OperationBuilder` documentation.
267    }
268
269    /// Catch this in a try-catch and print it with toString().
270    #[diplomat::opaque]
271    pub struct StringError(String);
272
273    impl StringError {
274        fn from_boxed(s: String) -> Box<StringError> {
275            Box::new(StringError(s))
276        }
277
278        #[diplomat::attr(auto, stringifier)]
279        pub fn to_string(&self, out: &mut DiplomatWrite) {
280            write!(out, "{}", self.0).unwrap();
281        }
282    }
283}