Skip to main content

wowlab_engine_domain/rotation/lower/
mod.rs

1//! Generic rotation lowerer — every operation written exactly once.
2
3mod action;
4mod condition;
5mod eval_kind;
6pub(super) mod ops;
7mod value;
8
9use wowlab_types::sim::{Condition, FastMap, Rotation, RotationAction as AstAction};
10
11#[cfg(test)]
12thread_local! {
13    static LOWERING_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
14}
15
16use super::{
17    backend::RotationBackend,
18    buffer::{BufferRequirements, DescriptorTable},
19    context::{ContextSchema, SchemaDraft, schema_draft},
20    error::{Error, Result},
21    expr::FieldType,
22    resolver::SpecResolver,
23};
24
25#[rustfmt::skip]
26pub(super) use value::{EvalValueAbstract, typed_load, typed_select, typed_store};
27
28pub(super) struct Lowerer<'a> {
29    pub(super) schema: &'a ContextSchema,
30    pub(super) resolver: &'a SpecResolver,
31    pub(super) table: &'a DescriptorTable,
32    pub(super) lists: &'a FastMap<String, Vec<AstAction>>,
33    pub(super) variables: &'a FastMap<String, Condition>,
34    pub(super) run_depth: u32,
35    pub(super) unconditionally_returned: bool,
36    pub(super) spell_usability_prevalidated: bool,
37}
38
39pub(super) fn lower_rotation<B>(
40    b: &mut B,
41    rotation: &Rotation,
42    schema: &ContextSchema,
43    resolver: &SpecResolver,
44    table: &DescriptorTable,
45    spell_usability_prevalidated: bool,
46) where
47    B: RotationBackend,
48{
49    wowlab_types::bench_metrics::record_rotation_ast_lowering();
50
51    #[cfg(test)]
52    LOWERING_COUNT.with(|count| count.set(count.get().saturating_add(1)));
53
54    let mut lo = Lowerer {
55        schema,
56        resolver,
57        table,
58        lists: &rotation.lists,
59        variables: &rotation.variables,
60        run_depth: 0,
61        unconditionally_returned: false,
62        spell_usability_prevalidated,
63    };
64
65    action::init_user_variables(&mut lo, b);
66
67    action::lower_actions(&mut lo, b, "actions", &rotation.actions);
68
69    // JIT needs a trailing terminator for LLVM verification; other backends treat it idempotently.
70    b.return_none();
71}
72
73#[cfg(test)]
74pub(super) fn reset_lowering_count() {
75    LOWERING_COUNT.with(|count| count.set(0));
76}
77
78#[cfg(test)]
79pub(super) fn lowering_count() -> usize {
80    LOWERING_COUNT.with(std::cell::Cell::get)
81}
82
83pub(in crate::rotation) fn eval_condition_as<T>(
84    cond: &Condition,
85    ctx: &mut super::trace::TraceContext<'_>,
86    coerce: impl for<'b> FnOnce(
87        EvalValueAbstract<super::reference_backend::ReferenceBackend<'b>>,
88        &mut super::reference_backend::ReferenceBackend<'b>,
89    ) -> T,
90) -> T {
91    let mut backend =
92        super::reference_backend::ReferenceBackend::new(super::reference_backend::ReferenceFrame {
93            buffer: ctx.buffer,
94            now: ctx.now_secs,
95        });
96    let mut lo = Lowerer {
97        schema: ctx.schema,
98        resolver: ctx.resolver,
99        table: ctx.table,
100        lists: &ctx.rotation.lists,
101        variables: &ctx.rotation.variables,
102        run_depth: 0,
103        unconditionally_returned: false,
104        spell_usability_prevalidated: false,
105    };
106    let value = condition::lower_condition(&mut lo, &mut backend, cond);
107
108    coerce(value, &mut backend)
109}
110
111pub(super) fn prepare(
112    rotation: &Rotation,
113    resolver: &SpecResolver,
114    table: &DescriptorTable,
115    requirements: &BufferRequirements,
116) -> Result<ContextSchema> {
117    fn collect_setvars(actions: &[AstAction], sb: &mut SchemaDraft<'_>, table: &DescriptorTable) {
118        for action in actions {
119            if let AstAction::SetVar { name, value, .. } = action {
120                let ty = infer_field_type(value, table);
121
122                sb.add_user_var(name, ty);
123            }
124        }
125    }
126
127    let mut sb = schema_draft(table);
128
129    for (name, init) in &rotation.variables {
130        let ty = infer_field_type(init, table);
131
132        sb.add_user_var(name, ty);
133    }
134
135    collect_setvars(&rotation.actions, &mut sb, table);
136
137    for actions in rotation.lists.values() {
138        collect_setvars(actions, &mut sb, table);
139    }
140
141    for action in &rotation.actions {
142        sb.register_action_fields(action, resolver)?;
143    }
144
145    for actions in rotation.lists.values() {
146        for action in actions {
147            sb.register_action_fields(action, resolver)?;
148        }
149    }
150
151    for cond in rotation.variables.values() {
152        sb.register_condition_fields(cond, resolver)?;
153    }
154
155    let schema = sb.build_with_resolver(Some(resolver), requirements);
156
157    let registered = resolver.registered_resource_types();
158
159    for resource_type in schema.layout.offsets.slots.resources.keys() {
160        if !registered.contains(resource_type) {
161            // #t(rust_alloc_in_loop) building error message for early return
162            return Err(Error::validation(format!(
163                "rotation references resource {resource_type:?} but the spec does not register it — \
164                 conditions using this resource will always read 0",
165            )));
166        }
167    }
168
169    Ok(schema)
170}
171
172// #t(fn: rust_recursive_fn) depth bounded by validate_rotation's MAX_CONDITION_DEPTH check (rejected pre-lowering).
173pub(super) fn infer_field_type(condition: &Condition, table: &DescriptorTable) -> FieldType {
174    match condition {
175        Condition::Bool { .. }
176        | Condition::Compare { .. }
177        | Condition::And { .. }
178        | Condition::Or { .. }
179        | Condition::Not { .. } => FieldType::Bool,
180        Condition::Int { .. } => FieldType::Int,
181        Condition::Float { .. } | Condition::Var { .. } => FieldType::Float,
182        Condition::Read { field } => {
183            if let Some((_, desc)) = table.lookup(&field.domain, &field.name) {
184                // `Direct` keeps the source field's own type; everything else has a fixed result type.
185                desc.eval_kind
186                    .result_field_type()
187                    .unwrap_or(desc.field_type)
188            } else {
189                FieldType::Float
190            }
191        }
192        Condition::Arith { .. } | Condition::UnaryMath { .. } | Condition::MinMax { .. } => {
193            FieldType::Float
194        }
195        Condition::IfThenElse { then, .. } => infer_field_type(then, table),
196        _ => unreachable!("rotation validation rejects unsupported condition variants"),
197    }
198}