Skip to main content

wowlab_engine_domain/rotation/
interpreter.rs

1//! Interpreter shell around the generic lowerer.
2
3use wowlab_types::sim::Rotation;
4
5use super::{
6    buffer::{DenseBuffer, DescriptorTable},
7    context::ContextSchema,
8    decision_trace::DecisionTraceTarget,
9    error::Result,
10    interp_backend::{InterpBackend, InterpFrame},
11    lower,
12    resolver::SpecResolver,
13    result::EvalResult,
14};
15
16/// Interpreter backend (only choice on WASM; fallback on native without `jit`).
17#[derive(Debug)]
18pub struct InterpretedRotation {
19    rotation: Rotation,
20    resolver: SpecResolver,
21    schema: ContextSchema,
22    table: DescriptorTable,
23    sink: Option<DecisionTraceTarget>,
24}
25
26impl InterpretedRotation {
27    /// Build an interpreter from an already-prepared [`ContextSchema`].
28    #[must_use]
29    pub fn from_parts(rotation: Rotation, resolver: SpecResolver, schema: ContextSchema) -> Self {
30        Self {
31            rotation,
32            resolver,
33            schema,
34            table: DescriptorTable::build(),
35            sink: None,
36        }
37    }
38
39    /// Parse JSON and compile.
40    ///
41    /// # Errors
42    /// Returns an error when parsing, validation, or lowering fails.
43    pub fn compile_json(json: &str, resolver: &SpecResolver) -> Result<Self> {
44        let rotation = super::ast::parse_and_validate(json)?;
45
46        Self::compile_inner(rotation, resolver)
47    }
48
49    /// Compile an already-parsed rotation.
50    ///
51    /// # Errors
52    /// Returns an error when lowering or schema preparation fails.
53    pub fn compile(rotation: &Rotation, resolver: &SpecResolver) -> Result<Self> {
54        Self::compile_inner(rotation.clone(), resolver)
55    }
56
57    /// Attach a decision-trace sink.
58    pub fn set_sink(&mut self, sink: DecisionTraceTarget) {
59        self.sink = Some(sink);
60    }
61
62    /// True when a sink is attached.
63    #[must_use]
64    pub fn has_sink(&self) -> bool {
65        self.sink.is_some()
66    }
67
68    fn compile_inner(rotation: Rotation, resolver: &SpecResolver) -> Result<Self> {
69        let (schema, _table) = lower::prepare(&rotation, resolver)?;
70
71        Ok(Self::from_parts(rotation, resolver.clone(), schema))
72    }
73}
74
75impl super::backend::RuntimeBackend for InterpretedRotation {
76    fn evaluate(&mut self, buffer: &mut DenseBuffer, now_secs: f64) -> EvalResult {
77        if let Some(sink) = &self.sink {
78            let ctx = super::interp_backend::RecorderCtx {
79                rotation: &self.rotation,
80                schema: &self.schema,
81                resolver: &self.resolver,
82                table: &self.table,
83            };
84            let mut backend = InterpBackend::with_sink(
85                InterpFrame {
86                    buffer,
87                    now: now_secs,
88                },
89                sink.clone(),
90                ctx,
91            );
92
93            lower::lower_rotation(
94                &mut backend,
95                &self.rotation,
96                &self.schema,
97                &self.resolver,
98                &self.table,
99            );
100
101            backend.finish()
102        } else {
103            // docref:start rotation-compiler-interpreter-evaluate
104            let mut backend = InterpBackend::new(InterpFrame {
105                buffer,
106                now: now_secs,
107            });
108            lower::lower_rotation(
109                &mut backend,
110                &self.rotation,
111                &self.schema,
112                &self.resolver,
113                &self.table,
114            );
115            backend.finish()
116            // docref:end rotation-compiler-interpreter-evaluate
117        }
118    }
119
120    fn schema(&self) -> &ContextSchema {
121        &self.schema
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use googletest::{Result as GtestResult, prelude::*};
128
129    use super::*;
130
131    #[gtest]
132    fn enabled_unknown_cast_fails_compilation() -> GtestResult<()> {
133        let json = r#"{
134            "version": 1,
135            "name": "unknown cast",
136            "variables": {},
137            "actions": [{ "type": "cast", "spell": "missing_spell" }],
138            "lists": {}
139        }"#;
140
141        let error = InterpretedRotation::compile_json(json, &SpecResolver::new("test"))
142            .expect_err("an enabled unknown cast must not silently disappear");
143
144        verify_that!(error.to_string(), eq("unknown spell: missing_spell"))
145    }
146
147    #[gtest]
148    fn malformed_json_retains_the_typed_parse_error_source() {
149        let error = InterpretedRotation::compile_json("{", &SpecResolver::new("test"))
150            .expect_err("malformed JSON must fail before compilation");
151        let source = std::error::Error::source(&error)
152            .and_then(std::error::Error::source)
153            .expect("JSON failure retains its source");
154
155        expect_that!(source.is::<serde_json::Error>(), is_true());
156    }
157
158    #[gtest]
159    fn disabled_unknown_cast_is_ignored() -> GtestResult<()> {
160        let json = r#"{
161            "version": 1,
162            "name": "disabled unknown cast",
163            "variables": {},
164            "actions": [{ "type": "cast", "spell": "missing_spell", "enabled": false }],
165            "lists": {}
166        }"#;
167
168        verify_that!(
169            InterpretedRotation::compile_json(json, &SpecResolver::new("test")),
170            ok(anything())
171        )
172    }
173}