spl/oxidizer/
mod.rs

1//! This module creates a rust application that runs the desired SPL.
2//! At its current stage, this is just parsing and rewriting `@rust` functions from SPL into actual rust.
3//! The future plan is for this to effectively become a compiler.
4
5use std::{
6    collections::{hash_map::DefaultHasher, HashMap},
7    env,
8    ffi::OsStr,
9    fs,
10    hash::{Hash, Hasher},
11    io,
12    process::{Child, Command, Stdio},
13};
14
15use crate::{FuncImplType, Keyword, Word, Words};
16
17mod splrs;
18
19/// A specially compiled SPL version with custom parameters included.
20pub struct RustApp {
21    /// The path to the binary
22    binary: String,
23    /// The path to the project dir
24    dir: String,
25}
26
27impl RustApp {
28    /// Gets the path to the binary
29    pub fn get_binary(&self) -> &str {
30        &self.binary
31    }
32
33    /// Executes the binary with some args
34    pub fn execute<I, S>(&self, args: I) -> Result<Child, io::Error>
35    where
36        I: IntoIterator<Item = S>,
37        S: AsRef<OsStr>,
38    {
39        Command::new(self.binary.clone()).args(args).spawn()
40    }
41
42    /// Deletes the binary and rust project
43    pub fn delete(self) {
44        fs::remove_dir_all(self.dir).expect("unable to delete RustApp");
45    }
46}
47
48/// A rust function which was embedded in SPL
49pub struct RustFunction {
50    fn_name: String,
51    content: String,
52}
53
54/// A builder for [`RustApp`]s. This is work-in-progress.
55pub struct RustAppBuilder {
56    rust_functions: Vec<RustFunction>,
57    to_embed: HashMap<String, String>,
58    default_file: String,
59    name: Option<String>,
60}
61
62impl Hash for RustAppBuilder {
63    fn hash<H: Hasher>(&self, state: &mut H) {
64        state.write_usize(self.rust_functions.len());
65        for f in &self.rust_functions {
66            f.fn_name.hash(state);
67        }
68        for (k, _) in &self.to_embed {
69            k.hash(state);
70        }
71    }
72}
73
74impl RustAppBuilder {
75    pub fn new() -> RustAppBuilder {
76        Self {
77            rust_functions: Vec::new(),
78            to_embed: HashMap::new(),
79            default_file: "repl.spl".to_owned(),
80            name: None,
81        }
82    }
83
84    /// Embeds a file into the desired app
85    pub fn add_source(&mut self, name: String, source: String) {
86        self.to_embed.insert(name, source);
87    }
88
89    /// Sets the name of the folder it will sit in.
90    pub fn set_name(&mut self, name: String) {
91        self.name = Some(name);
92    }
93
94    /// Adds all `@rust` functions from the given SPL code's top level. Does NOT scan for lower levels at this time.
95    pub fn prepare(&mut self, spl: Words) -> bool {
96        let mut needs_new = false;
97        for word in spl.words {
98            match word {
99                Word::Key(Keyword::FuncOf(name, content, FuncImplType::Rust)) => {
100                    self.rust_functions.push(splrs::to_rust(name, content));
101                    needs_new = true;
102                }
103                _ => (),
104            }
105        }
106        needs_new
107    }
108
109    /// Sets the default file to start when none is provided. This will not work if the file is not also embedded.
110    pub fn set_default_file(&mut self, name: String) {
111        self.default_file = name;
112    }
113
114    /// Builds the desired app, including literally building it using cargo.
115    pub fn build(self, output: bool) -> Result<RustApp, io::Error> {
116        // we need a temp folder!
117        let tmp = "."; // TODO replace?
118        let name = match self.name {
119            Some(x) => x,
120            None => {
121                let mut hash = DefaultHasher::new();
122                self.hash(&mut hash);
123                let hash = hash.finish();
124                hash.to_string()
125            }
126        };
127        let _ = Command::new("cargo")
128            .arg("new")
129            .arg(format!("spl-{name}"))
130            .current_dir(tmp)
131            .stdout(Stdio::null())
132            .stderr(Stdio::null())
133            .spawn()
134            .unwrap()
135            .wait();
136        Command::new("cargo")
137            .arg("add")
138            .arg(format!("spl@{}", env!("CARGO_PKG_VERSION")))
139            .current_dir(format!("{tmp}/spl-{name}"))
140            .stdout(Stdio::null())
141            .stderr(if output { Stdio::inherit() } else { Stdio::null() })
142            .spawn()
143            .unwrap()
144            .wait()?;
145        let mut runtime_init = String::new();
146        let mut code = String::new();
147        for func in self.rust_functions.into_iter().enumerate() {
148            code += &format!(
149                "fn spl_oxidizer_{}(stack: &mut Stack) -> OError {{ {} Ok(()) }}\n",
150                func.0, func.1.content
151            );
152            runtime_init += &format!(
153                "rt.native_functions.insert({:?}, (0, FuncImpl::Native(spl_oxidizer_{})));\n",
154                func.1.fn_name, func.0
155            )
156        }
157        for (name, data) in self.to_embed.into_iter() {
158            runtime_init += &format!("rt.embedded_files.insert({:?}, {:?});\n", name, data);
159        }
160        fs::write(
161            format!("{tmp}/spl-{name}/src/main.rs"),
162            stringify! {
163                use spl::{runtime::*, *};
164
165                use std::env::args;
166
167                fn main() {
168                    let mut rt = Runtime::new();
169                    runtime_init
170                    rt.set();
171                    if let Err(x) = start_file_in_runtime(
172                        &args()
173                            .nth(1)
174                            .unwrap_or("default_file".to_owned()),
175                    ) {
176                        println!("{x:?}");
177                    }
178                    Runtime::reset();
179                }
180            }
181            .to_owned()
182            .replace("default_file", &self.default_file)
183            .replace("runtime_init", &runtime_init)
184                + &code,
185        )?;
186        Command::new("cargo")
187            .arg("build")
188            .arg("--release")
189            .current_dir(format!("{tmp}/spl-{name}"))
190            .stdout(if output {
191                Stdio::inherit()
192            } else {
193                Stdio::null()
194            })
195            .stderr(if output {
196                Stdio::inherit()
197            } else {
198                Stdio::null()
199            })
200            .spawn()
201            .unwrap()
202            .wait()?;
203        Ok(RustApp {
204            dir: format!("{tmp}/spl-{name}"),
205            binary: {
206                // insanity. will have to clean this up at some point.
207                let dir = format!("{tmp}/spl-{name}/target/release/");
208                fs::read_dir(dir)
209                    .expect("unable to build: dir was not created.")
210                    .filter(|x| {
211                        let x = x
212                            .as_ref()
213                            .expect("file system did something i cannot comprehend");
214                        let n = x.file_name().into_string().unwrap();
215                        x.file_type().expect("file system uhhh?????").is_file()
216                            && !n.ends_with(".d")
217                            && !n.starts_with(".")
218                    })
219                    .next()
220                    .expect("cargo was unable to build the binary")
221                    .expect("file system did something i cannot comprehend")
222                    .path()
223                    .into_os_string()
224                    .into_string()
225                    .expect("bad unicode in file path")
226            },
227        })
228    }
229}
230
231impl Default for RustAppBuilder {
232    fn default() -> Self {
233        Self::new()
234    }
235}