Skip to content

Commit 6c86abd

Browse files
authored
Cyclic and multiple include errors (#2278)
This PR adds cyclic and multiple include errors to the qasm parser.
1 parent 34ce0a7 commit 6c86abd

File tree

12 files changed

+404
-50
lines changed

12 files changed

+404
-50
lines changed

compiler/qsc_qasm3/src/io.rs

+152-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ use rustc_hash::FxHashMap;
1717
/// Implementations of this trait can be provided to the parser
1818
/// to customize how include files are resolved.
1919
pub trait SourceResolver {
20+
fn ctx(&mut self) -> &mut SourceResolverContext;
21+
2022
#[cfg(feature = "fs")]
21-
fn resolve<P>(&self, path: P) -> miette::Result<(PathBuf, String), Error>
23+
fn resolve<P>(&mut self, path: P) -> miette::Result<(PathBuf, String), Error>
2224
where
2325
P: AsRef<Path>,
2426
{
@@ -27,8 +29,14 @@ pub trait SourceResolver {
2729
"Could not resolve include file path: {e}"
2830
)))
2931
})?;
32+
33+
self.ctx().check_include_errors(&path)?;
34+
3035
match std::fs::read_to_string(&path) {
31-
Ok(source) => Ok((path, source)),
36+
Ok(source) => {
37+
self.ctx().add_path_to_include_graph(path.clone());
38+
Ok((path, source))
39+
}
3240
Err(_) => Err(Error(ErrorKind::NotFound(format!(
3341
"Could not resolve include file: {}",
3442
path.display()
@@ -41,6 +49,137 @@ pub trait SourceResolver {
4149
P: AsRef<Path>;
4250
}
4351

52+
pub struct IncludeGraphNode {
53+
parent: Option<PathBuf>,
54+
children: Vec<PathBuf>,
55+
}
56+
57+
#[derive(Default)]
58+
pub struct SourceResolverContext {
59+
/// A graph representation of the include chain.
60+
include_graph: FxHashMap<PathBuf, IncludeGraphNode>,
61+
/// Path being resolved.
62+
current_file: Option<PathBuf>,
63+
}
64+
65+
impl SourceResolverContext {
66+
pub fn check_include_errors(&mut self, path: &PathBuf) -> miette::Result<(), Error> {
67+
// If the new path makes a cycle in the include graph, we return
68+
// an error showing the cycle to the user.
69+
if let Some(cycle) = self.cycle_made_by_including_path(path) {
70+
return Err(Error(ErrorKind::CyclicInclude(cycle)));
71+
}
72+
73+
// If the new path doesn't make a cycle but it was already
74+
// included before, we return a `MultipleInclude`
75+
// error saying "<FILE> was already included in <FILE>".
76+
if let Some(parent_file) = self.path_was_already_included(path) {
77+
return Err(Error(ErrorKind::MultipleInclude(
78+
path.display().to_string(),
79+
parent_file.display().to_string(),
80+
)));
81+
}
82+
83+
self.add_path_to_include_graph(path.clone());
84+
85+
Ok(())
86+
}
87+
88+
/// Changes `current_path` to its parent in the `include_graph`.
89+
pub fn pop_current_file(&mut self) {
90+
let parent = self
91+
.current_file
92+
.as_ref()
93+
.and_then(|file| self.include_graph.get(file).map(|node| node.parent.clone()))
94+
.flatten();
95+
self.current_file = parent;
96+
}
97+
98+
/// If including the path makes a cycle, returns a vector of the paths
99+
/// that make the cycle. Else, returns None.
100+
///
101+
/// To check if adding `path` to the include graph creates a cycle we just
102+
/// need to verify if path is an ancestor of the current file.
103+
fn cycle_made_by_including_path(&self, path: &PathBuf) -> Option<Cycle> {
104+
let mut current_file = self.current_file.as_ref();
105+
let mut paths = Vec::new();
106+
107+
while let Some(file) = current_file {
108+
paths.push(file.clone());
109+
current_file = self.get_parent(file);
110+
if file == path {
111+
paths.reverse();
112+
paths.push(path.clone());
113+
return Some(Cycle { paths });
114+
}
115+
}
116+
117+
None
118+
}
119+
120+
/// Returns the file that included `path`.
121+
/// Returns `None` if `path` is the "main" file.
122+
fn get_parent(&self, path: &PathBuf) -> Option<&PathBuf> {
123+
self.include_graph
124+
.get(path)
125+
.and_then(|node| node.parent.as_ref())
126+
}
127+
128+
/// If the path was already included, returns the path of the file that
129+
/// included it. Else, returns None.
130+
fn path_was_already_included(&self, path: &PathBuf) -> Option<PathBuf> {
131+
// SAFETY: The call to expect should be unreachable, since the parent
132+
// will only be None for the "main" file. But including the
133+
// main file will trigger a cyclic include error before this
134+
// function is called.
135+
self.include_graph
136+
.get(path)
137+
.map(|node| node.parent.clone().expect("unreachable"))
138+
}
139+
140+
/// Adds `path` as a child of `current_path`, and then changes
141+
/// the `current_path` to `path`.
142+
fn add_path_to_include_graph(&mut self, path: PathBuf) {
143+
// 1. Add path to the current file children.
144+
self.current_file.as_ref().and_then(|file| {
145+
self.include_graph
146+
.get_mut(file)
147+
.map(|node| node.children.push(path.clone()))
148+
});
149+
150+
// 2. Add path to the include graph.
151+
self.include_graph.insert(
152+
path.clone(),
153+
IncludeGraphNode {
154+
parent: self.current_file.clone(),
155+
children: Vec::new(),
156+
},
157+
);
158+
159+
// 3. Update the current file.
160+
self.current_file = Some(path);
161+
}
162+
}
163+
164+
/// We use this struct to print a nice error message when we find a cycle.
165+
#[derive(Debug, Clone, Eq, PartialEq)]
166+
pub struct Cycle {
167+
paths: Vec<PathBuf>,
168+
}
169+
170+
impl std::fmt::Display for Cycle {
171+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172+
let parents = self.paths[0..(self.paths.len() - 1)].iter();
173+
let children = self.paths[1..].iter();
174+
175+
for (parent, child) in parents.zip(children) {
176+
write!(f, "\n {} includes {}", parent.display(), child.display())?;
177+
}
178+
179+
Ok(())
180+
}
181+
}
182+
44183
/// A source resolver that resolves include files from an in-memory map.
45184
/// This is useful for testing or environments in which file system access
46185
/// is not available.
@@ -49,6 +188,7 @@ pub trait SourceResolver {
49188
/// contents prior to parsing.
50189
pub struct InMemorySourceResolver {
51190
sources: FxHashMap<PathBuf, String>,
191+
ctx: SourceResolverContext,
52192
}
53193

54194
impl FromIterator<(Arc<str>, Arc<str>)> for InMemorySourceResolver {
@@ -58,16 +198,24 @@ impl FromIterator<(Arc<str>, Arc<str>)> for InMemorySourceResolver {
58198
map.insert(PathBuf::from(path.to_string()), source.to_string());
59199
}
60200

61-
InMemorySourceResolver { sources: map }
201+
InMemorySourceResolver {
202+
sources: map,
203+
ctx: Default::default(),
204+
}
62205
}
63206
}
64207

65208
impl SourceResolver for InMemorySourceResolver {
66-
fn resolve<P>(&self, path: P) -> miette::Result<(PathBuf, String), Error>
209+
fn ctx(&mut self) -> &mut SourceResolverContext {
210+
&mut self.ctx
211+
}
212+
213+
fn resolve<P>(&mut self, path: P) -> miette::Result<(PathBuf, String), Error>
67214
where
68215
P: AsRef<Path>,
69216
{
70217
let path = path.as_ref();
218+
self.ctx().check_include_errors(&path.to_path_buf())?;
71219
match self.sources.get(path) {
72220
Some(source) => Ok((path.to_owned(), source.clone())),
73221
None => Err(Error(ErrorKind::NotFound(format!(

compiler/qsc_qasm3/src/io/error.rs

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
use miette::Diagnostic;
55
use thiserror::Error;
66

7+
use super::Cycle;
8+
79
#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)]
810
#[error(transparent)]
911
#[diagnostic(transparent)]
@@ -15,6 +17,10 @@ pub enum ErrorKind {
1517
NotFound(String),
1618
#[error("IO Error: {0}")]
1719
IO(String),
20+
#[error("{0} was already included in: {1}")]
21+
MultipleInclude(String, String),
22+
#[error("Cyclic include:{0}")]
23+
CyclicInclude(Cycle),
1824
}
1925

2026
impl From<Error> for crate::Error {

compiler/qsc_qasm3/src/parse.rs

+9-5
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ impl QasmParseResult {
6767
/// This function will resolve includes using the provided resolver.
6868
/// If an include file cannot be resolved, an error will be returned.
6969
/// If a file is included recursively, a stack overflow occurs.
70-
pub fn parse_source<S, P, R>(source: S, path: P, resolver: &R) -> miette::Result<QasmParseResult>
70+
pub fn parse_source<S, P, R>(
71+
source: S,
72+
path: P,
73+
resolver: &mut R,
74+
) -> miette::Result<QasmParseResult>
7175
where
7276
S: AsRef<str>,
7377
P: AsRef<Path>,
@@ -203,7 +207,7 @@ impl QasmSource {
203207
/// This function is the start of a recursive process that will resolve all
204208
/// includes in the QASM file. Any includes are parsed as if their contents
205209
/// were defined where the include statement is.
206-
fn parse_qasm_file<P, R>(path: P, resolver: &R) -> miette::Result<QasmSource>
210+
fn parse_qasm_file<P, R>(path: P, resolver: &mut R) -> miette::Result<QasmSource>
207211
where
208212
P: AsRef<Path>,
209213
R: SourceResolver,
@@ -212,7 +216,7 @@ where
212216
parse_qasm_source(source, path, resolver)
213217
}
214218

215-
fn parse_qasm_source<S, P, R>(source: S, path: P, resolver: &R) -> miette::Result<QasmSource>
219+
fn parse_qasm_source<S, P, R>(source: S, path: P, resolver: &mut R) -> miette::Result<QasmSource>
216220
where
217221
S: AsRef<str>,
218222
P: AsRef<Path>,
@@ -224,7 +228,7 @@ where
224228

225229
fn parse_source_and_includes<P: AsRef<str>, R>(
226230
source: P,
227-
resolver: &R,
231+
resolver: &mut R,
228232
) -> miette::Result<(ParseOrErrors<SourceFile>, Vec<QasmSource>)>
229233
where
230234
R: SourceResolver,
@@ -240,7 +244,7 @@ where
240244

241245
fn parse_includes<R>(
242246
syntax_ast: &ParseOrErrors<oq3_syntax::SourceFile>,
243-
resolver: &R,
247+
resolver: &mut R,
244248
) -> miette::Result<Vec<QasmSource>>
245249
where
246250
R: SourceResolver,

compiler/qsc_qasm3/src/parser.rs

+15-6
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ fn update_offsets(source_map: &SourceMap, source: &mut QasmSource) {
110110
/// This function will resolve includes using the provided resolver.
111111
/// If an include file cannot be resolved, an error will be returned.
112112
/// If a file is included recursively, a stack overflow occurs.
113-
pub fn parse_source<S, P, R>(source: S, path: P, resolver: &R) -> QasmParseResult
113+
pub fn parse_source<S, P, R>(source: S, path: P, resolver: &mut R) -> QasmParseResult
114114
where
115115
S: AsRef<str>,
116116
P: AsRef<Path>,
@@ -230,13 +230,22 @@ impl QasmSource {
230230
/// This function is the start of a recursive process that will resolve all
231231
/// includes in the QASM file. Any includes are parsed as if their contents
232232
/// were defined where the include statement is.
233-
fn parse_qasm_file<P, R>(path: P, resolver: &R) -> QasmSource
233+
fn parse_qasm_file<P, R>(path: P, resolver: &mut R) -> QasmSource
234234
where
235235
P: AsRef<Path>,
236236
R: SourceResolver,
237237
{
238238
match resolver.resolve(&path) {
239-
Ok((path, source)) => parse_qasm_source(source, path, resolver),
239+
Ok((path, source)) => {
240+
let parse_result = parse_qasm_source(source, path, resolver);
241+
242+
// Once we finish parsing the source, we pop the file from the
243+
// resolver. This is needed to keep track of multiple includes
244+
// and cyclic includes.
245+
resolver.ctx().pop_current_file();
246+
247+
parse_result
248+
}
240249
Err(e) => {
241250
let error = crate::parser::error::ErrorKind::IO(e);
242251
let error = crate::parser::Error(error, None);
@@ -255,7 +264,7 @@ where
255264
}
256265
}
257266

258-
fn parse_qasm_source<S, P, R>(source: S, path: P, resolver: &R) -> QasmSource
267+
fn parse_qasm_source<S, P, R>(source: S, path: P, resolver: &mut R) -> QasmSource
259268
where
260269
S: AsRef<str>,
261270
P: AsRef<Path>,
@@ -267,7 +276,7 @@ where
267276

268277
fn parse_source_and_includes<P: AsRef<str>, R>(
269278
source: P,
270-
resolver: &R,
279+
resolver: &mut R,
271280
) -> (Program, Vec<Error>, Vec<QasmSource>)
272281
where
273282
R: SourceResolver,
@@ -277,7 +286,7 @@ where
277286
(program, errors, included)
278287
}
279288

280-
fn parse_includes<R>(program: &Program, resolver: &R) -> Vec<QasmSource>
289+
fn parse_includes<R>(program: &Program, resolver: &mut R) -> Vec<QasmSource>
281290
where
282291
R: SourceResolver,
283292
{

compiler/qsc_qasm3/src/parser/tests.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ pub(crate) fn parse_all<P>(
2323
where
2424
P: AsRef<Path>,
2525
{
26-
let resolver = InMemorySourceResolver::from_iter(sources);
26+
let mut resolver = InMemorySourceResolver::from_iter(sources);
2727
let (path, source) = resolver
2828
.resolve(path.as_ref())
2929
.map_err(|e| vec![Report::new(e)])?;
30-
let res = crate::parser::parse_source(source, path, &resolver);
30+
let res = crate::parser::parse_source(source, path, &mut resolver);
3131
if res.source.has_errors() {
3232
let errors = res
3333
.errors()
@@ -44,8 +44,8 @@ pub(crate) fn parse<S>(source: S) -> miette::Result<QasmParseResult, Vec<Report>
4444
where
4545
S: AsRef<str>,
4646
{
47-
let resolver = InMemorySourceResolver::from_iter([("test".into(), source.as_ref().into())]);
48-
let res = parse_source(source, "test", &resolver);
47+
let mut resolver = InMemorySourceResolver::from_iter([("test".into(), source.as_ref().into())]);
48+
let res = parse_source(source, "test", &mut resolver);
4949
if res.source.has_errors() {
5050
let errors = res
5151
.errors()

compiler/qsc_qasm3/src/semantic.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,18 @@ where
9696
S: AsRef<str>,
9797
P: AsRef<Path>,
9898
{
99-
let resolver = InMemorySourceResolver::from_iter([(
99+
let mut resolver = InMemorySourceResolver::from_iter([(
100100
path.as_ref().display().to_string().into(),
101101
source.as_ref().into(),
102102
)]);
103-
parse_source(source, path, &resolver)
103+
parse_source(source, path, &mut resolver)
104104
}
105105

106106
/// Parse a QASM file and return the parse result.
107107
/// This function will resolve includes using the provided resolver.
108108
/// If an include file cannot be resolved, an error will be returned.
109109
/// If a file is included recursively, a stack overflow occurs.
110-
pub fn parse_source<S, P, R>(source: S, path: P, resolver: &R) -> QasmSemanticParseResult
110+
pub fn parse_source<S, P, R>(source: S, path: P, resolver: &mut R) -> QasmSemanticParseResult
111111
where
112112
S: AsRef<str>,
113113
P: AsRef<Path>,

0 commit comments

Comments
 (0)