1use std::fmt;
42use std::path::{Path, PathBuf};
43use std::time::SystemTime;
44
45use glam::Vec3;
46use rhai::{Dynamic, Engine, Scope, AST};
47
48const MAX_OPERATIONS: u64 = 50_000;
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct ScriptedError {
54 pub message: String,
56}
57
58impl fmt::Display for ScriptedError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(&self.message)
61 }
62}
63
64impl std::error::Error for ScriptedError {}
65
66fn err(message: impl Into<String>) -> ScriptedError {
67 ScriptedError { message: message.into() }
68}
69
70pub struct ScriptedSystem {
72 engine: Engine,
73 ast: AST,
74 source: String,
75 file: Option<(PathBuf, Option<SystemTime>)>,
76}
77
78impl fmt::Debug for ScriptedSystem {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.debug_struct("ScriptedSystem")
81 .field("file", &self.file.as_ref().map(|(p, _)| p))
82 .field("source", &self.source)
83 .finish()
84 }
85}
86
87fn make_engine() -> Engine {
88 let mut engine = Engine::new();
89 engine.set_max_operations(MAX_OPERATIONS);
90 engine.set_max_expr_depths(64, 32);
91 engine.set_max_string_size(4096);
92 engine.set_max_array_size(1024);
93 engine.on_print(|s| log::info!("script: {s}"));
95 engine.on_debug(|s, _, pos| log::debug!("script {pos}: {s}"));
96 engine
97}
98
99fn mtime(path: &Path) -> Option<SystemTime> {
100 std::fs::metadata(path).and_then(|m| m.modified()).ok()
101}
102
103fn number(v: &Dynamic, i: usize) -> Result<f32, ScriptedError> {
104 if let Ok(f) = v.as_float() {
105 Ok(f as f32)
106 } else if let Ok(n) = v.as_int() {
107 Ok(n as f32)
108 } else {
109 Err(err(format!("element {i} of the result is a {}, not a number", v.type_name())))
110 }
111}
112
113impl ScriptedSystem {
114 pub fn compile(source: &str) -> Result<Self, ScriptedError> {
116 let engine = make_engine();
117 let ast = engine.compile(source).map_err(|e| err(format!("compile error: {e}")))?;
118 let sys = Self { engine, ast, source: source.to_string(), file: None };
119 sys.derivative(Vec3::ZERO, 0.0)?;
122 Ok(sys)
123 }
124
125 pub fn load(path: impl AsRef<Path>) -> Result<Self, ScriptedError> {
128 let path = path.as_ref();
129 let source = std::fs::read_to_string(path)
130 .map_err(|e| err(format!("could not read {}: {e}", path.display())))?;
131 let mut sys = Self::compile(&source).map_err(|e| err(format!("{}: {e}", path.display())))?;
132 sys.file = Some((path.to_path_buf(), mtime(path)));
133 Ok(sys)
134 }
135
136 pub fn source(&self) -> &str {
138 &self.source
139 }
140
141 pub fn path(&self) -> Option<&Path> {
143 self.file.as_ref().map(|(p, _)| p.as_path())
144 }
145
146 pub fn reload_if_changed(&mut self) -> Result<bool, ScriptedError> {
153 let Some((path, seen)) = self.file.clone() else { return Ok(false) };
154 let now = mtime(&path);
155 if now.is_none() || now == seen {
156 return Ok(false);
157 }
158 if let Some((_, t)) = self.file.as_mut() {
161 *t = now;
162 }
163 let fresh = Self::load(&path)?;
164 self.ast = fresh.ast;
165 self.source = fresh.source;
166 Ok(true)
167 }
168
169 pub fn derivative(&self, p: Vec3, t: f32) -> Result<Vec3, ScriptedError> {
171 let mut scope = Scope::new();
172 scope.push("x", p.x as f64);
173 scope.push("y", p.y as f64);
174 scope.push("z", p.z as f64);
175 scope.push("t", t as f64);
176 let out: Dynamic = self
177 .engine
178 .eval_ast_with_scope(&mut scope, &self.ast)
179 .map_err(|e| err(format!("runtime error: {e}")))?;
180 let arr = out
181 .into_typed_array::<Dynamic>()
182 .map_err(|ty| err(format!("the script must end with [dx, dy, dz], got a {ty}")))?;
183 if arr.len() != 3 {
184 return Err(err(format!("the script must end with [dx, dy, dz], got {} elements", arr.len())));
185 }
186 let v = Vec3::new(number(&arr[0], 0)?, number(&arr[1], 1)?, number(&arr[2], 2)?);
187 if !v.is_finite() {
188 return Err(err(format!("the derivative at {p} is not finite: {v}")));
189 }
190 Ok(v)
191 }
192
193 pub fn rk4_step(&self, p: Vec3, t: f32, dt: f32) -> Result<Vec3, ScriptedError> {
196 let h = dt * 0.5;
197 let k1 = self.derivative(p, t)?;
198 let k2 = self.derivative(p + k1 * h, t + h)?;
199 let k3 = self.derivative(p + k2 * h, t + h)?;
200 let k4 = self.derivative(p + k3 * dt, t + dt)?;
201 Ok(p + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::math::attractors::{rk4_step, AttractorType};
209
210 const LORENZ: &str = "
211 let sigma = 10.0;
212 let rho = 28.0;
213 let beta = 8.0 / 3.0;
214 [sigma * (y - x), x * (rho - z) - y, x * y - beta * z]
215 ";
216
217 #[test]
218 fn a_scripted_lorenz_tracks_the_built_in_one() {
219 let sys = ScriptedSystem::compile(LORENZ).unwrap();
220 let (mut a, mut b) = (Vec3::new(1.0, 1.0, 1.0), Vec3::new(1.0, 1.0, 1.0));
221 for i in 0..200 {
222 a = sys.rk4_step(a, i as f32 * 0.005, 0.005).unwrap();
223 b = rk4_step(AttractorType::Lorenz, b, 0.005);
224 }
225 assert!((a - b).length() < 1e-2, "{a} vs {b}");
228 }
229
230 #[test]
231 fn ints_floats_maths_and_time_all_work() {
232 let sys = ScriptedSystem::compile("[1, 2.5 * x, sin(t) + PI() * 0.0]").unwrap();
233 let d = sys.derivative(Vec3::new(2.0, 0.0, 0.0), std::f32::consts::FRAC_PI_2).unwrap();
234 assert_eq!(d, Vec3::new(1.0, 5.0, 1.0));
235 }
236
237 #[test]
238 fn bad_scripts_are_errors_not_panics() {
239 for (src, needle) in [
240 ("[x, y", "compile error"),
241 ("x + y", "must end with"),
242 ("[x, y]", "got 2 elements"),
243 ("[x, \"y\", z]", "not a number"),
244 ("[undefined_var, y, z]", "runtime error"),
245 ("loop { }", "runtime error"),
246 ("[1.0 / 0.0, 0.0, 0.0]", "not finite"),
247 ] {
248 let e = ScriptedSystem::compile(src).unwrap_err();
249 assert!(e.message.contains(needle), "{src:?}: {}", e.message);
250 }
251 }
252
253 #[test]
254 fn hot_reload_swaps_in_new_equations_and_survives_broken_edits() {
255 let dir = std::env::temp_dir().join("proof_engine_scripted_tests");
256 std::fs::create_dir_all(&dir).unwrap();
257 let path = dir.join("field.rhai");
258 std::fs::write(&path, "[1.0, 0.0, 0.0]").unwrap();
259 let mut sys = ScriptedSystem::load(&path).unwrap();
260 assert_eq!(sys.path(), Some(path.as_path()));
261 assert_eq!(sys.reload_if_changed(), Ok(false));
262
263 let bump = |p: &Path| {
265 let f = std::fs::File::options().write(true).open(p).unwrap();
266 let later = mtime(p).unwrap() + std::time::Duration::from_secs(2);
267 f.set_modified(later).unwrap();
268 };
269
270 std::fs::write(&path, "[0.0, 2.0, 0.0]").unwrap();
271 bump(&path);
272 assert_eq!(sys.reload_if_changed(), Ok(true));
273 assert_eq!(sys.derivative(Vec3::ZERO, 0.0).unwrap(), Vec3::new(0.0, 2.0, 0.0));
274
275 std::fs::write(&path, "[0.0, 3.0,").unwrap();
277 bump(&path);
278 assert!(sys.reload_if_changed().is_err());
279 assert_eq!(sys.reload_if_changed(), Ok(false), "the broken version is not retried every frame");
280 assert_eq!(sys.derivative(Vec3::ZERO, 0.0).unwrap(), Vec3::new(0.0, 2.0, 0.0));
281 }
282}