Skip to main content

hax_rust_engine/
ocaml_engine.rs

1//! This module implements an interface to the OCaml hax engine. Via this
2//! interface, the rust engine can communicate with the OCaml engine, and reuse
3//! some of its components.
4
5use std::{io::BufRead, sync::OnceLock};
6
7use hax_frontend_exporter::{
8    ThirBody,
9    id_table::{Table, WithTable},
10};
11use hax_types::engine_api::protocol::{FromEngine, ToEngine};
12use serde::Deserialize;
13
14/// A query for the OCaml engine
15#[derive(Debug, Clone, ::schemars::JsonSchema, ::serde::Deserialize, ::serde::Serialize)]
16pub struct Query {
17    #[serde(flatten)]
18    meta: Meta,
19    /// The kind of query we want to send to the engine
20    kind: QueryKind,
21}
22
23/// The metadata required to perform a query.
24#[derive(Debug, Clone, ::schemars::JsonSchema, ::serde::Deserialize, ::serde::Serialize)]
25pub struct Meta {
26    /// The version of hax currently used
27    pub hax_version: String,
28    /// Dictionary from `DefId`s to `impl_infos`
29    pub impl_infos: Vec<(
30        hax_frontend_exporter::DefId,
31        hax_frontend_exporter::ImplInfos,
32    )>,
33    /// Enable debugging of phases in the OCaml engine
34    pub debug_bind_phase: bool,
35    /// Enable profiling in the OCaml engine
36    pub profiling: bool,
37}
38
39static STATE: OnceLock<Meta> = OnceLock::new();
40
41/// Initialize query metadata.
42pub fn initialize(meta: Meta) {
43    STATE
44        .set(meta)
45        .expect("`ocaml_engine::initialize` was called more than once")
46}
47
48/// The payload of the query. [`Response`] below mirrors this enum to represent
49/// the response from the engine.
50#[derive(Debug, Clone, ::schemars::JsonSchema, ::serde::Deserialize, ::serde::Serialize)]
51pub enum QueryKind {
52    /// Ask the OCaml engine to import the given THIR from the frontend
53    ImportThir {
54        /// The input THIR items
55        input: Vec<hax_frontend_exporter::Item<ThirBody>>,
56        /// Translation options which contains include clauses (items filtering)
57        translation_options: hax_types::cli_options::TranslationOptions,
58    },
59
60    /// Ask the OCaml engine to run given phases on given items
61    ApplyPhases {
62        /// The phases to run. See `untyped_phases.ml`.
63        phases: Vec<String>,
64        /// The items on which the phases will be applied.
65        input: Vec<crate::ast::Item>,
66    },
67
68    /// Ask the OCaml engine to call an OCaml printer
69    Print {
70        /// Which printer to use
71        printer: hax_types::cli_options::Backend<()>,
72        /// The items after applying the phases.
73        input: Vec<crate::ast::Item>,
74    },
75}
76/// A Response after a [`Query`]
77#[derive(Debug, Clone, ::schemars::JsonSchema, ::serde::Deserialize, ::serde::Serialize)]
78pub enum Response {
79    /// Return imported THIR as an internal AST from Rust engine
80    ImportThir {
81        /// The output Rust AST items
82        output: Vec<crate::ast::Item>,
83    },
84    /// Return items after phase application
85    ApplyPhases {
86        /// The output Rust AST items after phases
87        output: Vec<crate::ast::Item>,
88    },
89    /// Printing was done successfully
90    PrintOk,
91}
92
93/// Extends the common `FromEngine` messages with one extra case: `Response`.
94#[derive(Debug, Clone, ::schemars::JsonSchema, ::serde::Deserialize, ::serde::Serialize)]
95#[serde(untagged)]
96pub enum ExtendedFromEngine {
97    /// A standard `FromEngine` message
98    FromEngine(FromEngine),
99    /// A `Response`
100    Response(Response),
101}
102
103impl QueryKind {
104    /// Execute the query synchronously.
105    pub fn execute(self, table: Option<Table>) -> Option<Response> {
106        let query = Query {
107            meta: STATE
108                .get()
109                .expect("`ocaml_engine::initialize` should be called first")
110                .clone(),
111            kind: self,
112        };
113        use std::io::Write;
114        use std::process::Command;
115
116        macro_rules! send {
117            ($where: expr, $value:expr) => {
118                serde_json::to_writer(&mut $where, $value).unwrap();
119                $where.write_all(b"\n").unwrap();
120                $where.flush().unwrap();
121            };
122        }
123
124        let mut engine_subprocess =
125            Command::new(std::env::var("HAX_ENGINE_BINARY").unwrap_or("hax-engine".into()))
126                .arg("driver_rust_engine")
127                .stdin(std::process::Stdio::piped())
128                .stdout(std::process::Stdio::piped())
129                .spawn()
130                .unwrap();
131
132        let mut stdin = std::io::BufWriter::new(
133            engine_subprocess
134                .stdin
135                .as_mut()
136                .expect("Could not write on stdin"),
137        );
138
139        if let Some(table) = table {
140            WithTable::run(table, query, |with_table| {
141                send!(stdin, with_table);
142            });
143        } else {
144            send!(stdin, &(vec![] as Vec<()>, query));
145        }
146
147        let mut response = None;
148        let stdout = std::io::BufReader::new(engine_subprocess.stdout.take().unwrap());
149        // TODO: this should be streaming (i.e. use a `LineAsEOF` reader wrapper that consumes a reader until `\n` occurs)
150        //       See https://github.com/cryspen/hax/issues/1537.
151        for slice in stdout.split(b'\n') {
152            let msg = (|| {
153                let slice = slice.ok()?;
154                let mut de = serde_json::Deserializer::from_slice(&slice);
155                de.disable_recursion_limit();
156                let de = serde_stacker::Deserializer::new(&mut de);
157                let msg = ExtendedFromEngine::deserialize(de);
158                msg.ok()
159            })()
160            .expect(
161                "Hax engine sent an invalid json value. \
162                                This might be caused by debug messages on stdout, \
163                                which is reserved for JSON communication with cargo-hax",
164            );
165
166            match msg {
167                ExtendedFromEngine::Response(res) => response = Some(res),
168                ExtendedFromEngine::FromEngine(FromEngine::Exit) => break,
169                // Proxy messages from the OCaml engine
170                ExtendedFromEngine::FromEngine(from_engine) => {
171                    crate::hax_io::write(&from_engine);
172                    if from_engine.requires_response() {
173                        let response: ToEngine = crate::hax_io::read_to_engine_message();
174                        send!(stdin, &response);
175                    }
176                }
177            }
178        }
179        drop(stdin);
180
181        let exit_status = engine_subprocess.wait().unwrap();
182        if !exit_status.success() {
183            panic!("ocaml engine crashed");
184        }
185
186        response
187    }
188}