Skip to content

Commit 73f559d

Browse files
authored
feat: Validate Array Ranges (PLC-lang#1195)
This commit adds a validation for the ranges of arrays, in which the start values must be less than the end values. For example something like `... ARRAY[1..-5] OF ...` is invalid because `-5` < `1`. Previously this would either panic (debug mode) or generate huge arrays (release mode) due to overflows.
1 parent 6a01122 commit 73f559d

File tree

6 files changed

+110
-6
lines changed

6 files changed

+110
-6
lines changed

compiler/plc_diagnostics/src/diagnostics.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use std::fmt::Display;
22

3+
use serde::{Deserialize, Serialize};
4+
35
use plc_ast::ast::AstNode;
46
use plc_source::{
57
source_location::{SourceLocation, SourceLocationFactory},
68
SourceCode,
79
};
8-
use serde::{Deserialize, Serialize};
910

1011
pub mod diagnostics_registry;
1112

compiler/plc_diagnostics/src/diagnostics/diagnostics_registry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ lazy_static! {
388388
Error,
389389
include_str!("./error_codes/E095.md"), // Action call without `()`
390390
E096, Warning, include_str!("./error_codes/E096.md"), // Integer Condition
391+
E097, Error, include_str!("./error_codes/E097.md"), // Invalid Array Range
391392
);
392393
}
393394

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Invalid Array Range
2+
3+
Ranges such as `ARRAY [0..-1]` are invalid in ST because end values of ranges must be greater than their start values.
4+
A valid range for the given statement would have been `ARRAY[-1..0]`.

src/typesystem.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,23 @@ impl DataTypeInformation {
692692
}
693693
}
694694

