wowlab_engine_domain/rotation/
interpreter.rs1use 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#[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 #[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 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 pub fn compile(rotation: &Rotation, resolver: &SpecResolver) -> Result<Self> {
54 Self::compile_inner(rotation.clone(), resolver)
55 }
56
57 pub fn set_sink(&mut self, sink: DecisionTraceTarget) {
59 self.sink = Some(sink);
60 }
61
62 #[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 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 }
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}