Skip to content

Commit 9f534b2

Browse files
committed
Merge branch 'master' into mihr/typed-enums
2 parents 2460c83 + 6c860b9 commit 9f534b2

File tree

4 files changed

+312
-0
lines changed

4 files changed

+312
-0
lines changed

Cargo.lock

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ serial_test = "*"
5757
tempfile = "3"
5858
encoding_rs.workspace = true
5959
encoding_rs_io.workspace = true
60+
tabled = "0.20.0"
6061

6162
[lib]
6263
name = "rusty"

src/resolver/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ mod resolve_expressions_tests;
99
mod resolve_generic_calls;
1010
mod resolve_literals_tests;
1111
mod resolver_dependency_resolution;
12+
13+
mod resolver_test_util;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
use plc_ast::ast::AstId;
2+
use plc_ast::provider::IdProvider;
3+
use plc_ast::visitor::{AstVisitor, Walker};
4+
use plc_source::SourceCode;
5+
use std::collections::HashSet;
6+
use std::ops::Range;
7+
use tabled::derive::display;
8+
use tabled::settings::object::Columns;
9+
use tabled::settings::{Alignment, Style};
10+
use tabled::Tabled;
11+
12+
use crate::resolver::{AnnotationMap, AnnotationMapImpl, StatementAnnotation};
13+
use crate::{resolver::TypeAnnotator, test_utils::tests::index_with_ids};
14+
15+
/// Extracts marked ranges from the source code, returning the cleaned source and marker ranges.
16+
/// Markers are denoted by `{...}` in the source.
17+
fn extract_markers(src: &str) -> Result<(String, Vec<Range<usize>>), String> {
18+
let mut clean_src = String::new();
19+
let mut marker_ranges = Vec::new();
20+
let mut open_marker_positions = Vec::new();
21+
22+
for (i, c) in src.chars().enumerate() {
23+
match c {
24+
'{' => {
25+
open_marker_positions.push(clean_src.len());
26+
}
27+
'}' => {
28+
if let Some(start) = open_marker_positions.pop() {
29+
let end = clean_src.len();
30+
marker_ranges.push(start..end);
31+
} else {
32+
return Err(format!("Unmatched closing marker '}}' at offset {i}."));
33+
}
34+
}
35+
_ => {
36+
clean_src.push(c);
37+
}
38+
}
39+
}
40+
41+
if !open_marker_positions.is_empty() {
42+
return Err(format!("Unmatched opening marker '{{' at offset {open_marker_positions:?}."));
43+
}
44+
45+
Ok((clean_src, marker_ranges))
46+
}
47+
48+
/// Collects AST nodes whose spans match the provided marker ranges.
49+
struct ExpressionCollector {
50+
// the location of the marked expression and its id
51+
expressions: Vec<(Range<usize>, AstId)>,
52+
// the location of un-processed markers
53+
// once an expression was visisted for a marker, it is removed from this set
54+
ordered_markers: HashSet<Range<usize>>,
55+
}
56+
57+
impl ExpressionCollector {
58+
/// Creates a new ExpressionCollector with the given marker ranges.
59+
///
60+
/// see also `fn extract_markers(...)`
61+
pub fn new(markers: &[Range<usize>]) -> Self {
62+
Self { expressions: Vec::new(), ordered_markers: markers.iter().cloned().collect() }
63+
}
64+
}
65+
66+
impl AstVisitor for ExpressionCollector {
67+
fn visit(&mut self, node: &plc_ast::ast::AstNode) {
68+
let current_range = node.get_location().to_range().unwrap_or_else(|| 0..0);
69+
70+
// lets see if we have a marker for this range
71+
if self.ordered_markers.remove(&current_range) {
72+
// record the id and the range
73+
self.expressions.push((current_range, node.get_id()))
74+
}
75+
76+
node.walk(self);
77+
}
78+
}
79+
80+
/// Represents the result of resolving an expression, including its type and hint.
81+
#[derive(Tabled)]
82+
struct ResolveResult {
83+
#[tabled(rename = "EXPR")]
84+
expression: String,
85+
#[tabled(rename = "TYPE", display("display::option", "-"))]
86+
ty: Option<String>,
87+
#[tabled(rename = "HINT", display("display::option", "-"))]
88+
hint: Option<String>,
89+
}
90+
91+
/// Helper function to display the type of a StatementAnnotation.
92+
fn display_annotation(
93+
annotation: &StatementAnnotation,
94+
annotation_map: &AnnotationMapImpl,
95+
) -> Option<String> {
96+
annotation_map.get_type_name_for_annotation(annotation).map(|it| it.to_string())
97+
}
98+
99+
/// Runs type resolution on the provided source code and returns a formatted table of results.
100+
/// Expressions to be resolved should be surrounded by `{}` in the source.
101+
pub fn test_resolve<T: Into<SourceCode>>(src: T) -> Result<String, String> {
102+
let id_provider = IdProvider::default();
103+
104+
let (clean_src, marker_ranges) = extract_markers(src.into().source.as_str())?;
105+
106+
let (unit, index) = index_with_ids(clean_src.as_str(), id_provider.clone());
107+
let (annotation_map, ..) = TypeAnnotator::visit_unit(&index, &unit, id_provider);
108+
109+
let mut collector = ExpressionCollector::new(&marker_ranges);
110+
collector.visit_compilation_unit(&unit);
111+
112+
let results = collector
113+
.expressions
114+
.iter()
115+
.map(|(marker, ast_id)| {
116+
let ty = annotation_map
117+
.get_with_id(*ast_id)
118+
.and_then(|annotation| display_annotation(annotation, &annotation_map));
119+
let hint = annotation_map
120+
.get_hint_with_id(*ast_id)
121+
.and_then(|annotation| display_annotation(annotation, &annotation_map));
122+
123+
ResolveResult { expression: clean_src[marker.start..marker.end].to_string(), ty, hint }
124+
})
125+
.collect::<Vec<_>>();
126+
127+
let table = tabled::Table::new(results)
128+
.with(Style::psql())
129+
.modify(Columns::first(), Alignment::right())
130+
.to_string();
131+
132+
if collector.ordered_markers.is_empty() {
133+
Ok(table)
134+
} else {
135+
Err(format!(
136+
"Cannot find Expression for markers at {:?}.",
137+
collector.ordered_markers.iter().collect::<Vec<_>>()
138+
))
139+
}
140+
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use insta::assert_snapshot;
145+
146+
use super::*;
147+
148+
#[test]
149+
fn print_marked_expressions() {
150+
let src = "
151+
FUNCTION foo
152+
VAR
153+
x : INT;
154+
y : DINT;
155+
END_VAR
156+
{{x} + {y}} // surrounded expressions to be evaluated
157+
END_FUNCTION
158+
";
159+
160+
let resolves = test_resolve(src).unwrap();
161+
assert_snapshot!(resolves, @r"
162+
EXPR | TYPE | HINT
163+
-------+------+------
164+
x + y | DINT | -
165+
x | INT | DINT
166+
y | DINT | -
167+
");
168+
}
169+
170+
#[test]
171+
fn test_unclosed_marker() {
172+
let src_unmatched_open = "
173+
FUNCTION foo
174+
VAR
175+
x : INT;
176+
y : DINT;
177+
END_VAR
178+
{x + {y // Missing closing markers
179+
END_FUNCTION
180+
";
181+
182+
assert_eq!(
183+
test_resolve(src_unmatched_open),
184+
Err("Unmatched opening marker '{' at offset [120, 124].".to_string())
185+
);
186+
}
187+
188+
#[test]
189+
fn test_invalid_closing_marker() {
190+
let src_unmatched_close = "
191+
FUNCTION foo
192+
VAR
193+
x : INT;
194+
y : DINT;
195+
END_VAR
196+
{x + y}} // Extra closing marker
197+
END_FUNCTION
198+
";
199+
200+
assert_eq!(
201+
test_resolve(src_unmatched_close),
202+
Err("Unmatched closing marker '}' at offset 127.".to_string())
203+
);
204+
}
205+
206+
#[test]
207+
fn test_no_markers() {
208+
let src_no_markers = "
209+
FUNCTION foo
210+
VAR
211+
x : INT;
212+
y : DINT;
213+
END_VAR
214+
x + y // No markers present
215+
END_FUNCTION
216+
";
217+
218+
let resolves = test_resolve(src_no_markers).unwrap();
219+
assert_snapshot!(resolves, @r"
220+
EXPR | TYPE | HINT
221+
------+------+------
222+
");
223+
}
224+
225+
#[test]
226+
fn test_invalid_marker_position() {
227+
let src_invalid_marker = "
228+
FUNCTION foo
229+
VAR
230+
x : INT;
231+
y : DINT;
232+
END_VAR
233+
{x +} y // marker does not match any expression
234+
END_FUNCTION
235+
";
236+
237+
assert_eq!(
238+
test_resolve(src_invalid_marker),
239+
Err("Cannot find Expression for markers at [120..123].".to_string())
240+
);
241+
}
242+
}

0 commit comments

Comments
 (0)