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