695+
/// Recursively retrieves all type names for nested arrays.
696+
///
697+
/// This is needed because a nested array such as `foo : ARRAY[1..5] OF ARRAY[5..10] OF DINT`
698+
/// provides range information for `[1..5]` and `[5..10]` in two different types stored in
699+
/// the index.
700+
pub fn get_inner_array_types<'a>(&'a self, types: &mut Vec<&'a DataTypeInformation>, index: &'a Index) {
701+
if let DataTypeInformation::Array { name, inner_type_name, .. } = self {
702+
if name != inner_type_name {
703+
types.push(self);
704+
705+
if let Some(ty) = index.find_type(inner_type_name).map(DataType::get_type_information) {
706+
ty.get_inner_array_types(types, index);
707+
}
708+
}
709+
}
710+
}
711+
695712
pub fn get_inner_pointer_type_name(&self) -> Option<&str> {
696713
match self {
697714
DataTypeInformation::Pointer { inner_type_name, .. } => Some(inner_type_name),

src/validation/tests/array_validation_test.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,52 @@ fn array_assignment_function_call() {
346346

347347
assert_snapshot!(diagnostics);
348348
}
349+
350+
#[test]
351+
fn validate_ranges() {
352+
let diagnostics = parse_and_validate_buffered(
353+
r"
354+
FUNCTION foo
355+
VAR
356+
arr_A : ARRAY[1..-5] OF DINT;
357+
arr_B : ARRAY[1..5] OF ARRAY[1..-10] OF DINT;
358+
arr_C : ARRAY[1..-5] OF ARRAY[1..-10] OF DINT;
359+
arr_D : ARRAY[1..5, 1..-5] OF DINT;
360+
END_VAR
361+
END_FUNCTION
362+
",
363+
);
364+
365+
assert_snapshot!(diagnostics, @r###"
366+
error[E097]: Invalid range `1..-5`, did you mean `-5..1`?
367+
┌─ <internal>:4:17
368+
369+
4 │ arr_A : ARRAY[1..-5] OF DINT;
370+
│ ^^^^^ Invalid range `1..-5`, did you mean `-5..1`?
371+
372+
error[E097]: Invalid range `1..-10`, did you mean `-10..1`?
373+
┌─ <internal>:5:17
374+
375+
5 │ arr_B : ARRAY[1..5] OF ARRAY[1..-10] OF DINT;
376+
│ ^^^^^ Invalid range `1..-10`, did you mean `-10..1`?
377+
378+
error[E097]: Invalid range `1..-5`, did you mean `-5..1`?
379+
┌─ <internal>:6:17
380+
381+
6 │ arr_C : ARRAY[1..-5] OF ARRAY[1..-10] OF DINT;
382+
│ ^^^^^ Invalid range `1..-5`, did you mean `-5..1`?
383+
384+
error[E097]: Invalid range `1..-10`, did you mean `-10..1`?
385+
┌─ <internal>:6:17
386+
387+
6 │ arr_C : ARRAY[1..-5] OF ARRAY[1..-10] OF DINT;
388+
│ ^^^^^ Invalid range `1..-10`, did you mean `-10..1`?
389+
390+
error[E097]: Invalid range `1..-5`, did you mean `-5..1`?
391+
┌─ <internal>:7:17
392+
393+
7 │ arr_D : ARRAY[1..5, 1..-5] OF DINT;
394+
│ ^^^^^ Invalid range `1..-5`, did you mean `-5..1`?
395+
396+
"###);
397+
}

src/validation/variable.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use plc_ast::ast::{ArgumentProperty, Pou, PouType, Variable, VariableBlock, VariableBlockType};
22
use plc_diagnostics::diagnostics::Diagnostic;
33

4+
use crate::typesystem::DataTypeInformation;
45
use crate::{index::const_expressions::ConstExpression, resolver::AnnotationMap};
56

67
use super::{
@@ -101,16 +102,47 @@ fn validate_vla(validator: &mut Validator, pou: Option<&Pou>, block: &VariableBl
101102
}
102103
}
103104

105+
fn validate_array_ranges<T>(validator: &mut Validator, variable: &Variable, context: &ValidationContext<T>)
106+
where
107+
T: AnnotationMap,
108+
{
109+
let ty_name = variable.data_type_declaration.get_name().unwrap_or_default();
110+
let ty_info = context.index.get_effective_type_or_void_by_name(ty_name).get_type_information();
111+
112+
if ty_info.is_array() {
113+
let mut types = vec![];
114+
ty_info.get_inner_array_types(&mut types, context.index);
115+
116+
for ty in types {
117+
let DataTypeInformation::Array { dimensions, .. } = ty else {
118+
unreachable!("`get_inner_types()` only operates on Arrays");
119+
};
120+
121+
for dimension in dimensions.iter().filter_map(|dim| dim.get_range(context.index).ok()) {
122+
let std::ops::Range { start, end } = dimension;
123+
124+
if start > end {
125+
validator.push_diagnostic(
126+
Diagnostic::new(format!(
127+
"Invalid range `{start}..{end}`, did you mean `{end}..{start}`?"
128+
))
129+
.with_location(variable.location.clone())
130+
.with_error_code("E097"),
131+
);
132+
}
133+
}
134+
}
135+
}
136+
}
137+
104138
fn validate_variable<T: AnnotationMap>(
105139
validator: &mut Validator,
106140
variable: &Variable,
107141
context: &ValidationContext<T>,
108142
) {
109-
if let Some(v_entry) = context
110-
.qualifier
111-
.and_then(|qualifier| context.index.find_member(qualifier, variable.name.as_str()))
112-
.or_else(|| context.index.find_global_variable(variable.name.as_str()))
113-
{
143+
validate_array_ranges(validator, variable, context);
144+
145+
if let Some(v_entry) = context.index.find_variable(context.qualifier, &[&variable.name]) {
114146
if let Some(initializer) = &variable.initializer {
115147
// Assume `foo : ARRAY[1..5] OF DINT := [...]`, here the first function call validates the
116148
// assignment as a whole whereas the second function call (`visit_statement`) validates the

0 commit comments

Comments
 (0)