Skip to main content

plc_ast/
ser.rs

1use crate::{
2    ast::{
3        Allocation, ArgumentProperty, Assignment, AstNode, AstStatement, AutoDerefType, BinaryExpression,
4        CallStatement, CompilationUnit, ConfigVariable, DataType, DataTypeDeclaration, DefaultValue,
5        DirectAccess, EmptyStatement, HardwareAccess, Implementation, Interface, JumpStatement,
6        LabelStatement, MultipliedStatement, Pou, PouType, PropertyBlock, RangeStatement, ReferenceAccess,
7        ReferenceExpr, UnaryExpression, UserTypeDeclaration, Variable, VariableBlock, VariableBlockType,
8    },
9    control_statements::{AstControlStatement, ReturnStatement},
10    literals::AstLiteral,
11    visitor::{AstVisitor, Walker},
12};
13
14pub struct AstSerializer<'a> {
15    result: String,
16    indent: usize,
17    unit: Option<&'a CompilationUnit>,
18    user_type_context: Option<&'a UserTypeDeclaration>,
19    is_in_paren: bool,
20}
21
22impl AstSerializer<'_> {
23    pub fn format(node: &AstNode) -> String {
24        let mut serializer = AstSerializer {
25            result: String::new(),
26            indent: 0,
27            unit: None,
28            user_type_context: None,
29            is_in_paren: false,
30        };
31        serializer.visit(node);
32
33        serializer.result
34    }
35
36    /// Serializes a whole compilation unit back to Structured Text, POU by POU
37    /// (header, variable blocks, body statements, closing keyword).
38    pub fn from_unit(unit: &CompilationUnit) -> String {
39        let mut serializer = AstSerializer {
40            result: String::new(),
41            indent: 0,
42            unit: Some(unit),
43            user_type_context: None,
44            is_in_paren: false,
45        };
46        serializer.visit_compilation_unit(unit);
47
48        serializer.result
49    }
50
51    pub fn format_nodes(nodes: &[AstNode]) -> String {
52        let mut serializer = AstSerializer {
53            result: String::new(),
54            indent: 0,
55            unit: None,
56            user_type_context: None,
57            is_in_paren: false,
58        };
59
60        let nodes = nodes.iter().filter(|node| !node.is_empty_statement());
61
62        for (index, node) in nodes.enumerate() {
63            if index > 0 {
64                serializer.result.push('\n');
65            }
66            serializer.visit(node);
67
68            // Expression lists push their own ';'
69            if !node.is_expression_list() {
70                serializer.result.push(';');
71            }
72        }
73
74        serializer.result
75    }
76
77    pub fn format_variable_block(variable_block: &VariableBlock, unit: &CompilationUnit) -> String {
78        let mut serializer = AstSerializer {
79            result: String::new(),
80            indent: 0,
81            unit: Some(unit),
82            user_type_context: None,
83            is_in_paren: false,
84        };
85        serializer.visit_variable_block(variable_block);
86
87        serializer.result
88    }
89
90    /// Serializes a list of statements, each on its own indented line.
91    fn serialize_statement_list(&mut self, stmts: &[AstNode]) {
92        self.indent += 1;
93        for stmt in stmts {
94            self.push_indent();
95            stmt.walk(self);
96        }
97        self.indent -= 1;
98    }
99
100    /// Pushes a newline followed by the current indentation.
101    fn push_indent(&mut self) {
102        self.result.push('\n');
103        for _ in 0..self.indent {
104            self.result.push_str("    ");
105        }
106    }
107
108    fn set_user_type_declaration_context(&mut self, type_name: &str) {
109        let Some(unit) = self.unit else {
110            panic!("cannot retrieve user type declaration without a compilation unit")
111        };
112
113        self.user_type_context =
114            unit.user_types.iter().find(|it| it.data_type.get_name().is_some_and(|name| name == type_name));
115    }
116}
117
118fn pou_keywords(kind: &PouType) -> (&'static str, &'static str) {
119    match kind {
120        PouType::Program => ("PROGRAM", "END_PROGRAM"),
121        PouType::FunctionBlock => ("FUNCTION_BLOCK", "END_FUNCTION_BLOCK"),
122        PouType::Class => ("CLASS", "END_CLASS"),
123        PouType::Action => ("ACTION", "END_ACTION"),
124        _ => ("FUNCTION", "END_FUNCTION"),
125    }
126}
127
128impl AstVisitor for AstSerializer<'_> {
129    fn visit(&mut self, node: &AstNode) {
130        node.walk(self)
131    }
132
133    fn visit_compilation_unit(&mut self, unit: &CompilationUnit) {
134        for (index, pou) in unit.pous.iter().enumerate() {
135            if index > 0 {
136                self.result.push_str("\n\n");
137            }
138            self.visit_pou(pou);
139            if let Some(implementation) = unit.implementations.iter().find(|it| it.name == pou.name) {
140                self.visit_implementation(implementation);
141            }
142            self.result.push('\n');
143            self.result.push_str(pou_keywords(&pou.kind).1);
144        }
145    }
146
147    fn visit_implementation(&mut self, implementation: &Implementation) {
148        self.indent += 1;
149        for statement in &implementation.statements {
150            if statement.is_empty_statement() {
151                continue;
152            }
153            self.push_indent();
154            statement.walk(self);
155            // A label reads as `LABEL: name`; an expression list ends its own members.
156            if !statement.is_expression_list() && statement.get_label_name().is_none() {
157                self.result.push(';');
158            }
159        }
160        self.indent -= 1;
161    }
162
163    fn visit_variable_block(&mut self, variable_block: &VariableBlock) {
164        let var_start = match variable_block.kind {
165            VariableBlockType::InOut => "VAR_IN_OUT",
166            VariableBlockType::Input(ArgumentProperty::ByVal) => "VAR_INPUT",
167            VariableBlockType::Input(ArgumentProperty::ByRef) => "VAR_INPUT {ref}",
168            VariableBlockType::Output => "VAR_OUTPUT",
169            VariableBlockType::Temp => "VAR_TEMP",
170            VariableBlockType::Global => "VAR_GLOBAL",
171            _ => "VAR",
172        };
173        let var_end: &str = "END_VAR";
174
175        self.result.push_str(var_start);
176        self.indent += 1;
177        variable_block.variables.iter().for_each(|v| {
178            self.push_indent();
179            self.result.push_str(&format!("{} : ", v.name));
180            self.visit_variable(v);
181            self.result.push(';');
182        });
183        self.indent -= 1;
184        self.result.push_str(&format!("\n{var_end}"));
185    }
186
187    fn visit_variable(&mut self, variable: &Variable) {
188        self.visit_data_type_declaration(&variable.data_type_declaration);
189
190        if let Some(initializer) = &variable.initializer {
191            self.result.push_str(" := ");
192            initializer.walk(self);
193        }
194    }
195
196    fn visit_config_variable(&mut self, _: &ConfigVariable) {
197        unimplemented!("for now only interested in individual nodes located in a POU body")
198    }
199
200    fn visit_interface(&mut self, _: &Interface) {
201        unimplemented!("for now only interested in individual nodes located in a POU body")
202    }
203
204    fn visit_property(&mut self, _: &PropertyBlock) {
205        unimplemented!("for now only interested in individual nodes located in a POU body")
206    }
207
208    fn visit_enum_element(&mut self, element: &AstNode) {
209        element.walk(self);
210    }
211
212    fn visit_data_type_declaration(&mut self, data_type_declaration: &DataTypeDeclaration) {
213        match data_type_declaration {
214            DataTypeDeclaration::Reference { referenced_type, .. } => {
215                self.set_user_type_declaration_context(referenced_type);
216
217                if let Some(user_type_declaration) = self.user_type_context {
218                    self.visit_user_type_declaration(user_type_declaration);
219                } else {
220                    self.result.push_str(referenced_type);
221                }
222            }
223            DataTypeDeclaration::Definition { data_type, .. } => {
224                self.visit_data_type(data_type);
225            }
226            DataTypeDeclaration::Aggregate { referenced_type, .. } => {
227                self.result.push_str(referenced_type);
228            }
229        }
230    }
231
232    fn visit_user_type_declaration(&mut self, user_type_declaration: &UserTypeDeclaration) {
233        self.visit_data_type(&user_type_declaration.data_type);
234    }
235
236    fn visit_data_type(&mut self, data_type: &DataType) {
237        match data_type {
238            DataType::PointerType { referenced_type, auto_deref, type_safe, .. } => {
239                match auto_deref {
240                    Some(AutoDerefType::Reference) => {
241                        self.result.push_str("REFERENCE TO ");
242                    }
243                    // TODO: We also want to handle these cases at some point
244                    Some(AutoDerefType::Alias) | Some(AutoDerefType::Default) => (),
245                    _ => {
246                        if *type_safe {
247                            self.result.push_str("REF_TO ");
248                        } else {
249                            self.result.push_str("POINTER TO ");
250                        }
251                    }
252                }
253
254                self.visit_data_type_declaration(referenced_type.as_ref());
255            }
256            DataType::StructType { name: Some(name), .. } => {
257                self.result.push_str(name);
258            }
259            // TODO: This should be expanded to include the other types as needed
260            _ => (),
261        }
262    }
263
264    fn visit_pou(&mut self, pou: &Pou) {
265        self.result.push_str(pou_keywords(&pou.kind).0);
266        self.result.push(' ');
267        self.result.push_str(&pou.name);
268        if let Some(return_type) = &pou.return_type {
269            self.result.push_str(" : ");
270            self.visit_data_type_declaration(return_type);
271        }
272        for variable_block in &pou.variable_blocks {
273            self.result.push('\n');
274            self.visit_variable_block(variable_block);
275        }
276    }
277
278    fn visit_empty_statement(&mut self, _stmt: &EmptyStatement, _node: &AstNode) {}
279
280    fn visit_default_value(&mut self, _stmt: &DefaultValue, _node: &AstNode) {}
281
282    fn visit_literal(&mut self, stmt: &AstLiteral, _node: &AstNode) {
283        use crate::literals::AstLiteral;
284        match stmt {
285            AstLiteral::Integer(value) => self.result.push_str(&value.to_string()),
286            AstLiteral::Real(value) => self.result.push_str(value),
287            AstLiteral::Bool(value) => self.result.push_str(&value.to_string().to_uppercase()),
288            AstLiteral::String(string_value) => {
289                if string_value.is_wide {
290                    self.result.push_str(&format!("\"{}\"", string_value.value));
291                } else {
292                    self.result.push_str(&format!("'{}'", string_value.value));
293                }
294            }
295            AstLiteral::Null => self.result.push_str("NULL"),
296            _ => stmt.walk(self), // Let other literals use their default walking behavior
297        }
298    }
299
300    fn visit_multiplied_statement(&mut self, stmt: &MultipliedStatement, _node: &AstNode) {
301        stmt.walk(self)
302    }
303
304    fn visit_reference_expr(&mut self, stmt: &ReferenceExpr, _node: &AstNode) {
305        if let Some(base) = &stmt.base {
306            base.walk(self);
307        }
308
309        match &stmt.access {
310            ReferenceAccess::Global(reference) => {
311                self.result.push('.');
312                reference.walk(self);
313            }
314            ReferenceAccess::Member(reference) => {
315                if stmt.base.is_some() {
316                    self.result.push('.');
317                }
318                reference.walk(self);
319            }
320            ReferenceAccess::Index(index) => {
321                self.result.push('[');
322                self.is_in_paren = true;
323                index.walk(self);
324                self.is_in_paren = false;
325                self.result.push(']');
326            }
327            ReferenceAccess::Cast(reference) => {
328                self.result.push('#');
329                reference.walk(self);
330            }
331            ReferenceAccess::Deref => {
332                self.result.push('^');
333            }
334            ReferenceAccess::Address => {
335                self.result.insert_str(0, "ADR(");
336                self.result.push(')');
337            }
338        }
339    }
340
341    fn visit_identifier(&mut self, stmt: &str, _node: &AstNode) {
342        self.result.push_str(stmt);
343    }
344
345    fn visit_direct_access(&mut self, stmt: &DirectAccess, _node: &AstNode) {
346        stmt.walk(self)
347    }
348
349    fn visit_hardware_access(&mut self, stmt: &HardwareAccess, _node: &AstNode) {
350        stmt.walk(self)
351    }
352
353    fn visit_binary_expression(&mut self, stmt: &BinaryExpression, _node: &AstNode) {
354        stmt.left.walk(self);
355        self.result.push(' ');
356        self.result.push_str(&stmt.operator.to_string());
357        self.result.push(' ');
358        stmt.right.walk(self);
359    }
360
361    fn visit_unary_expression(&mut self, stmt: &UnaryExpression, _node: &AstNode) {
362        let op = stmt.operator.to_string();
363        self.result.push_str(&op);
364        // Word-based operators (NOT, MINUS as identifier) need a trailing space.
365        if op.chars().next().is_some_and(|c| c.is_alphabetic()) {
366            self.result.push(' ');
367        }
368        stmt.value.walk(self);
369    }
370
371    fn visit_expression_list(&mut self, stmt: &Vec<AstNode>, _node: &AstNode) {
372        let len = stmt.iter().filter(|stmt| !stmt.is_empty_statement()).count();
373        let stmt = stmt.iter().filter(|stmt| !stmt.is_empty_statement());
374        if self.is_in_paren {
375            for (i, node) in stmt.enumerate() {
376                if i > 0 {
377                    self.result.push_str(", ");
378                }
379                node.walk(self);
380            }
381        } else {
382            for (i, node) in stmt.enumerate() {
383                node.walk(self);
384                self.result.push(';');
385                if i != len - 1 {
386                    self.push_indent();
387                }
388            }
389        }
390    }
391
392    fn visit_paren_expression(&mut self, inner: &AstNode, _node: &AstNode) {
393        self.result.push('(');
394        self.is_in_paren = true;
395        inner.walk(self);
396        self.is_in_paren = false;
397        self.result.push(')');
398    }
399
400    fn visit_range_statement(&mut self, stmt: &RangeStatement, _node: &AstNode) {
401        stmt.walk(self)
402    }
403
404    fn visit_vla_range_statement(&mut self, _node: &AstNode) {}
405
406    fn visit_assignment(&mut self, stmt: &Assignment, _node: &AstNode) {
407        stmt.left.walk(self);
408        self.result.push_str(" := ");
409        stmt.right.walk(self);
410    }
411
412    fn visit_output_assignment(&mut self, stmt: &Assignment, _node: &AstNode) {
413        stmt.left.walk(self);
414        self.result.push_str(" => ");
415        stmt.right.walk(self);
416    }
417
418    fn visit_ref_assignment(&mut self, stmt: &Assignment, _node: &AstNode) {
419        stmt.left.walk(self);
420        self.result.push_str(" REF= ");
421        stmt.right.walk(self);
422    }
423
424    fn visit_call_statement(&mut self, stmt: &CallStatement, _node: &AstNode) {
425        stmt.operator.walk(self);
426        self.result.push('(');
427        self.is_in_paren = true;
428        if let Some(opt) = stmt.parameters.as_ref() {
429            opt.walk(self)
430        }
431        self.is_in_paren = false;
432        self.result.push(')');
433    }
434
435    fn visit_control_statement(&mut self, stmt: &AstControlStatement, _node: &AstNode) {
436        match stmt {
437            AstControlStatement::If(if_stmt) => {
438                for (i, block) in if_stmt.blocks.iter().enumerate() {
439                    if i == 0 {
440                        self.result.push_str("IF ");
441                    } else {
442                        self.push_indent();
443                        self.result.push_str("ELSIF ");
444                    }
445                    block.condition.walk(self);
446                    self.result.push_str(" THEN");
447                    self.serialize_statement_list(&block.body);
448                }
449                if !if_stmt.else_block.is_empty() {
450                    self.push_indent();
451                    self.result.push_str("ELSE");
452                    self.serialize_statement_list(&if_stmt.else_block);
453                }
454                self.push_indent();
455                self.result.push_str("END_IF");
456            }
457            AstControlStatement::ForLoop(for_stmt) => {
458                self.result.push_str("FOR ");
459                for_stmt.counter.walk(self);
460                self.result.push_str(" := ");
461                for_stmt.start.walk(self);
462                self.result.push_str(" TO ");
463                for_stmt.end.walk(self);
464                if let Some(step) = &for_stmt.by_step {
465                    self.result.push_str(" BY ");
466                    step.walk(self);
467                }
468                self.result.push_str(" DO");
469                self.serialize_statement_list(&for_stmt.body);
470                self.push_indent();
471                self.result.push_str("END_FOR");
472            }
473            AstControlStatement::WhileLoop(loop_stmt) => {
474                self.result.push_str("WHILE ");
475                loop_stmt.condition.walk(self);
476                self.result.push_str(" DO");
477                self.serialize_statement_list(&loop_stmt.body);
478                self.push_indent();
479                self.result.push_str("END_WHILE");
480            }
481            AstControlStatement::RepeatLoop(loop_stmt) => {
482                self.result.push_str("REPEAT");
483                self.serialize_statement_list(&loop_stmt.body);
484                self.push_indent();
485                self.result.push_str("UNTIL ");
486                loop_stmt.condition.walk(self);
487                self.push_indent();
488                self.result.push_str("END_REPEAT");
489            }
490            AstControlStatement::Case(case_stmt) => {
491                self.result.push_str("CASE ");
492                case_stmt.selector.walk(self);
493                self.result.push_str(" OF");
494                self.indent += 1;
495                for block in &case_stmt.case_blocks {
496                    self.push_indent();
497                    block.condition.walk(self);
498                    self.result.push(':');
499                    self.serialize_statement_list(&block.body);
500                }
501                if !case_stmt.else_block.is_empty() {
502                    self.push_indent();
503                    self.result.push_str("ELSE");
504                    self.serialize_statement_list(&case_stmt.else_block);
505                }
506                self.indent -= 1;
507                self.push_indent();
508                self.result.push_str("END_CASE");
509            }
510        }
511    }
512
513    fn visit_case_condition(&mut self, child: &AstNode, _node: &AstNode) {
514        child.walk(self)
515    }
516
517    fn visit_exit_statement(&mut self, _node: &AstNode) {
518        self.result.push_str("EXIT;");
519    }
520
521    fn visit_continue_statement(&mut self, _node: &AstNode) {
522        self.result.push_str("CONTINUE;");
523    }
524
525    fn visit_return_statement(&mut self, stmt: &ReturnStatement, _node: &AstNode) {
526        // A CFC return carries a guard condition that has no bare ST syntax, so
527        // render it as the equivalent conditional block.
528        if stmt.condition.is_some() {
529            self.result.push_str("IF ");
530            stmt.walk(self);
531            self.result.push_str(" THEN RETURN; END_IF");
532        } else {
533            self.result.push_str("RETURN");
534        }
535    }
536
537    fn visit_jump_statement(&mut self, stmt: &JumpStatement, _node: &AstNode) {
538        // A CFC jump carries a guard condition and has no bare ST syntax, so
539        // render it as a conditional GOTO to its target label.
540        self.result.push_str("IF ");
541        stmt.condition.walk(self);
542        self.result.push_str(" THEN GOTO ");
543        stmt.target.walk(self);
544    }
545
546    fn visit_label_statement(&mut self, stmt: &LabelStatement, _node: &AstNode) {
547        self.result.push_str("LABEL: ");
548        self.result.push_str(&stmt.name);
549    }
550
551    fn visit_allocation(&mut self, stmt: &Allocation, _node: &AstNode) {
552        self.result.push_str(&format!("alloca {}: {}", stmt.name, stmt.reference_type));
553    }
554
555    fn visit_super(&mut self, _stmt: &AstStatement, _node: &AstNode) {
556        self.result.push_str("SUPER");
557    }
558
559    fn visit_this(&mut self, _stmt: &AstStatement, _node: &AstNode) {
560        self.result.push_str("THIS");
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use crate::ast::AstFactory;
568    use crate::literals::{AstLiteral, StringValue};
569    use plc_source::source_location::SourceLocation;
570
571    #[test]
572    fn expression_list() {
573        let function_name = AstFactory::create_identifier("foo", SourceLocation::undefined(), 0);
574        let expressions = vec![
575            AstFactory::create_literal(AstLiteral::Integer(1), SourceLocation::undefined(), 1),
576            AstFactory::create_literal(
577                AstLiteral::String(StringValue { value: "two".to_string(), is_wide: false }),
578                SourceLocation::undefined(),
579                2,
580            ),
581            AstFactory::create_literal(AstLiteral::Integer(3), SourceLocation::undefined(), 3),
582            AstFactory::create_literal(
583                AstLiteral::String(StringValue { value: "four".to_string(), is_wide: false }),
584                SourceLocation::undefined(),
585                4,
586            ),
587        ];
588        let expression_list = AstFactory::create_expression_list(expressions, SourceLocation::undefined(), 5);
589        let call = AstFactory::create_call_statement(
590            function_name,
591            Some(expression_list),
592            6,
593            SourceLocation::undefined(),
594        );
595
596        let result = AstSerializer::format(&call);
597        assert_eq!(result, "foo(1, 'two', 3, 'four')");
598    }
599
600    #[test]
601    fn variable_with_initializer() {
602        let variable = Variable {
603            name: "foo".to_string(),
604            data_type_declaration: DataTypeDeclaration::reference("DINT", SourceLocation::undefined()),
605            initializer: Some(AstFactory::create_literal(
606                AstLiteral::Integer(3),
607                SourceLocation::undefined(),
608                0,
609            )),
610            address: None,
611            location: SourceLocation::undefined(),
612        };
613        let block = VariableBlock::default().with_variables(vec![variable]);
614
615        let unit = CompilationUnit::new("<test>");
616        let result = AstSerializer::format_variable_block(&block, &unit);
617        assert_eq!(result, "VAR\n    foo : DINT := 3;\nEND_VAR");
618    }
619}