1
use std::ffi::c_void;
2
use std::path::Path;
3
use std::path::PathBuf;
4

            
5
use libloading::Library;
6
use libloading::Symbol;
7
use log::info;
8
use tempfile::TempDir;
9
use tempfile::tempdir;
10
use toml::Table;
11

            
12
use merc_data::DataExpression;
13
use merc_sabre::RewriteEngine;
14
use merc_sabre::RewriteSpecification;
15
use merc_sabre_ffi::DataExpressionFFI;
16
use merc_sabre_ffi::DataExpressionRefFFI;
17
use merc_sabre_ffi::SabreRewriteVTable;
18
use merc_sabre_ffi::data_expression_ref_from_term;
19
use merc_sabre_ffi::into_data_expression;
20
use merc_sabre_ffi::rewrite_vtable;
21
use merc_utilities::MercError;
22

            
23
use crate::generate;
24
use crate::library::RuntimeLibrary;
25

            
26
pub struct SabreCompilingRewriter {
27
    /// Cached `rewrite` entry point of `library`. Resolved once in `new`; valid
28
    /// for as long as `library` stays loaded.
29
    rewrite_fn: extern "C-unwind" fn(&DataExpressionRefFFI<'_>) -> DataExpressionFFI,
30
    /// Keeps every term hose raw address is baked into the generated
31
    /// library protected.
32
    _spec: RewriteSpecification,
33
    /// The loaded library, keeps rewrite_fn alive.
34
    _library: Library,
35
    /// Keep the temporary directory alive so it is not removed while the
36
    /// library is still mapped. `None` when a caller-managed local directory is
37
    /// used instead.
38
    _temp_dir: Option<TempDir>,
39
}
40

            
41
impl RewriteEngine for SabreCompilingRewriter {
42
1
    fn rewrite(&mut self, term: &DataExpression) -> DataExpression {
43
1
        let result = (self.rewrite_fn)(&data_expression_ref_from_term(term));
44

            
45
        // SAFETY: `result` is an owned handle produced by the generated
46
        // `rewrite` entry point (always via the host `create`/`protect`), so it
47
        // wraps a live `Box<DataExpression>`.
48
1
        unsafe { into_data_expression(result) }
49
1
    }
50
}
51

            
52
impl SabreCompilingRewriter {
53
    /// Creates a new compiling rewriter for the given specifications.
54
    ///
55
    /// - use_local_workspace: Use the development version of the toolset instead of referring to the github one.
56
    /// - use_local_tmp: Use a relative 'tmp' directory instead of using the system directory. Mostly used for debugging purposes.
57
    ///
58
    /// - [`RewriteEngine`]
59
1
    pub fn new(
60
1
        spec: &RewriteSpecification,
61
1
        use_local_workspace: bool,
62
1
        use_local_tmp: bool,
63
1
    ) -> Result<SabreCompilingRewriter, MercError> {
64
        // Only allocate a system temporary directory when one is needed; the
65
        // local-tmp path uses a fixed `./tmp` directory instead.
66
1
        let system_tmp_dir = if use_local_tmp { None } else { Some(tempdir()?) };
67
1
        let temp_dir = match &system_tmp_dir {
68
            Some(dir) => dir.path(),
69
1
            None => Path::new("./tmp"),
70
        };
71

            
72
1
        let compilation_toml = include_str!(concat!(env!("OUT_DIR"), "/Compilation.toml")).parse::<Table>()?;
73
1
        let sabrec = compilation_toml.get("sabrec").ok_or("Missing [sabrec] section")?;
74

            
75
1
        let mut dependencies = vec![];
76

            
77
1
        if use_local_workspace {
78
1
            let path = sabrec
79
1
                .get("path")
80
1
                .ok_or("Missing path entry")?
81
1
                .as_str()
82
1
                .ok_or("Not a string")?;
83

            
84
1
            info!("Using local dependency {path}");
85
1
            dependencies.push(format!(
86
                "merc_sabre-ffi = {{ path = '{}' }}",
87
1
                PathBuf::from(path)
88
1
                    .join("../../crates/sabre_compiling/sabre_ffi")
89
1
                    .to_string_lossy()
90
            ));
91
        } else {
92
            // Pin to the host's git commit so the loaded library's `#[repr(C)]`
93
            // vtable layout matches the host exactly.
94
            let repository = "https://github.com/MERCorg/merc.git";
95
            let commit = sabrec.get("commit").and_then(|c| c.as_str()).unwrap_or_default();
96

            
97
            if commit.is_empty() {
98
                info!("Using git dependency {repository} (unpinned; no commit recorded at build time)");
99
                dependencies.push(format!("merc_sabre-ffi = {{ git = '{repository}' }}"));
100
            } else {
101
                info!("Using git dependency {repository} pinned to {commit}");
102
                dependencies.push(format!("merc_sabre-ffi = {{ git = '{repository}', rev = '{commit}' }}"));
103
            }
104
        }
105

            
106
1
        let mut compilation_crate = RuntimeLibrary::new(temp_dir, dependencies)?;
107

            
108
        // Write the output source file(s).
109
1
        generate(spec, compilation_crate.source_dir())?;
110

            
111
1
        let library = compilation_crate.compile()?;
112

            
113
        // Install the host vtable into the loaded library exactly once and cache
114
        // the `rewrite` entry point. All term-pool access in the library is
115
        // routed back through the vtable, so the library never touches its own
116
        // (duplicated) term pool.
117
        //
118
        // SAFETY: `initialise` and `rewrite` have the signatures emitted by the
119
        // code generator. The vtable's function pointers live in the host binary
120
        // for the whole process, satisfying the contract of `set_rewrite_vtable`.
121
1
        let rewrite_fn = unsafe {
122
1
            let initialise: Symbol<extern "C-unwind" fn(*mut c_void)> = library.get(b"initialise")?;
123
1
            let vtable: SabreRewriteVTable = rewrite_vtable();
124
1
            initialise(std::ptr::addr_of!(vtable) as *mut c_void);
125

            
126
1
            let rewrite: Symbol<extern "C-unwind" fn(&DataExpressionRefFFI<'_>) -> DataExpressionFFI> =
127
1
                library.get(b"rewrite")?;
128
1
            *rewrite
129
        };
130

            
131
1
        Ok(SabreCompilingRewriter {
132
1
            rewrite_fn,
133
1
            _spec: spec.clone(),
134
1
            _library: library,
135
1
            _temp_dir: system_tmp_dir,
136
1
        })
137
1
    }
138
}
139

            
140
#[cfg(test)]
141
mod tests {
142
    use test_log::test;
143

            
144
    use merc_data::to_untyped_data_expression;
145
    use merc_rec_tests::load_rec_from_strings;
146
    use merc_sabre::RewriteEngine;
147

            
148
    use super::SabreCompilingRewriter;
149

            
150
    #[test]
151
    #[cfg_attr(miri, ignore)] // Miri does not support FFI.
152
1
    fn test_sabre_compiling_example() {
153
1
        let (spec, terms) = load_rec_from_strings(&[
154
1
            include_str!("../../../examples/REC/rec/factorial5.rec"),
155
1
            include_str!("../../../examples/REC/rec/factorial.rec"),
156
1
        ])
157
1
        .unwrap();
158

            
159
1
        let spec = spec.to_rewrite_spec();
160
1
        let mut rewriter = SabreCompilingRewriter::new(&spec, true, true).unwrap();
161

            
162
1
        for t in terms {
163
1
            let data_term = to_untyped_data_expression(t, None);
164
1
            let rewritten_term = rewriter.rewrite(&data_term);
165

            
166
1
            println!("Original term: {data_term}");
167
1
            println!("Rewritten term: {rewritten_term}");
168

            
169
1
            assert_eq!(
170
362
                rewritten_term.to_string().chars().filter(|c| *c == 's').count(),
171
                120, // 5! = 120.
172
                "The rewritten result does not match the expected result"
173
            );
174
        }
175
1
    }
176
}