Skip to main content

snc_core/
analysis.rs

1use std::collections::HashMap;
2
3use crate::ast::{self, *};
4use crate::error::{CompileError, CompileResult};
5use crate::ir::*;
6
7/// Analyze a parsed AST and produce the lowered IR.
8pub fn analyze(program: &Program) -> CompileResult<SeqIR> {
9    let mut analyzer = Analyzer::new();
10    analyzer.analyze_program(program)
11}
12
13struct Analyzer {
14    channels: Vec<IRChannel>,
15    event_flags: Vec<IREventFlag>,
16    variables: Vec<IRVariable>,
17    // Lookup maps
18    var_to_channel: HashMap<String, usize>,
19    var_types: HashMap<String, IRType>,
20    ef_name_to_id: HashMap<String, usize>,
21    // Sync: var_name → ef_name
22    sync_map: HashMap<String, String>,
23    // Options
24    options: ProgramOptions,
25}
26
27impl Analyzer {
28    fn new() -> Self {
29        Self {
30            channels: Vec::new(),
31            event_flags: Vec::new(),
32            variables: Vec::new(),
33            var_to_channel: HashMap::new(),
34            var_types: HashMap::new(),
35            ef_name_to_id: HashMap::new(),
36            sync_map: HashMap::new(),
37            options: ProgramOptions::default(),
38        }
39    }
40
41    fn analyze_program(&mut self, program: &Program) -> CompileResult<SeqIR> {
42        // Pass 1: collect options
43        for opt in &program.options {
44            match opt {
45                ProgramOption::Safe => self.options.safe_mode = true,
46                ProgramOption::Reentrant => self.options.reentrant = true,
47                ProgramOption::Main => self.options.main_flag = true,
48            }
49        }
50
51        // Pass 2: collect declarations
52        for def in &program.definitions {
53            match def {
54                Definition::VarDecl(vd) => self.collect_var_decl(vd)?,
55                Definition::EvFlag(ef) => self.collect_evflag(ef)?,
56                Definition::Assign(a) => self.collect_assign(a)?,
57                Definition::Monitor(m) => self.collect_monitor(m)?,
58                Definition::Sync(s) => self.collect_sync(s)?,
59                Definition::Option(_) | Definition::CPreprocessor(_) | Definition::EmbeddedCode(_) => {}
60            }
61        }
62
63        // Pass 3: resolve sync → event flag → channels
64        self.resolve_sync()?;
65
66        // Pass 4: analyze state sets
67        let mut state_sets = Vec::new();
68        for (ss_id, ss) in program.state_sets.iter().enumerate() {
69            state_sets.push(self.analyze_state_set(ss, ss_id)?);
70        }
71
72        Ok(SeqIR {
73            program_name: program.name.clone(),
74            options: self.options.clone(),
75            channels: self.channels.clone(),
76            event_flags: self.event_flags.clone(),
77            variables: self.variables.clone(),
78            state_sets,
79            entry_block: program.entry.as_ref().map(|b| self.lower_block(b)),
80            exit_block: program.exit.as_ref().map(|b| self.lower_block(b)),
81        })
82    }
83
84    fn collect_var_decl(&mut self, vd: &VarDecl) -> CompileResult<()> {
85        let mut ir_type = self.convert_type(&vd.type_spec)?;
86        // Wrap in Array if dimensions are present (innermost first)
87        for dim_expr in vd.dimensions.iter().rev() {
88            let size = self.eval_const_expr(dim_expr);
89            ir_type = IRType::Array {
90                element: Box::new(ir_type),
91                size,
92            };
93        }
94        let init_value = vd.init.as_ref().map(|e| self.expr_to_rust(e));
95        self.var_types.insert(vd.name.clone(), ir_type.clone());
96        self.variables.push(IRVariable {
97            name: vd.name.clone(),
98            var_type: ir_type,
99            channel_id: None,
100            init_value,
101        });
102        Ok(())
103    }
104
105    /// Evaluate a constant expression for array dimension sizing.
106    fn eval_const_expr(&self, expr: &Expr) -> usize {
107        match expr {
108            Expr::IntLit(v, _) => *v as usize,
109            _ => 0, // non-constant dimensions not supported
110        }
111    }
112
113    fn collect_evflag(&mut self, ef: &EvFlagDecl) -> CompileResult<()> {
114        let id = self.event_flags.len();
115        self.ef_name_to_id.insert(ef.name.clone(), id);
116        self.event_flags.push(IREventFlag {
117            id,
118            name: ef.name.clone(),
119            synced_channels: Vec::new(),
120        });
121        Ok(())
122    }
123
124    fn collect_assign(&mut self, assign: &Assign) -> CompileResult<()> {
125        let pv_name = assign
126            .pv_name
127            .as_deref()
128            .unwrap_or(&assign.var_name)
129            .to_string();
130
131        let ch_id = self.channels.len();
132        let var_type = self
133            .var_types
134            .get(&assign.var_name)
135            .cloned()
136            .ok_or_else(|| {
137                CompileError::semantic(
138                    assign.span.clone(),
139                    format!("assign: undefined variable '{}'", assign.var_name),
140                )
141            })?;
142
143        self.channels.push(IRChannel {
144            id: ch_id,
145            var_name: assign.var_name.clone(),
146            pv_name,
147            var_type,
148            monitored: false,
149            sync_ef: None,
150        });
151
152        self.var_to_channel.insert(assign.var_name.clone(), ch_id);
153
154        // Update the variable's channel_id
155        if let Some(var) = self.variables.iter_mut().find(|v| v.name == assign.var_name) {
156            var.channel_id = Some(ch_id);
157        }
158
159        Ok(())
160    }
161
162    fn collect_monitor(&mut self, monitor: &Monitor) -> CompileResult<()> {
163        if let Some(&ch_id) = self.var_to_channel.get(&monitor.var_name) {
164            self.channels[ch_id].monitored = true;
165            Ok(())
166        } else {
167            Err(CompileError::semantic(
168                monitor.span.clone(),
169                format!(
170                    "monitor: variable '{}' is not assigned to a PV",
171                    monitor.var_name
172                ),
173            ))
174        }
175    }
176
177    fn collect_sync(&mut self, sync: &Sync) -> CompileResult<()> {
178        self.sync_map
179            .insert(sync.var_name.clone(), sync.ef_name.clone());
180        Ok(())
181    }
182
183    fn resolve_sync(&mut self) -> CompileResult<()> {
184        for (var_name, ef_name) in &self.sync_map {
185            let ef_id = self
186                .ef_name_to_id
187                .get(ef_name)
188                .copied()
189                .ok_or_else(|| {
190                    CompileError::Other(format!("sync: undefined event flag '{ef_name}'"))
191                })?;
192
193            let ch_id = self.var_to_channel.get(var_name).copied().ok_or_else(|| {
194                CompileError::Other(format!(
195                    "sync: variable '{var_name}' is not assigned to a PV"
196                ))
197            })?;
198
199            self.channels[ch_id].sync_ef = Some(ef_id);
200            self.event_flags[ef_id].synced_channels.push(ch_id);
201        }
202        Ok(())
203    }
204
205    fn analyze_state_set(&self, ss: &StateSet, ss_id: usize) -> CompileResult<IRStateSet> {
206        let local_vars: Vec<IRVariable> = ss
207            .local_vars
208            .iter()
209            .map(|vd| {
210                let mut ir_type = self.convert_type(&vd.type_spec).unwrap();
211                for dim_expr in vd.dimensions.iter().rev() {
212                    let size = self.eval_const_expr(dim_expr);
213                    ir_type = IRType::Array {
214                        element: Box::new(ir_type),
215                        size,
216                    };
217                }
218                IRVariable {
219                    name: vd.name.clone(),
220                    var_type: ir_type,
221                    channel_id: None,
222                    init_value: vd.init.as_ref().map(|e| self.expr_to_rust(e)),
223                }
224            })
225            .collect();
226
227        let mut states = Vec::new();
228        let state_name_to_id: HashMap<String, usize> = ss
229            .states
230            .iter()
231            .enumerate()
232            .map(|(i, s)| (s.name.clone(), i))
233            .collect();
234
235        for (state_id, state) in ss.states.iter().enumerate() {
236            let transitions = state
237                .transitions
238                .iter()
239                .map(|t| {
240                    let condition = t.condition.as_ref().map(|e| self.condition_to_rust(e));
241                    let action = self.lower_block(&t.body);
242                    let target_state = match &t.target {
243                        TransitionTarget::State(name) => {
244                            Some(*state_name_to_id.get(name).unwrap_or(&0))
245                        }
246                        TransitionTarget::Exit => None,
247                    };
248                    IRTransition {
249                        condition,
250                        action,
251                        target_state,
252                    }
253                })
254                .collect();
255
256            states.push(IRState {
257                name: state.name.clone(),
258                id: state_id,
259                entry: state.entry.as_ref().map(|b| self.lower_block(b)),
260                transitions,
261                exit: state.exit.as_ref().map(|b| self.lower_block(b)),
262            });
263        }
264
265        Ok(IRStateSet {
266            name: ss.name.clone(),
267            id: ss_id,
268            local_vars,
269            states,
270        })
271    }
272
273    fn lower_block(&self, block: &ast::Block) -> IRBlock {
274        let mut code = String::new();
275        for stmt in &block.stmts {
276            code.push_str(&self.stmt_to_rust(stmt));
277        }
278        IRBlock { code }
279    }
280
281    // --- Expression → Rust code string ---
282
283    fn condition_to_rust(&self, expr: &Expr) -> String {
284        self.expr_to_rust(expr)
285    }
286
287    fn expr_to_rust(&self, expr: &Expr) -> String {
288        match expr {
289            Expr::IntLit(v, _) => v.to_string(),
290            Expr::FloatLit(v, _) => format_float(*v),
291            Expr::StringLit(s, _) => format!("\"{s}\""),
292            Expr::Ident(name, _) => self.ident_to_rust(name),
293            Expr::BinaryOp(lhs, op, rhs, _) => {
294                let l = self.expr_to_rust(lhs);
295                let r = self.expr_to_rust(rhs);
296                let op_str = binop_to_rust(*op);
297                format!("{l} {op_str} {r}")
298            }
299            Expr::UnaryOp(op, e, _) => {
300                let e = self.expr_to_rust(e);
301                let op_str = match op {
302                    UnaryOp::Neg => "-",
303                    UnaryOp::Not => "!",
304                    UnaryOp::BitNot => "!",
305                };
306                format!("{op_str}{e}")
307            }
308            Expr::Call(name, args, _) => {
309                let args_str: Vec<String> = args.iter().map(|a| self.expr_to_rust(a)).collect();
310                self.call_to_rust(name, &args_str)
311            }
312            Expr::Assign(lhs, rhs, _) => {
313                let l = self.expr_to_rust(lhs);
314                let r = self.expr_to_rust(rhs);
315                format!("{l} = {r}")
316            }
317            Expr::CompoundAssign(lhs, op, rhs, _) => {
318                let l = self.expr_to_rust(lhs);
319                let r = self.expr_to_rust(rhs);
320                let op_str = binop_to_rust(*op);
321                format!("{l} {op_str}= {r}")
322            }
323            Expr::Paren(e, _) => {
324                let inner = self.expr_to_rust(e);
325                format!("({inner})")
326            }
327            Expr::PostIncr(e, _) => {
328                let e = self.expr_to_rust(e);
329                format!("{{ {e} += 1; {e} - 1 }}")
330            }
331            Expr::PostDecr(e, _) => {
332                let e = self.expr_to_rust(e);
333                format!("{{ {e} -= 1; {e} + 1 }}")
334            }
335            Expr::PreIncr(e, _) => {
336                let e = self.expr_to_rust(e);
337                format!("{{ {e} += 1; {e} }}")
338            }
339            Expr::PreDecr(e, _) => {
340                let e = self.expr_to_rust(e);
341                format!("{{ {e} -= 1; {e} }}")
342            }
343            Expr::Ternary(cond, then_e, else_e, _) => {
344                let c = self.expr_to_rust(cond);
345                let t = self.expr_to_rust(then_e);
346                let e = self.expr_to_rust(else_e);
347                format!("if {c} {{ {t} }} else {{ {e} }}")
348            }
349            Expr::Field(e, field, _) => {
350                let e = self.expr_to_rust(e);
351                format!("{e}.{field}")
352            }
353            Expr::Index(e, idx, _) => {
354                let e = self.expr_to_rust(e);
355                let i = self.expr_to_rust(idx);
356                format!("{e}[{i}]")
357            }
358            Expr::Cast(_, e, _) => self.expr_to_rust(e),
359            Expr::ArrayInit(elements, _) => {
360                let elems: Vec<String> = elements.iter().map(|e| self.expr_to_rust(e)).collect();
361                format!("[{}]", elems.join(", "))
362            }
363        }
364    }
365
366    fn ident_to_rust(&self, name: &str) -> String {
367        // If it's a channel-assigned variable, prefix with ctx.local_vars.
368        if self.var_to_channel.contains_key(name) || self.var_types.contains_key(name) {
369            format!("ctx.local_vars.{name}")
370        } else {
371            name.to_string()
372        }
373    }
374
375    fn resolve_comp_type(&self, args: &[String], default: &str) -> String {
376        if args.len() >= 2 {
377            let comp_arg = args[1].trim();
378            match comp_arg {
379                "ASYNC" | "ctx.local_vars.ASYNC" => "CompType::Async".to_string(),
380                "SYNC" | "ctx.local_vars.SYNC" => "CompType::Sync".to_string(),
381                "DEFAULT" | "ctx.local_vars.DEFAULT" => "CompType::Default".to_string(),
382                _ => default.to_string(),
383            }
384        } else {
385            default.to_string()
386        }
387    }
388
389    fn resolve_var_name<'a>(&'a self, args: &'a [String]) -> &'a str {
390        args.first()
391            .map(|a| a.strip_prefix("ctx.local_vars.").unwrap_or(a))
392            .unwrap_or("")
393    }
394
395    fn call_to_rust(&self, name: &str, args: &[String]) -> String {
396        match name {
397            "delay" => format!("ctx.delay({})", args.join(", ")),
398            "pvGet" => {
399                let var_name = self.resolve_var_name(args);
400                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
401                    let comp = self.resolve_comp_type(args, "CompType::Default");
402                    format!("ctx.pv_get({ch_id}, {comp}).await")
403                } else {
404                    format!("/* pvGet({}) - unresolved */", args.join(", "))
405                }
406            }
407            "pvPut" => {
408                let var_name = self.resolve_var_name(args);
409                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
410                    let comp = self.resolve_comp_type(args, "CompType::Default");
411                    format!("ctx.pv_put({ch_id}, {comp}).await")
412                } else {
413                    format!("/* pvPut({}) - unresolved */", args.join(", "))
414                }
415            }
416            "pvGetComplete" => {
417                let var_name = self.resolve_var_name(args);
418                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
419                    format!("ctx.pv_get_complete({ch_id}).await")
420                } else {
421                    format!("/* pvGetComplete({}) - unresolved */", args.join(", "))
422                }
423            }
424            "pvPutComplete" => {
425                let var_name = self.resolve_var_name(args);
426                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
427                    format!("ctx.pv_put_complete({ch_id}).await")
428                } else {
429                    format!("/* pvPutComplete({}) - unresolved */", args.join(", "))
430                }
431            }
432            "pvGetCancel" => {
433                let var_name = self.resolve_var_name(args);
434                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
435                    format!("ctx.pv_get_cancel({ch_id}).await")
436                } else {
437                    format!("/* pvGetCancel({}) - unresolved */", args.join(", "))
438                }
439            }
440            "pvPutCancel" => {
441                let var_name = self.resolve_var_name(args);
442                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
443                    format!("ctx.pv_put_cancel({ch_id}).await")
444                } else {
445                    format!("/* pvPutCancel({}) - unresolved */", args.join(", "))
446                }
447            }
448            "pvStatus" => {
449                let var_name = self.resolve_var_name(args);
450                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
451                    format!("ctx.pv_status({ch_id})")
452                } else {
453                    format!("/* pvStatus({}) - unresolved */", args.join(", "))
454                }
455            }
456            "pvSeverity" => {
457                let var_name = self.resolve_var_name(args);
458                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
459                    format!("ctx.pv_severity({ch_id})")
460                } else {
461                    format!("/* pvSeverity({}) - unresolved */", args.join(", "))
462                }
463            }
464            "pvMessage" => {
465                let var_name = self.resolve_var_name(args);
466                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
467                    format!("ctx.pv_message({ch_id})")
468                } else {
469                    format!("/* pvMessage({}) - unresolved */", args.join(", "))
470                }
471            }
472            "pvAssignCount" => {
473                format!("{}", self.channels.len())
474            }
475            "pvMonitorCount" => {
476                let count = self.channels.iter().filter(|ch| ch.monitored).count();
477                format!("{count}")
478            }
479            "efSet" => {
480                let ef_name = self.resolve_var_name(args);
481                if let Some(&ef_id) = self.ef_name_to_id.get(ef_name) {
482                    format!("ctx.ef_set({ef_id})")
483                } else {
484                    format!("/* efSet({}) - unresolved */", args.join(", "))
485                }
486            }
487            "efTest" => {
488                let ef_name = self.resolve_var_name(args);
489                if let Some(&ef_id) = self.ef_name_to_id.get(ef_name) {
490                    format!("ctx.ef_test({ef_id})")
491                } else {
492                    format!("/* efTest({}) - unresolved */", args.join(", "))
493                }
494            }
495            "efClear" => {
496                let ef_name = self.resolve_var_name(args);
497                if let Some(&ef_id) = self.ef_name_to_id.get(ef_name) {
498                    format!("ctx.ef_clear({ef_id})")
499                } else {
500                    format!("/* efClear({}) - unresolved */", args.join(", "))
501                }
502            }
503            "efTestAndClear" => {
504                let ef_name = self.resolve_var_name(args);
505                if let Some(&ef_id) = self.ef_name_to_id.get(ef_name) {
506                    format!("ctx.ef_test_and_clear({ef_id})")
507                } else {
508                    format!("/* efTestAndClear({}) - unresolved */", args.join(", "))
509                }
510            }
511            "pvConnected" => {
512                let var_name = self.resolve_var_name(args);
513                if let Some(&ch_id) = self.var_to_channel.get(var_name) {
514                    format!("ctx.pv_connected({ch_id})")
515                } else {
516                    format!("/* pvConnected({}) - unresolved */", args.join(", "))
517                }
518            }
519            "pvConnectCount" => "ctx.pv_connect_count()".to_string(),
520            "pvChannelCount" => "ctx.pv_channel_count()".to_string(),
521            _ => format!("{name}({})", args.join(", ")),
522        }
523    }
524
525    fn stmt_to_rust(&self, stmt: &Stmt) -> String {
526        match stmt {
527            Stmt::Expr(e) => format!("{};\n", self.expr_to_rust(e)),
528            Stmt::VarDecl(vd) => {
529                let mut ir_type = self.convert_type(&vd.type_spec).unwrap();
530                for dim_expr in vd.dimensions.iter().rev() {
531                    let size = self.eval_const_expr(dim_expr);
532                    ir_type = IRType::Array {
533                        element: Box::new(ir_type),
534                        size,
535                    };
536                }
537                let init = vd
538                    .init
539                    .as_ref()
540                    .map(|e| self.expr_to_rust(e))
541                    .unwrap_or_else(|| ir_type.default_value());
542                let rust_type = ir_type.rust_type();
543                format!("let mut {}: {rust_type} = {init};\n", vd.name)
544            }
545            Stmt::If(cond, then_b, else_b) => {
546                let c = self.expr_to_rust(cond);
547                let t = self.block_to_rust(then_b);
548                if let Some(else_block) = else_b {
549                    let e = self.block_to_rust(else_block);
550                    format!("if {c} {{\n{t}}} else {{\n{e}}}\n")
551                } else {
552                    format!("if {c} {{\n{t}}}\n")
553                }
554            }
555            Stmt::While(cond, body) => {
556                let c = self.expr_to_rust(cond);
557                let b = self.block_to_rust(body);
558                format!("while {c} {{\n{b}}}\n")
559            }
560            Stmt::For(init, cond, step, body) => {
561                // Convert C-style for to Rust loop
562                let mut code = String::new();
563                if let Some(init) = init {
564                    code.push_str(&format!("{};\n", self.expr_to_rust(init)));
565                }
566                let cond_str = cond
567                    .as_ref()
568                    .map(|c| self.expr_to_rust(c))
569                    .unwrap_or_else(|| "true".to_string());
570                let body_str = self.block_to_rust(body);
571                let step_str = step
572                    .as_ref()
573                    .map(|s| format!("{};\n", self.expr_to_rust(s)))
574                    .unwrap_or_default();
575                code.push_str(&format!(
576                    "while {cond_str} {{\n{body_str}{step_str}}}\n"
577                ));
578                code
579            }
580            Stmt::Break => "break;\n".to_string(),
581            Stmt::Return(val) => match val {
582                Some(e) => format!("return {};\n", self.expr_to_rust(e)),
583                None => "return;\n".to_string(),
584            },
585            Stmt::Block(b) => {
586                let inner = self.block_to_rust(b);
587                format!("{{\n{inner}}}\n")
588            }
589            Stmt::EmbeddedCode(code) => format!("{code}\n"),
590        }
591    }
592
593    fn block_to_rust(&self, block: &ast::Block) -> String {
594        let mut code = String::new();
595        for stmt in &block.stmts {
596            code.push_str(&self.stmt_to_rust(stmt));
597        }
598        code
599    }
600
601    fn convert_type(&self, ts: &TypeSpec) -> CompileResult<IRType> {
602        match ts {
603            TypeSpec::Int => Ok(IRType::Int),
604            TypeSpec::Short => Ok(IRType::Short),
605            TypeSpec::Long => Ok(IRType::Long),
606            TypeSpec::Float => Ok(IRType::Float),
607            TypeSpec::Double => Ok(IRType::Double),
608            TypeSpec::String => Ok(IRType::String),
609            TypeSpec::Char => Ok(IRType::Char),
610            TypeSpec::Unsigned(inner) => self.convert_type(inner), // simplified
611        }
612    }
613}
614
615fn binop_to_rust(op: BinOp) -> &'static str {
616    match op {
617        BinOp::Add => "+",
618        BinOp::Sub => "-",
619        BinOp::Mul => "*",
620        BinOp::Div => "/",
621        BinOp::Mod => "%",
622        BinOp::Eq => "==",
623        BinOp::Ne => "!=",
624        BinOp::Lt => "<",
625        BinOp::Le => "<=",
626        BinOp::Gt => ">",
627        BinOp::Ge => ">=",
628        BinOp::And => "&&",
629        BinOp::Or => "||",
630        BinOp::BitAnd => "&",
631        BinOp::BitOr => "|",
632        BinOp::BitXor => "^",
633        BinOp::Shl => "<<",
634        BinOp::Shr => ">>",
635    }
636}
637
638fn format_float(v: f64) -> String {
639    let s = v.to_string();
640    if s.contains('.') || s.contains('e') || s.contains('E') {
641        s
642    } else {
643        format!("{s}.0")
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use crate::lexer::Lexer;
651    use crate::parser::Parser;
652
653    fn analyze_str(input: &str) -> SeqIR {
654        let tokens = Lexer::new(input).tokenize().unwrap();
655        let ast = Parser::new(tokens).parse_program().unwrap();
656        analyze(&ast).unwrap()
657    }
658
659    #[test]
660    fn test_basic_analysis() {
661        let ir = analyze_str(r#"
662            program test
663            option +s;
664            double x;
665            assign x to "PV:x";
666            monitor x;
667            evflag ef_x;
668            sync x to ef_x;
669            ss s1 {
670                state init {
671                    when (efTestAndClear(ef_x)) {
672                        x += 1.0;
673                        pvPut(x);
674                    } state init
675                    when (delay(10.0)) {
676                    } exit
677                }
678            }
679        "#);
680
681        assert_eq!(ir.program_name, "test");
682        assert!(ir.options.safe_mode);
683        assert_eq!(ir.channels.len(), 1);
684        assert_eq!(ir.channels[0].var_name, "x");
685        assert_eq!(ir.channels[0].pv_name, "PV:x");
686        assert!(ir.channels[0].monitored);
687        assert_eq!(ir.channels[0].sync_ef, Some(0));
688        assert_eq!(ir.event_flags.len(), 1);
689        assert_eq!(ir.event_flags[0].synced_channels, vec![0]);
690        assert_eq!(ir.state_sets.len(), 1);
691        assert_eq!(ir.state_sets[0].states.len(), 1);
692        assert_eq!(ir.state_sets[0].states[0].transitions.len(), 2);
693    }
694
695    #[test]
696    fn test_builtin_translation() {
697        let ir = analyze_str(r#"
698            program test
699            double v;
700            assign v to "PV:v";
701            monitor v;
702            evflag ef_v;
703            sync v to ef_v;
704            ss s1 {
705                state init {
706                    when (delay(1.0)) {
707                        pvGet(v);
708                        pvPut(v);
709                        efSet(ef_v);
710                        efClear(ef_v);
711                    } state init
712                }
713            }
714        "#);
715
716        let trans = &ir.state_sets[0].states[0].transitions[0];
717        assert_eq!(trans.condition.as_deref(), Some("ctx.delay(1.0)"));
718        assert!(trans.action.code.contains("ctx.pv_get(0, CompType::Default).await"));
719        assert!(trans.action.code.contains("ctx.pv_put(0, CompType::Default).await"));
720        assert!(trans.action.code.contains("ctx.ef_set(0)"));
721        assert!(trans.action.code.contains("ctx.ef_clear(0)"));
722    }
723
724    #[test]
725    fn test_async_builtins() {
726        let ir = analyze_str(r#"
727            program test
728            double v;
729            assign v to "PV:v";
730            ss s1 {
731                state init {
732                    when (delay(1.0)) {
733                        pvGet(v, ASYNC);
734                        pvPut(v, SYNC);
735                    } state wait
736                    when (pvGetComplete(v)) {
737                    } state done
738                }
739                state wait {
740                    when (pvGetComplete(v)) {
741                    } state done
742                }
743                state done {
744                    when () { } exit
745                }
746            }
747        "#);
748
749        let trans = &ir.state_sets[0].states[0].transitions[0];
750        assert!(trans.action.code.contains("ctx.pv_get(0, CompType::Async).await"));
751        assert!(trans.action.code.contains("ctx.pv_put(0, CompType::Sync).await"));
752
753        let trans2 = &ir.state_sets[0].states[0].transitions[1];
754        assert_eq!(
755            trans2.condition.as_deref(),
756            Some("ctx.pv_get_complete(0).await")
757        );
758    }
759
760    #[test]
761    fn test_pv_status_builtins() {
762        let ir = analyze_str(r#"
763            program test
764            double v;
765            assign v to "PV:v";
766            monitor v;
767            ss s1 {
768                state init {
769                    when (delay(1.0)) {
770                        pvGet(v);
771                        pvStatus(v);
772                        pvSeverity(v);
773                        pvMessage(v);
774                    } state init
775                }
776            }
777        "#);
778
779        let trans = &ir.state_sets[0].states[0].transitions[0];
780        assert!(trans.action.code.contains("ctx.pv_status(0)"));
781        assert!(trans.action.code.contains("ctx.pv_severity(0)"));
782        assert!(trans.action.code.contains("ctx.pv_message(0)"));
783    }
784
785    #[test]
786    fn test_pv_assign_monitor_count() {
787        let ir = analyze_str(r#"
788            program test
789            double a;
790            double b;
791            assign a to "PV:a";
792            assign b to "PV:b";
793            monitor a;
794            ss s1 {
795                state init {
796                    when (delay(1.0)) {
797                        pvAssignCount();
798                        pvMonitorCount();
799                    } state init
800                }
801            }
802        "#);
803
804        let trans = &ir.state_sets[0].states[0].transitions[0];
805        // 2 channels assigned
806        assert!(trans.action.code.contains("2"));
807        // 1 monitored
808        assert!(trans.action.code.contains("1"));
809    }
810
811    #[test]
812    fn test_multi_state_set() {
813        let ir = analyze_str(r#"
814            program test
815            double counter;
816            assign counter to "PV:counter";
817            int light;
818            assign light to "PV:light";
819            ss counter_ss {
820                state counting {
821                    when (delay(1.0)) {
822                        counter += 1.0;
823                        pvPut(counter);
824                    } state counting
825                }
826            }
827            ss light_ss {
828                state idle {
829                    when (delay(15.0)) {
830                    } exit
831                }
832            }
833        "#);
834
835        assert_eq!(ir.state_sets.len(), 2);
836        assert_eq!(ir.channels.len(), 2);
837        assert_eq!(ir.variables.len(), 2);
838    }
839
840    #[test]
841    fn test_array_variable() {
842        let ir = analyze_str(r#"
843            program test
844            double arr[5];
845            ss s1 {
846                state init {
847                    when () { } exit
848                }
849            }
850        "#);
851
852        assert_eq!(ir.variables.len(), 1);
853        assert!(matches!(
854            &ir.variables[0].var_type,
855            IRType::Array { element, size: 5 } if **element == IRType::Double
856        ));
857        assert_eq!(ir.variables[0].var_type.rust_type(), "[f64; 5]");
858        assert_eq!(ir.variables[0].var_type.default_value(), "[0.0; 5]");
859    }
860
861    #[test]
862    fn test_array_with_brace_init() {
863        let ir = analyze_str(r#"
864            program test
865            int vals[3] = {10, 20, 30};
866            ss s1 {
867                state init {
868                    when () { } exit
869                }
870            }
871        "#);
872
873        assert_eq!(ir.variables[0].init_value, Some("[10, 20, 30]".to_string()));
874    }
875
876    #[test]
877    fn test_local_vars() {
878        let ir = analyze_str(r#"
879            program test
880            ss s1 {
881                int n = 0;
882                double avg = 0.0;
883                state init {
884                    when () { } exit
885                }
886            }
887        "#);
888
889        assert_eq!(ir.state_sets[0].local_vars.len(), 2);
890        assert_eq!(ir.state_sets[0].local_vars[0].name, "n");
891        assert_eq!(ir.state_sets[0].local_vars[0].init_value, Some("0".to_string()));
892    }
893}