diff --git a/compiler/plc_ast/src/ast.rs b/compiler/plc_ast/src/ast.rs index 3426f94d12..77f65f7642 100644 --- a/compiler/plc_ast/src/ast.rs +++ b/compiler/plc_ast/src/ast.rs @@ -1691,7 +1691,13 @@ pub fn flatten_expression_list(list: &AstNode) -> Vec<&AstNode> { expressions.iter().by_ref().flat_map(flatten_expression_list).collect() } AstStatement::MultipliedStatement(MultipliedStatement { multiplier, element }, ..) => { - std::iter::repeat_n(flatten_expression_list(element), *multiplier as usize).flatten().collect() + if let Some(count) = multiplier.get_literal_integer_value() { + std::iter::repeat_n(flatten_expression_list(element), count as usize).flatten().collect() + } else { + // Non-literal multiplier (e.g. variable reference) — cannot flatten + // at this stage; the array lowering pass will handle it. + vec![list] + } } AstStatement::ParenExpression(expression) => flatten_expression_list(expression), _ => vec![list], @@ -2100,13 +2106,16 @@ impl AstFactory { } pub fn create_multiplied_statement( - multiplier: u32, + multiplier: AstNode, element: AstNode, location: SourceLocation, id: AstId, ) -> AstNode { AstNode::new( - AstStatement::MultipliedStatement(MultipliedStatement { multiplier, element: Box::new(element) }), + AstStatement::MultipliedStatement(MultipliedStatement { + multiplier: Box::new(multiplier), + element: Box::new(element), + }), id, location, ) @@ -2190,7 +2199,7 @@ pub struct CastStatement { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(bound(deserialize = "'de: 'static"))] pub struct MultipliedStatement { - pub multiplier: u32, + pub multiplier: Box, pub element: Box, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/compiler/plc_ast/src/mut_visitor.rs b/compiler/plc_ast/src/mut_visitor.rs index 92d556232f..823ad5d009 100644 --- a/compiler/plc_ast/src/mut_visitor.rs +++ b/compiler/plc_ast/src/mut_visitor.rs @@ -243,7 +243,8 @@ impl WalkerMut for MultipliedStatement { where V: AstVisitorMut, { - visitor.visit(&mut self.element) + visitor.visit(&mut self.multiplier); + visitor.visit(&mut self.element); } } diff --git a/compiler/plc_ast/src/visitor.rs b/compiler/plc_ast/src/visitor.rs index baf498ed75..6c8a2defea 100644 --- a/compiler/plc_ast/src/visitor.rs +++ b/compiler/plc_ast/src/visitor.rs @@ -466,7 +466,8 @@ impl Walker for MultipliedStatement { where V: AstVisitor, { - visitor.visit(&self.element) + visitor.visit(&self.multiplier); + visitor.visit(&self.element); } } diff --git a/compiler/plc_lowering/src/array_lowering.rs b/compiler/plc_lowering/src/array_lowering.rs index 2fcf53ec22..59fc658f64 100644 --- a/compiler/plc_lowering/src/array_lowering.rs +++ b/compiler/plc_lowering/src/array_lowering.rs @@ -83,8 +83,8 @@ use std::collections::{BTreeSet, HashMap}; use plc::index::Index; use plc_ast::{ ast::{ - AstFactory, AstNode, AstStatement, CallStatement, CompilationUnit, DataTypeDeclaration, - MultipliedStatement, Variable, VariableBlock, VariableBlockType, + AstFactory, AstNode, AstStatement, CompilationUnit, DataTypeDeclaration, MultipliedStatement, + Variable, VariableBlock, VariableBlockType, }, control_statements::ForLoopStatement, literals::{Array, AstLiteral}, @@ -158,13 +158,11 @@ pub fn lower_literal_arrays(unit: &mut CompilationUnit, index: &Index, id_provid // Track which POUs need which counter variables for FOR loops. let mut pou_counters: HashMap> = HashMap::new(); - // First pass: rewrite `(CONST)(value)` call statements inside array literal - // initializers into proper `MultipliedStatement` nodes. The parser produces a - // `CallStatement { operator: ParenExpression(ref), parameters: value }` for the - // IEC 61131-3 syntax `[(NB)(value)]` because it cannot distinguish this from a - // function call at parse time. With the index available we can resolve the - // constant and emit the correct AST. - rewrite_const_multiplied_initializers(unit, index); + // Resolve constant references in MultipliedStatement multipliers to literal + // integers. The parser emits `(NB_BOOL)` as a reference node; with the index + // available we can replace it with the actual value so that downstream passes + // (flatten_expression_list, codegen memcpy) see a plain literal. + resolve_const_multipliers(unit, index); for implementation in &mut unit.implementations { let mut new_statements = Vec::new(); @@ -194,100 +192,78 @@ pub fn lower_literal_arrays(unit: &mut CompilationUnit, index: &Index, id_provid } } -// ── Constant-multiplier rewriting ─────────────────────────────────────────── +// ── Constant multiplier resolution ───────────────────────────────────────── -/// Rewrites `CallStatement` nodes inside `LiteralArray` initializers that represent -/// the IEC 61131-3 constant-multiplied syntax `[(CONST)(value)]`. -/// -/// The parser cannot distinguish `(NB_BOOL)(0.0033)` from a call on a parenthesized -/// expression, so it produces a `CallStatement { operator: ParenExpression(NB_BOOL), -/// parameters: 0.0033 }`. With the index available we can resolve the constant to -/// its integer value and rewrite the node into a proper `MultipliedStatement`. -fn rewrite_const_multiplied_initializers(unit: &mut CompilationUnit, index: &Index) { - // Rewrite in POU variable block initializers. +/// Walks all `MultipliedStatement` nodes in the compilation unit and resolves +/// constant-reference multipliers to literal integers. This ensures that +/// `[(NB_BOOL)(0.0033)]` where `NB_BOOL` is a constant produces a literal +/// multiplier that downstream passes (flatten, codegen memcpy) can handle. +fn resolve_const_multipliers(unit: &mut CompilationUnit, index: &Index) { for pou in &mut unit.pous { let pou_name = pou.name.clone(); for block in &mut pou.variable_blocks { for var in &mut block.variables { if let Some(init) = &mut var.initializer { - rewrite_array_literal_elements(init, index, Some(&pou_name)); + resolve_multipliers_in_node(init, index, Some(&pou_name)); } } } } - // Rewrite in global variable block initializers (no POU context). for block in &mut unit.global_vars { for var in &mut block.variables { if let Some(init) = &mut var.initializer { - rewrite_array_literal_elements(init, index, None); + resolve_multipliers_in_node(init, index, None); } } } - // Rewrite in user-type initializers (no POU context). for udt in &mut unit.user_types { if let Some(init) = &mut udt.initializer { - rewrite_array_literal_elements(init, index, None); + resolve_multipliers_in_node(init, index, None); } } - // Rewrite in implementation body statements (assignments generated by the initializer pass). for implementation in &mut unit.implementations { let type_name = implementation.type_name.clone(); for stmt in &mut implementation.statements { if let AstStatement::Assignment(data) = stmt.get_stmt_mut() { - rewrite_array_literal_elements(&mut data.right, index, Some(&type_name)); + resolve_multipliers_in_node(&mut data.right, index, Some(&type_name)); } } } } -/// If `node` is a `LiteralArray`, rewrites any constant-multiplied call statements -/// in its element tree into `MultipliedStatement` nodes. -fn rewrite_array_literal_elements(node: &mut AstNode, index: &Index, pou_name: Option<&str>) { - if let AstStatement::Literal(AstLiteral::Array(Array { elements: Some(elements) })) = node.get_stmt_mut() - { - rewrite_element_tree(elements, index, pou_name); - } -} - -/// Recursively walks the element tree of an array literal and rewrites -/// constant-multiplied call statements. -fn rewrite_element_tree(node: &mut AstNode, index: &Index, pou_name: Option<&str>) { +/// Recursively walks a node tree and resolves constant-reference multipliers +/// inside `MultipliedStatement` nodes to literal integers. +fn resolve_multipliers_in_node(node: &mut AstNode, index: &Index, pou_name: Option<&str>) { match node.get_stmt_mut() { + AstStatement::Literal(AstLiteral::Array(Array { elements: Some(elements) })) => { + resolve_multipliers_in_node(elements, index, pou_name); + } AstStatement::ExpressionList(exprs) => { for expr in exprs.iter_mut() { - rewrite_element_tree(expr, index, pou_name); + resolve_multipliers_in_node(expr, index, pou_name); } } - AstStatement::CallStatement(_) => { - if let Some(multiplied) = try_rewrite_call_as_multiplied(node, index, pou_name) { - *node = multiplied; + AstStatement::MultipliedStatement(data) => { + // Try to resolve the multiplier to a literal integer. + if data.multiplier.get_literal_integer_value().is_none() { + if let Some(value) = resolve_const_reference(&data.multiplier, index, pou_name) { + *data.multiplier = AstFactory::create_literal( + AstLiteral::Integer(value as i128), + data.multiplier.get_location(), + data.multiplier.get_id(), + ); + } } + // Also recurse into the element (for nested multiplied statements). + resolve_multipliers_in_node(&mut data.element, index, pou_name); } _ => {} } } -/// Attempts to rewrite a `CallStatement` into a `MultipliedStatement`. -/// Matches the pattern: `CallStatement { operator: ParenExpression(ref), parameters: value }` -/// where `ref` resolves to a constant integer variable in the index. -/// -/// NOTE: Non-constant variables used as multipliers (e.g. `[(n)(val)]` where `n` -/// is a `VAR` rather than `VAR CONSTANT`) cannot be supported without changing -/// `MultipliedStatement.multiplier` from `u32` to `Box`. For now only -/// constant integer references are rewritten; non-constant references are left as -/// `CallStatement` nodes and will produce a codegen error. -fn try_rewrite_call_as_multiplied(node: &AstNode, index: &Index, pou_name: Option<&str>) -> Option { - let AstStatement::CallStatement(CallStatement { operator, parameters }) = node.get_stmt() else { - return None; - }; - let AstStatement::ParenExpression(inner) = operator.get_stmt() else { - return None; - }; - let ref_name = inner.get_flat_reference_name()?; - let parameters = parameters.as_ref()?; - - // Look up the variable: first as a POU-local member (if we have a POU context), - // then as a global variable. +/// Tries to resolve a reference node to a constant integer value via the index. +fn resolve_const_reference(node: &AstNode, index: &Index, pou_name: Option<&str>) -> Option { + let ref_name = node.get_flat_reference_name()?; let variable = pou_name .and_then(|pou| index.find_member(pou, ref_name)) .or_else(|| index.find_global_variable(ref_name))?; @@ -296,16 +272,8 @@ fn try_rewrite_call_as_multiplied(node: &AstNode, index: &Index, pou_name: Optio return None; } - // Resolve the initial value to a literal integer. let initial_value = index.get_initial_value(&variable.initial_value)?; - let AstStatement::Literal(AstLiteral::Integer(value)) = initial_value.get_stmt() else { - return None; - }; - - let multiplier = u32::try_from(*value).ok()?; - let location = operator.get_location().span(¶meters.get_location()); - - Some(AstFactory::create_multiplied_statement(multiplier, *parameters.clone(), location, node.get_id())) + initial_value.get_literal_integer_value().map(|v| v as i64) } // ── Counter variable insertion ────────────────────────────────────────────── @@ -464,7 +432,10 @@ fn lower_array_elements( ) -> LoweredResult { let mut statements = Vec::new(); let mut counter_names = BTreeSet::new(); - let mut current_flat_offset: i64 = 0; + // The flat offset tracks where the next element goes. It is an AstNode + // expression so that non-literal multipliers (variables) can be accumulated + // into the offset for subsequent segments. + let mut current_flat_offset = make_int_literal(0, id_provider); match elements.get_stmt() { // [N(val)] — single multiplied segment @@ -472,8 +443,8 @@ fn lower_array_elements( let segment = lower_multiplied_segment( lhs, element, - *multiplier, - current_flat_offset, + multiplier, + ¤t_flat_offset, array_info, id_provider, ); @@ -489,22 +460,30 @@ fn lower_array_elements( let segment = lower_multiplied_segment( lhs, element, - *multiplier, - current_flat_offset, + multiplier, + ¤t_flat_offset, array_info, id_provider, ); counter_names.extend(segment.counter_names); statements.extend(segment.statements); - current_flat_offset += i64::from(*multiplier); + current_flat_offset = advance_offset(¤t_flat_offset, multiplier, id_provider); } _ => { - let indexed_lhs = - make_array_element_access(lhs, current_flat_offset, array_info, id_provider); + let indexed_lhs = make_array_element_access_expr( + lhs, + ¤t_flat_offset, + array_info, + id_provider, + ); let assignment = AstFactory::create_assignment(indexed_lhs, expr.clone(), id_provider.next_id()); statements.push(assignment); - current_flat_offset += 1; + current_flat_offset = advance_offset( + ¤t_flat_offset, + &make_int_literal(1, id_provider), + id_provider, + ); } } } @@ -512,7 +491,8 @@ fn lower_array_elements( // Single value (unusual but valid: `[val]`) _ => { - let indexed_lhs = make_array_element_access(lhs, current_flat_offset, array_info, id_provider); + let indexed_lhs = + make_array_element_access_expr(lhs, ¤t_flat_offset, array_info, id_provider); let assignment = AstFactory::create_assignment(indexed_lhs, elements.clone(), id_provider.next_id()); statements.push(assignment); @@ -522,42 +502,121 @@ fn lower_array_elements( LoweredResult { statements, counter_names } } -/// Lowers a `MultipliedStatement(count, element)` segment. -/// Small counts → unrolled individual assignments. -/// Large counts on single-dim → a FOR loop. -/// Large counts on multi-dim filling the whole array → nested FOR loops. -/// Large counts on multi-dim (partial) → unrolled individual assignments. +/// Lowers a `MultipliedStatement(multiplier, element)` segment. +/// +/// When the multiplier resolves to a literal integer: +/// - Small counts → unrolled individual assignments. +/// - Large counts on single-dim → a FOR loop. +/// - Large counts on multi-dim filling the whole array → nested FOR loops. +/// - Large counts on multi-dim (partial) → unrolled individual assignments. +/// +/// When the multiplier is a non-constant expression (e.g. a variable): +/// - Always emits a FOR loop using the expression as the loop bound. fn lower_multiplied_segment( lhs: &AstNode, element: &AstNode, - count: u32, - flat_offset: i64, + multiplier: &AstNode, + flat_offset: &AstNode, array_info: &ArrayInfo, id_provider: &mut IdProvider, ) -> LoweredResult { - let use_for_loop = count >= FOR_LOOP_THRESHOLD; + let count = resolve_multiplier_value(multiplier); + let flat_offset_val = flat_offset.get_literal_integer_value().map(|v| v as i64); - if !use_for_loop { - // Small count: unroll to individual indexed assignments. - return unroll_to_assignments(lhs, element, count, flat_offset, array_info, id_provider); - } + // When both the multiplier and the offset are known literals, we can use + // optimised paths (unrolling for small counts, nested loops for multi-dim). + if let (Some(count), Some(offset)) = (count, flat_offset_val) { + let use_for_loop = count >= FOR_LOOP_THRESHOLD; - if array_info.is_single_dim() { - // Single-dim, large: emit a FOR loop. - let start = array_info.dims[0].start + flat_offset; - let end = start + i64::from(count) - 1; - let for_loop = make_for_loop(lhs, element, start, end, id_provider); - LoweredResult { statements: vec![for_loop], counter_names: BTreeSet::from([IDX_VAR.to_string()]) } - } else if flat_offset == 0 && i64::from(count) == array_info.total_elements() { - // Multi-dim, full fill: emit nested FOR loops (one per dimension). - let for_loop = make_nested_for_loops(lhs, element, array_info, id_provider); - let counter_names = array_info.nested_counter_names().into_iter().collect(); - LoweredResult { statements: vec![for_loop], counter_names } - } else { - // Multi-dim, partial large: unroll with computed multi-dim indices. - // This is rare but correct — each element gets its own assignment. - unroll_to_assignments(lhs, element, count, flat_offset, array_info, id_provider) + if !use_for_loop { + return unroll_to_assignments(lhs, element, count, offset, array_info, id_provider); + } + + if array_info.is_single_dim() { + let start = array_info.dims[0].start + offset; + let end = start + i64::from(count) - 1; + let for_loop = make_for_loop(lhs, element, start, end, id_provider); + return LoweredResult { + statements: vec![for_loop], + counter_names: BTreeSet::from([IDX_VAR.to_string()]), + }; + } else if offset == 0 && i64::from(count) == array_info.total_elements() { + let for_loop = make_nested_for_loops(lhs, element, array_info, id_provider); + let counter_names = array_info.nested_counter_names().into_iter().collect(); + return LoweredResult { statements: vec![for_loop], counter_names }; + } else { + return unroll_to_assignments(lhs, element, count, offset, array_info, id_provider); + } } + + // Either the multiplier or the offset (or both) are non-literal expressions. + // Emit a FOR loop with expression-based bounds. + lower_expression_multiplier(lhs, element, multiplier, flat_offset, array_info, id_provider) +} + +/// Extracts the multiplier as a `u32` if it is a literal integer. +fn resolve_multiplier_value(multiplier: &AstNode) -> Option { + multiplier.get_literal_integer_value().and_then(|v| u32::try_from(v).ok()) +} + +/// Lowers a multiplied segment with expression-based multiplier and/or offset into a +/// FOR loop: `FOR __idx := dim_start + offset TO dim_start + offset + multiplier - 1 DO ... END_FOR` +fn lower_expression_multiplier( + lhs: &AstNode, + element: &AstNode, + multiplier: &AstNode, + flat_offset: &AstNode, + array_info: &ArrayInfo, + id_provider: &mut IdProvider, +) -> LoweredResult { + let loc = SourceLocation::internal(); + let dim_start = make_int_literal(array_info.dims[0].start, id_provider); + + // start = dim_start + flat_offset + let start_node = AstFactory::create_binary_expression( + dim_start, + plc_ast::ast::Operator::Plus, + flat_offset.clone(), + id_provider.next_id(), + ); + + // end = start + multiplier - 1 + let start_plus_mult = AstFactory::create_binary_expression( + start_node.clone(), + plc_ast::ast::Operator::Plus, + multiplier.clone(), + id_provider.next_id(), + ); + let end_node = AstFactory::create_binary_expression( + start_plus_mult, + plc_ast::ast::Operator::Minus, + make_int_literal(1, id_provider), + id_provider.next_id(), + ); + + let counter = make_member_reference(IDX_VAR, id_provider); + + // lhs[__literal_idx] + let idx_ref = make_member_reference(IDX_VAR, id_provider); + let indexed_lhs = + AstFactory::create_index_reference(idx_ref, Some(lhs.clone()), id_provider.next_id(), loc.clone()); + + let body_assignment = AstFactory::create_assignment(indexed_lhs, element.clone(), id_provider.next_id()); + + let for_loop = AstFactory::create_for_loop( + ForLoopStatement { + counter: Box::new(counter), + start: Box::new(start_node), + end: Box::new(end_node), + by_step: None, + body: vec![body_assignment], + end_location: loc.clone(), + }, + loc, + id_provider.next_id(), + ); + + LoweredResult { statements: vec![for_loop], counter_names: BTreeSet::from([IDX_VAR.to_string()]) } } /// Unrolls a multiplied segment into individual indexed assignments. @@ -581,6 +640,53 @@ fn unroll_to_assignments( // ── AST construction helpers ──────────────────────────────────────────────── +/// Advances the flat offset by the given increment (which may be a variable expression). +/// If both are literal integers, produces a simplified literal sum. Otherwise +/// produces a binary `offset + increment` expression. +fn advance_offset(offset: &AstNode, increment: &AstNode, id_provider: &mut IdProvider) -> AstNode { + if let (Some(a), Some(b)) = (offset.get_literal_integer_value(), increment.get_literal_integer_value()) { + make_int_literal(a as i64 + b as i64, id_provider) + } else { + AstFactory::create_binary_expression( + offset.clone(), + plc_ast::ast::Operator::Plus, + increment.clone(), + id_provider.next_id(), + ) + } +} + +/// Expression-based array element access. For single-dim arrays, produces +/// `lhs[dim_start + flat_offset_expr]`. Multi-dim arrays with non-literal +/// offsets are not supported (would require runtime division); this falls +/// back to `make_array_element_access` when the offset is a literal. +fn make_array_element_access_expr( + lhs: &AstNode, + flat_offset: &AstNode, + array_info: &ArrayInfo, + id_provider: &mut IdProvider, +) -> AstNode { + // Fast path: if the offset is a literal, use the existing i64-based function. + if let Some(offset_val) = flat_offset.get_literal_integer_value() { + return make_array_element_access(lhs, offset_val as i64, array_info, id_provider); + } + + // Non-literal offset — only supported for single-dim arrays. + let dim_start = make_int_literal(array_info.dims[0].start, id_provider); + let idx_expr = AstFactory::create_binary_expression( + dim_start, + plc_ast::ast::Operator::Plus, + flat_offset.clone(), + id_provider.next_id(), + ); + AstFactory::create_index_reference( + idx_expr, + Some(lhs.clone()), + id_provider.next_id(), + SourceLocation::internal(), + ) +} + /// Creates the appropriate indexed access for a given flat offset. /// For single-dim arrays: `lhs[start + flat_offset]`. /// For multi-dim arrays: `lhs[i0, i1, ...]` with computed indices. diff --git a/compiler/plc_lowering/src/helper.rs b/compiler/plc_lowering/src/helper.rs index a296e6feda..1ea23dda4c 100644 --- a/compiler/plc_lowering/src/helper.rs +++ b/compiler/plc_lowering/src/helper.rs @@ -11,8 +11,8 @@ pub fn is_const_expression(node: &AstNode, index: Option<&Index>, pou_name: Opti match node.get_stmt() { AstStatement::Literal(..) => true, AstStatement::ExpressionList(exprs) => exprs.iter().all(|e| is_const_expression(e, index, pou_name)), - AstStatement::MultipliedStatement(MultipliedStatement { element, .. }) => { - is_const_expression(element, index, pou_name) + AstStatement::MultipliedStatement(MultipliedStatement { multiplier, element }) => { + is_const_expression(multiplier, index, pou_name) && is_const_expression(element, index, pou_name) } AstStatement::ParenExpression(inner) => is_const_expression(inner, index, pou_name), AstStatement::Identifier(..) | AstStatement::ReferenceExpr(..) => { diff --git a/compiler/plc_lowering/src/tests/array_lowering_tests.rs b/compiler/plc_lowering/src/tests/array_lowering_tests.rs index 42c7d7ef05..02367ffcc8 100644 --- a/compiler/plc_lowering/src/tests/array_lowering_tests.rs +++ b/compiler/plc_lowering/src/tests/array_lowering_tests.rs @@ -1,4 +1,4 @@ -use plc_ast::{ast::AstStatement, control_statements::AstControlStatement}; +use insta::assert_snapshot; use plc_driver::parse_and_annotate; use plc_source::SourceCode; @@ -9,44 +9,18 @@ fn lower(src: &str) -> plc_driver::pipelines::AnnotatedProject { project } -/// Finds an implementation by name in the project's annotated units. -fn find_impl_stmts<'a>( - project: &'a plc_driver::pipelines::AnnotatedProject, - name: &str, -) -> &'a [plc_ast::ast::AstNode] { +/// Finds an implementation by name and returns its statements as pseudo-ST. +fn lowered_statements(project: &plc_driver::pipelines::AnnotatedProject, name: &str) -> String { for unit in &project.units { for imp in &unit.get_unit().implementations { if imp.name == name { - return &imp.statements; + return imp.statements.iter().map(|s| s.as_string()).collect::>().join("\n"); } } } panic!("Implementation '{name}' not found"); } -/// Returns the number of assignments in a statement list. -fn count_assignments(stmts: &[plc_ast::ast::AstNode]) -> usize { - stmts.iter().filter(|s| matches!(s.get_stmt(), AstStatement::Assignment(..))).count() -} - -/// Returns true if any statement is a FOR loop. -fn has_for_loop(stmts: &[plc_ast::ast::AstNode]) -> bool { - stmts - .iter() - .any(|s| matches!(s.get_stmt(), AstStatement::ControlStatement(AstControlStatement::ForLoop(..)))) -} - -/// Returns true if any top-level assignment has a `LiteralArray` on the RHS. -fn has_literal_array(stmts: &[plc_ast::ast::AstNode]) -> bool { - stmts.iter().any(|s| { - if let AstStatement::Assignment(data) = s.get_stmt() { - matches!(data.right.get_stmt(), AstStatement::Literal(plc_ast::literals::AstLiteral::Array(..))) - } else { - false - } - }) -} - // ═══════════════════════════════════════════════════════════════════════════ // Guard: constant arrays are NOT lowered (handled at codegen via memcpy) // ═══════════════════════════════════════════════════════════════════════════ @@ -61,8 +35,11 @@ fn constant_int_array_is_not_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(has_literal_array(stmts), "Constant array should NOT be lowered"); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr := + main := 0 + "#); } #[test] @@ -75,15 +52,15 @@ fn constant_expression_list_is_not_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(has_literal_array(stmts), "Constant expression list array should NOT be lowered"); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr := + main := 0 + "#); } #[test] fn const_variable_as_multiplier_is_rewritten_and_not_lowered() { - // `[(NB_BOOL)(0.0033)]` is parsed as a CallStatement but should be rewritten - // into a MultipliedStatement when NB_BOOL is a constant integer. Since all - // elements are constant REALs, the result should NOT be lowered further. let project = lower( " VAR_GLOBAL CONSTANT @@ -98,13 +75,15 @@ fn const_variable_as_multiplier_is_rewritten_and_not_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(has_literal_array(stmts), "Constant multiplied array should NOT be lowered"); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_MAX_TIME_BOOL__ctor(MAX_TIME_BOOL) + MAX_TIME_BOOL := + main := 0 + "#); } #[test] fn const_variable_as_multiplier_in_global_is_rewritten() { - // Same rewrite for global constant arrays with `[(CONST)(value)]` syntax. let project = lower( " VAR_GLOBAL CONSTANT @@ -117,15 +96,11 @@ fn const_variable_as_multiplier_in_global_is_rewritten() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert_eq!(count_assignments(stmts), 1); + assert_snapshot!(lowered_statements(&project, "main"), @"main := 0"); } #[test] fn const_variable_as_multiplier_with_non_constant_element_is_lowered() { - // `[(N)(ADR(x))]` — the multiplier is a constant but the element is a runtime - // value (function call). The CallStatement should be rewritten to a - // MultipliedStatement, then lowered into individual assignments. let project = lower( " VAR_GLOBAL CONSTANT @@ -141,16 +116,17 @@ fn const_variable_as_multiplier_with_non_constant_element_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "Non-constant elements should be lowered"); - assert_eq!(count_assignments(stmts), 3 + 1); // 3 element assignments + return assignment + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := ADR(x) + arr[1] := ADR(x) + arr[2] := ADR(x) + main := 0 + "#); } #[test] fn local_const_variable_as_multiplier_is_rewritten_and_not_lowered() { - // `[(N)(42)]` where N is a POU-local `VAR CONSTANT` should be rewritten - // into a MultipliedStatement. Since all elements are constant, the result - // should NOT be lowered further. let project = lower( " FUNCTION main : DINT @@ -164,15 +140,15 @@ fn local_const_variable_as_multiplier_is_rewritten_and_not_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(has_literal_array(stmts), "Local constant multiplied array should NOT be lowered"); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr := + main := 0 + "#); } #[test] fn local_const_variable_as_multiplier_with_non_constant_element_is_lowered() { - // `[(N)(ADR(x))]` where N is a POU-local `VAR CONSTANT` — the multiplier - // resolves to a constant but the element is a runtime value, so the array - // should be lowered into individual assignments. let project = lower( " FUNCTION main : DINT @@ -187,9 +163,65 @@ fn local_const_variable_as_multiplier_with_non_constant_element_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "Non-constant elements should be lowered"); - assert_eq!(count_assignments(stmts), 3 + 1); // 3 element assignments + return assignment + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := ADR(x) + arr[1] := ADR(x) + arr[2] := ADR(x) + main := 0 + "#); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Non-constant variable multipliers — lowered to FOR loops +// ═══════════════════════════════════════════════════════════════════════════ + +#[test] +fn non_constant_variable_multiplier_is_lowered_to_for_loop() { + let project = lower( + " + FUNCTION main : DINT + VAR + n : DINT := 5; + arr : ARRAY[0..4] OF DINT := [(n)(42)]; + END_VAR + main := 0; + END_FUNCTION + ", + ); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + n := 5 + __main_arr__ctor(arr) + FOR __literal_idx := 0 + 0 TO 0 + 0 + n - 1 DO + arr[__literal_idx] := 42 + END_FOR + main := 0 + "#); +} + +#[test] +fn non_constant_variable_multiplier_mixed_segments_lowered() { + let project = lower( + " + FUNCTION main : DINT + VAR + n : DINT := 3; + arr : ARRAY[0..4] OF DINT := [(n)(10), 20, 30]; + END_VAR + main := 0; + END_FUNCTION + ", + ); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + n := 3 + __main_arr__ctor(arr) + FOR __literal_idx := 0 + 0 TO 0 + 0 + n - 1 DO + arr[__literal_idx] := 10 + END_FOR + arr[0 + 0 + n] := 20 + arr[0 + 0 + n + 1] := 30 + main := 0 + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -209,11 +241,13 @@ fn small_adr_array_is_unrolled() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "ADR() array should be lowered"); - // 3 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 4); - assert!(!has_for_loop(stmts), "Small arrays should be unrolled"); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := ADR(x) + arr[1] := ADR(x) + arr[2] := ADR(x) + main := 0 + "#); } #[test] @@ -229,11 +263,13 @@ fn large_adr_array_uses_for_loop() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "ADR() array should be lowered"); - assert!(has_for_loop(stmts), "Large ADR arrays should use FOR loop"); - // Only `main := 0` as direct assignment - assert_eq!(count_assignments(stmts), 1); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + FOR __literal_idx := 0 TO 99 DO + arr[__literal_idx] := ADR(x) + END_FOR + main := 0 + "#); } #[test] @@ -251,14 +287,15 @@ fn adr_expression_list_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "ADR expression list should be lowered"); - // 3 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 4); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := ADR(a) + arr[1] := ADR(b) + arr[2] := ADR(c) + main := 0 + "#); } -/// Verify that FB member array non-constant initializers are lowered into -/// indexed assignments in the constructor body. #[test] fn fb_member_array_non_const_initializer_is_lowered() { let project = lower( @@ -271,10 +308,14 @@ fn fb_member_array_non_const_initializer_is_lowered() { END_FUNCTION_BLOCK ", ); - let stmts = find_impl_stmts(&project, "Foo__ctor"); - assert!(!has_literal_array(stmts), "FB member non-const array should be lowered"); - // 3 indexed assignments for arr + 1 init for x - assert_eq!(count_assignments(stmts), 4); + assert_snapshot!(lowered_statements(&project, "Foo__ctor"), @r#" + __Foo___vtable__ctor(self.__vtable) + __Foo_arr__ctor(self.arr) + self.arr[0] := ADR(x) + self.arr[1] := ADR(x) + self.arr[2] := ADR(x) + self.__vtable := ADR(__vtable_Foo_instance) + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -295,11 +336,13 @@ fn small_struct_array_is_unrolled() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "Struct literal array should be lowered"); - // 3 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 4); - assert!(!has_for_loop(stmts)); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := (a := 5, b := 10) + arr[1] := (a := 5, b := 10) + arr[2] := (a := 5, b := 10) + main := 0 + "#); } #[test] @@ -316,10 +359,13 @@ fn large_struct_array_uses_for_loop() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "Struct literal array should be lowered"); - assert!(has_for_loop(stmts), "Large struct arrays should use FOR loop"); - assert_eq!(count_assignments(stmts), 1); // only `main := 0` + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + FOR __literal_idx := 0 TO 99 DO + arr[__literal_idx] := (a := 7) + END_FOR + main := 0 + "#); } #[test] @@ -336,10 +382,12 @@ fn struct_expression_list_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts)); - // 2 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 3); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := (a := 1, b := 2) + arr[1] := (a := 3, b := 4) + main := 0 + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -359,17 +407,18 @@ fn variable_as_element_value_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "Variable-element array should be lowered"); - // 3 indexed assignments + `a := 42` + `main := 0` - assert_eq!(count_assignments(stmts), 5); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + a := 42 + __main_arr__ctor(arr) + arr[0] := a + arr[1] := a + arr[2] := a + main := 0 + "#); } #[test] fn variable_elements_in_expression_list_are_lowered() { - // The array uses an expression list where each element is a variable - // reference (`[a, a, a]`), so the initializer is non-constant and must - // be lowered into indexed runtime assignments. let project = lower( " FUNCTION main : DINT @@ -381,13 +430,16 @@ fn variable_elements_in_expression_list_are_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts), "Variable-element expression list should be lowered"); - // 3 indexed assignments + `a := 42` + `main := 0` - assert_eq!(count_assignments(stmts), 5); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + a := 42 + __main_arr__ctor(arr) + arr[0] := a + arr[1] := a + arr[2] := a + main := 0 + "#); } -/// Verify that only the non-constant array is lowered while the constant one is left as-is. #[test] fn shared_type_non_const_is_lowered_const_is_not() { let project = lower( @@ -404,12 +456,16 @@ fn shared_type_non_const_is_lowered_const_is_not() { END_FUNCTION ", ); - - let stmts = find_impl_stmts(&project, "main"); - // 3 indexed assignments for lowered_arr + `seed := 42` + const_arr literal assignment + `main := 0` - assert_eq!(count_assignments(stmts), 6); - // const_arr's literal array assignment is still present - assert!(has_literal_array(stmts), "Constant array should remain as literal"); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + seed := 42 + tarr__ctor(lowered_arr) + lowered_arr[0] := seed + lowered_arr[1] := seed + lowered_arr[2] := seed + tarr__ctor(const_arr) + const_arr := + main := 0 + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -430,10 +486,15 @@ fn mixed_adr_segments_are_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts)); - // 2 + 1 + 2 = 5 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 6); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0] := ADR(x) + arr[1] := ADR(x) + arr[2] := ADR(y) + arr[3] := ADR(x) + arr[4] := ADR(x) + main := 0 + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -453,10 +514,13 @@ fn nonzero_offset_adr_array_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts)); - // 3 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 4); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[5] := ADR(x) + arr[6] := ADR(x) + arr[7] := ADR(x) + main := 0 + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -476,10 +540,16 @@ fn multi_dim_adr_array_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts)); - // 6 indexed assignments + `main := 0` - assert_eq!(count_assignments(stmts), 7); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + __main_arr__ctor(arr) + arr[0, 0] := ADR(x) + arr[0, 1] := ADR(x) + arr[0, 2] := ADR(x) + arr[1, 0] := ADR(x) + arr[1, 1] := ADR(x) + arr[1, 2] := ADR(x) + main := 0 + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -499,28 +569,11 @@ fn type_level_struct_array_ctor_is_lowered() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "tarr__ctor"); - assert!(!has_literal_array(stmts)); - assert_eq!(count_assignments(stmts), 3); -} - -/// Verify that type-level struct array initializers are lowered in the constructor body. -#[test] -fn type_level_struct_array_is_lowered_in_ctor() { - let project = lower( - " - TYPE MyStruct : STRUCT a : DINT; END_STRUCT END_TYPE - TYPE tarr : ARRAY[0..2] OF MyStruct := [3((a := 42))]; END_TYPE - - FUNCTION main : DINT - VAR arr : tarr; END_VAR - main := 0; - END_FUNCTION - ", - ); - let stmts = find_impl_stmts(&project, "tarr__ctor"); - assert!(!has_literal_array(stmts), "Type-level struct array should be lowered"); - assert_eq!(count_assignments(stmts), 3); + assert_snapshot!(lowered_statements(&project, "tarr__ctor"), @r#" + self[0] := (a := 42) + self[1] := (a := 42) + self[2] := (a := 42) + "#); } // ═══════════════════════════════════════════════════════════════════════════ @@ -569,8 +622,8 @@ fn flat_to_indices_3d() { /// `FOR_LOOP_THRESHOLD` (32) is checked per `MultipliedStatement` /// segment, not against the total element count. An initializer split into -/// multiple sub-threshold segments - e.g. `[10(v), 10(v), 10(v), 10(v)]` -/// totalling 40 elements - is fully unrolled into 40 individual assignments +/// multiple sub-threshold segments — e.g. `[10(v), 10(v), 10(v), 10(v)]` +/// totalling 40 elements — is fully unrolled into 40 individual assignments /// instead of emitting a FOR loop, because each segment's count (10) is below /// the threshold. This documents the current behaviour so that /// any future fix is intentional. @@ -587,12 +640,49 @@ fn multi_segment_above_threshold_is_unrolled_per_segment() { END_FUNCTION ", ); - let stmts = find_impl_stmts(&project, "main"); - assert!(!has_literal_array(stmts)); - // Each of the 4 segments (count=10, below threshold=32) is unrolled - // individually, producing 40 assignments rather than a FOR loop. - // A fix would need to apply the threshold to the *total* element count. - assert!(!has_for_loop(stmts), "Each segment is below threshold so no FOR loop is emitted"); - // 40 indexed assignments + `v := 1` + `main := 0` - assert_eq!(count_assignments(stmts), 42); + assert_snapshot!(lowered_statements(&project, "main"), @r#" + v := 1 + __main_arr__ctor(arr) + arr[0] := v + arr[1] := v + arr[2] := v + arr[3] := v + arr[4] := v + arr[5] := v + arr[6] := v + arr[7] := v + arr[8] := v + arr[9] := v + arr[10] := v + arr[11] := v + arr[12] := v + arr[13] := v + arr[14] := v + arr[15] := v + arr[16] := v + arr[17] := v + arr[18] := v + arr[19] := v + arr[20] := v + arr[21] := v + arr[22] := v + arr[23] := v + arr[24] := v + arr[25] := v + arr[26] := v + arr[27] := v + arr[28] := v + arr[29] := v + arr[30] := v + arr[31] := v + arr[32] := v + arr[33] := v + arr[34] := v + arr[35] := v + arr[36] := v + arr[37] := v + arr[38] := v + arr[39] := v + main := 0 + "#); } diff --git a/src/parser/expressions_parser.rs b/src/parser/expressions_parser.rs index 94ac211d0e..ad5ae2e237 100644 --- a/src/parser/expressions_parser.rs +++ b/src/parser/expressions_parser.rs @@ -389,6 +389,25 @@ pub fn parse_call_statement(lexer: &mut ParseSession) -> Option { return Some(reference); } + // `(expr)(value)` — a ParenExpression followed by `(` is the IEC 61131-3 syntax + // for a multiplied array initializer, e.g. `[(NB_BOOL)(0.0033)]`. The inner + // expression is the multiplier (resolved later by lowering) and the parenthesized + // value is the repeated element. A ParenExpression is never a valid call operator + // (functions are called by name, pointers via `^`), so this is unambiguous. + if let AstStatement::ParenExpression(inner) = reference.get_stmt() { + let multiplier_node = *inner.clone(); + let element = parse_expression(lexer); + expect_token!(lexer, KeywordParensClose, None); + let end_location = lexer.location(); + lexer.advance(); + return Some(AstFactory::create_multiplied_statement( + multiplier_node, + element, + reference_loc.span(&end_location), + lexer.next_id(), + )); + } + let call = if lexer.try_consume(KeywordParensClose) { AstFactory::create_call_statement( reference, @@ -588,24 +607,29 @@ fn parse_literal_number(lexer: &mut ParseSession, is_negative: bool) -> Option() { + let multiplier_value = match result.parse::() { Ok(v) => Some(v), Err(e) => { lexer.accept_diagnostic( Diagnostic::new(format!("Failed to parse number {result}")) .with_error_code("E011") - .with_location(lexer.source_range_factory.create_range(location)) + .with_location(lexer.source_range_factory.create_range(location.clone())) .with_internal_error(e.into()), ); None } }?; + let multiplier_node = AstNode::new_literal( + AstLiteral::new_integer(multiplier_value), + lexer.next_id(), + lexer.source_range_factory.create_range(location), + ); let element = parse_expression(lexer); expect_token!(lexer, KeywordParensClose, None); let end = lexer.range().end; lexer.advance(); return Some(AstFactory::create_multiplied_statement( - multiplier, + multiplier_node, element, lexer.source_range_factory.create_range(start..end), lexer.next_id(), diff --git a/src/parser/tests/ast_visitor_tests.rs b/src/parser/tests/ast_visitor_tests.rs index c97533c520..5df91c9015 100644 --- a/src/parser/tests/ast_visitor_tests.rs +++ b/src/parser/tests/ast_visitor_tests.rs @@ -195,8 +195,8 @@ fn test_visit_multiplied_statement() { 3(a+b); END_PROGRAM", ); - // THEN we expect to visit the multiplied expression and its subexpressions - assert_eq!(get_character_range('a', 'b'), visitor.identifiers); + // THEN we expect to visit the multiplier literal and the subexpressions + assert_eq!(vec!["3", "a", "b"], visitor.identifiers); } #[test] @@ -438,7 +438,8 @@ fn test_visit_datatype_initializers_statement() { END_TYPE", ); // THEN we expect to visit all initializers and enum elements - let mut expected = ["1", "3", "4", "7"].iter().map(|c| c.to_string()).collect::>(); + // "4" appears twice: once from the array bounds `4..7` and once from the multiplier `4(d)` + let mut expected = ["1", "3", "4", "4", "7"].iter().map(|c| c.to_string()).collect::>(); expected.extend(get_character_range('a', 'm')); assert_eq!(expected, visitor.identifiers); diff --git a/src/parser/tests/initializer_parser_tests.rs b/src/parser/tests/initializer_parser_tests.rs index 90d5e6c15c..1266daa84d 100644 --- a/src/parser/tests/initializer_parser_tests.rs +++ b/src/parser/tests/initializer_parser_tests.rs @@ -276,7 +276,9 @@ fn array_initializer_multiplier_can_be_parsed() { LiteralArray { elements: Some( MultipliedStatement { - multiplier: 3, + multiplier: LiteralInteger { + value: 3, + }, element: LiteralInteger { value: 7, }, diff --git a/src/resolver/const_evaluator.rs b/src/resolver/const_evaluator.rs index 3a809f5a90..7bca88c6f4 100644 --- a/src/resolver/const_evaluator.rs +++ b/src/resolver/const_evaluator.rs @@ -549,13 +549,22 @@ fn evaluate_with_target_hint( .into_iter() .collect::>>(); + // Try to evaluate the multiplier as well (it may be a constant reference). + let evaluated_multiplier = + evaluate(multiplier, scope, index, lhs).ok().flatten().unwrap_or_else(|| *multiplier.clone()); + //return a new array, or return none if one was not resolvable inner_elements.map(|ie| { if let [ie] = ie.as_slice() { - AstFactory::create_multiplied_statement(*multiplier, ie.clone(), location.clone(), id) + AstFactory::create_multiplied_statement( + evaluated_multiplier.clone(), + ie.clone(), + location.clone(), + id, + ) } else { AstFactory::create_multiplied_statement( - *multiplier, + evaluated_multiplier.clone(), AstFactory::create_expression_list(ie, location.clone(), id), location.clone(), id, diff --git a/src/validation/array.rs b/src/validation/array.rs index 10e1988099..844204d33a 100644 --- a/src/validation/array.rs +++ b/src/validation/array.rs @@ -163,7 +163,12 @@ fn statement_to_array_length(context: &ValidationContext, s .and_then(|it| it.information.get_array_length(context.index)) .unwrap_or(0), - AstStatement::MultipliedStatement(data) => data.multiplier as usize, + AstStatement::MultipliedStatement(data) => { + // If the multiplier is a literal integer, use it directly. + // Otherwise (e.g. variable reference), we can't statically determine the + // count — skip validation by returning 0. + data.multiplier.get_literal_integer_value().unwrap_or(0) as usize + } AstStatement::ExpressionList { .. } | AstStatement::ParenExpression(_) => 1, // Any literal other than an array can be counted as 1 diff --git a/tests/lit/single/init/array_variable_multiplied.st b/tests/lit/single/init/array_variable_multiplied.st new file mode 100644 index 0000000000..b27d85cd3d --- /dev/null +++ b/tests/lit/single/init/array_variable_multiplied.st @@ -0,0 +1,15 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s +// Tests that a non-constant variable can be used as the multiplier +// in an array initializer with the [(var)(value)] syntax. + +FUNCTION main : DINT +VAR + n : DINT := 5; + arr : ARRAY [0..4] OF DINT := [(n)(77)]; +END_VAR + printf('%d$N', arr[0]); // CHECK: 77 + printf('%d$N', arr[2]); // CHECK: 77 + printf('%d$N', arr[4]); // CHECK: 77 + + main := 0; +END_FUNCTION diff --git a/tests/lit/single/init/array_variable_multiplied_mixed.st b/tests/lit/single/init/array_variable_multiplied_mixed.st new file mode 100644 index 0000000000..8d37a3f262 --- /dev/null +++ b/tests/lit/single/init/array_variable_multiplied_mixed.st @@ -0,0 +1,20 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s +// Tests that a non-constant variable multiplier works in a mixed expression +// list where subsequent elements use expression-based offsets. + +FUNCTION main : DINT +VAR + n : DINT := 3; + arr : ARRAY [0..5] OF DINT := [(n)(10), 20, 30, 40]; +END_VAR + // First n elements filled by the FOR loop + printf('%d$N', arr[0]); // CHECK: 10 + printf('%d$N', arr[1]); // CHECK: 10 + printf('%d$N', arr[2]); // CHECK: 10 + // Subsequent elements at offsets n, n+1, n+2 + printf('%d$N', arr[3]); // CHECK: 20 + printf('%d$N', arr[4]); // CHECK: 30 + printf('%d$N', arr[5]); // CHECK: 40 + + main := 0; +END_FUNCTION diff --git a/tests/lit/single/init/array_variable_multiplied_mixed_negative_offset.st b/tests/lit/single/init/array_variable_multiplied_mixed_negative_offset.st new file mode 100644 index 0000000000..bb7bf7287d --- /dev/null +++ b/tests/lit/single/init/array_variable_multiplied_mixed_negative_offset.st @@ -0,0 +1,23 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s +// Tests mixed variable multipliers with negative array bounds. +// [(var1)(10), 11, (var2)(12)] where var1=3, var2=7 + +FUNCTION main : DINT +VAR + var1 : DINT := 3; + var2 : DINT := 7; + arr : ARRAY [-10..0] OF DINT := [(var1)(10), 11, (var2)(12)]; +END_VAR + // var1=3 elements: arr[-10]=10, arr[-9]=10, arr[-8]=10 + printf('%d$N', arr[-10]); // CHECK: 10 + printf('%d$N', arr[-9]); // CHECK: 10 + printf('%d$N', arr[-8]); // CHECK: 10 + // individual: arr[-7]=11 + printf('%d$N', arr[-7]); // CHECK: 11 + // var2=7 elements: arr[-6]=12 .. arr[0]=12 + printf('%d$N', arr[-6]); // CHECK: 12 + printf('%d$N', arr[-3]); // CHECK: 12 + printf('%d$N', arr[0]); // CHECK: 12 + + main := 0; +END_FUNCTION diff --git a/tests/lit/single/init/array_variable_multiplied_negative_offset.st b/tests/lit/single/init/array_variable_multiplied_negative_offset.st new file mode 100644 index 0000000000..3553d8166b --- /dev/null +++ b/tests/lit/single/init/array_variable_multiplied_negative_offset.st @@ -0,0 +1,15 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s +// Tests that a non-constant variable multiplier works correctly with +// negative array bounds. + +FUNCTION main : DINT +VAR + n : DINT := 11; + arr : ARRAY [-10..0] OF DINT := [(n)(99)]; +END_VAR + printf('%d$N', arr[-10]); // CHECK: 99 + printf('%d$N', arr[-5]); // CHECK: 99 + printf('%d$N', arr[0]); // CHECK: 99 + + main := 0; +END_FUNCTION