Skip to content

Commit 864d046

Browse files
authored
Merge pull request #1912 from mo8it/performance
Optimizations 1
2 parents b13bafa + a27741b commit 864d046

File tree

5 files changed

+155
-62
lines changed

5 files changed

+155
-62
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ console = "0.15.8"
1414
glob = "0.3.0"
1515
indicatif = "0.17.8"
1616
notify-debouncer-mini = "0.4.1"
17-
regex = "1.10.3"
1817
serde_json = "1.0.114"
1918
serde = { version = "1.0.197", features = ["derive"] }
2019
shlex = "1.3.0"
2120
toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
2221
which = "6.0.1"
22+
winnow = "0.6.5"
2323

2424
[[bin]]
2525
name = "rustlings"

src/exercise.rs

Lines changed: 124 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1-
use regex::Regex;
21
use serde::Deserialize;
3-
use std::env;
42
use std::fmt::{self, Display, Formatter};
53
use std::fs::{self, remove_file, File};
6-
use std::io::Read;
4+
use std::io::{self, BufRead, BufReader};
75
use std::path::PathBuf;
8-
use std::process::{self, Command, Stdio};
6+
use std::process::{self, exit, Command, Stdio};
7+
use std::{array, env, mem};
8+
use winnow::ascii::{space0, Caseless};
9+
use winnow::combinator::opt;
10+
use winnow::Parser;
911

1012
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
1113
const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"];
1214
const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"];
13-
const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE";
1415
const CONTEXT: usize = 2;
1516
const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml";
1617

18+
// Checks if the line contains the "I AM NOT DONE" comment.
19+
fn contains_not_done_comment(input: &str) -> bool {
20+
(
21+
space0::<_, ()>,
22+
"//",
23+
opt('/'),
24+
space0,
25+
Caseless("I AM NOT DONE"),
26+
)
27+
.parse_next(&mut &*input)
28+
.is_ok()
29+
}
30+
1731
// Get a temporary file name that is hopefully unique
1832
#[inline]
1933
fn temp_file() -> String {
@@ -211,51 +225,101 @@ path = "{}.rs""#,
211225
}
212226

213227
pub fn state(&self) -> State {
214-
let mut source_file = File::open(&self.path).unwrap_or_else(|e| {
215-
panic!(
216-
"We were unable to open the exercise file {}! {e}",
217-
self.path.display()
218-
)
228+
let source_file = File::open(&self.path).unwrap_or_else(|e| {
229+
println!(
230+
"Failed to open the exercise file {}: {e}",
231+
self.path.display(),
232+
);
233+
exit(1);
219234
});
220-
221-
let source = {
222-
let mut s = String::new();
223-
source_file.read_to_string(&mut s).unwrap_or_else(|e| {
224-
panic!(
225-
"We were unable to read the exercise file {}! {e}",
226-
self.path.display()
227-
)
228-
});
229-
s
235+
let mut source_reader = BufReader::new(source_file);
236+
237+
// Read the next line into `buf` without the newline at the end.
238+
let mut read_line = |buf: &mut String| -> io::Result<_> {
239+
let n = source_reader.read_line(buf)?;
240+
if buf.ends_with('\n') {
241+
buf.pop();
242+
if buf.ends_with('\r') {
243+
buf.pop();
244+
}
245+
}
246+
Ok(n)
230247
};
231248

232-
let re = Regex::new(I_AM_DONE_REGEX).unwrap();
249+
let mut current_line_number: usize = 1;
250+
// Keep the last `CONTEXT` lines while iterating over the file lines.
251+
let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256));
252+
let mut line = String::with_capacity(256);
233253

234-
if !re.is_match(&source) {
235-
return State::Done;
236-
}
254+
loop {
255+
let n = read_line(&mut line).unwrap_or_else(|e| {
256+
println!(
257+
"Failed to read the exercise file {}: {e}",
258+
self.path.display(),
259+
);
260+
exit(1);
261+
});
237262

238-
let matched_line_index = source
239-
.lines()
240-
.enumerate()
241-
.find_map(|(i, line)| if re.is_match(line) { Some(i) } else { None })
242-
.expect("This should not happen at all");
243-
244-
let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize;
245-
let max_line = matched_line_index + CONTEXT;
246-
247-
let context = source
248-
.lines()
249-
.enumerate()
250-
.filter(|&(i, _)| i >= min_line && i <= max_line)
251-
.map(|(i, line)| ContextLine {
252-
line: line.to_string(),
253-
number: i + 1,
254-
important: i == matched_line_index,
255-
})
256-
.collect();
263+
// Reached the end of the file and didn't find the comment.
264+
if n == 0 {
265+
return State::Done;
266+
}
267+
268+
if contains_not_done_comment(&line) {
269+
let mut context = Vec::with_capacity(2 * CONTEXT + 1);
270+
// Previous lines.
271+
for (ind, prev_line) in prev_lines
272+
.into_iter()
273+
.take(current_line_number - 1)
274+
.enumerate()
275+
.rev()
276+
{
277+
context.push(ContextLine {
278+
line: prev_line,
279+
number: current_line_number - 1 - ind,
280+
important: false,
281+
});
282+
}
283+
284+
// Current line.
285+
context.push(ContextLine {
286+
line,
287+
number: current_line_number,
288+
important: true,
289+
});
290+
291+
// Next lines.
292+
for ind in 0..CONTEXT {
293+
let mut next_line = String::with_capacity(256);
294+
let Ok(n) = read_line(&mut next_line) else {
295+
// If an error occurs, just ignore the next lines.
296+
break;
297+
};
298+
299+
// Reached the end of the file.
300+
if n == 0 {
301+
break;
302+
}
303+
304+
context.push(ContextLine {
305+
line: next_line,
306+
number: current_line_number + 1 + ind,
307+
important: false,
308+
});
309+
}
310+
311+
return State::Pending(context);
312+
}
257313

258-
State::Pending(context)
314+
current_line_number += 1;
315+
// Add the current line as a previous line and shift the older lines by one.
316+
for prev_line in &mut prev_lines {
317+
mem::swap(&mut line, prev_line);
318+
}
319+
// The current line now contains the oldest previous line.
320+
// Recycle it for reading the next line.
321+
line.clear();
322+
}
259323
}
260324

261325
// Check that the exercise looks to be solved using self.state()
@@ -381,4 +445,20 @@ mod test {
381445
let out = exercise.compile().unwrap().run().unwrap();
382446
assert!(out.stdout.contains("THIS TEST TOO SHALL PASS"));
383447
}
448+
449+
#[test]
450+
fn test_not_done() {
451+
assert!(contains_not_done_comment("// I AM NOT DONE"));
452+
assert!(contains_not_done_comment("/// I AM NOT DONE"));
453+
assert!(contains_not_done_comment("// I AM NOT DONE"));
454+
assert!(contains_not_done_comment("/// I AM NOT DONE"));
455+
assert!(contains_not_done_comment("// I AM NOT DONE "));
456+
assert!(contains_not_done_comment("// I AM NOT DONE!"));
457+
assert!(contains_not_done_comment("// I am not done"));
458+
assert!(contains_not_done_comment("// i am NOT done"));
459+
460+
assert!(!contains_not_done_comment("I AM NOT DONE"));
461+
assert!(!contains_not_done_comment("// NOT DONE"));
462+
assert!(!contains_not_done_comment("DONE"));
463+
}
384464
}

src/main.rs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -130,31 +130,43 @@ fn main() {
130130
println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
131131
}
132132
let mut exercises_done: u16 = 0;
133-
let filters = filter.clone().unwrap_or_default().to_lowercase();
134-
exercises.iter().for_each(|e| {
135-
let fname = format!("{}", e.path.display());
133+
let lowercase_filter = filter
134+
.as_ref()
135+
.map(|s| s.to_lowercase())
136+
.unwrap_or_default();
137+
let filters = lowercase_filter
138+
.split(',')
139+
.filter_map(|f| {
140+
let f = f.trim();
141+
if f.is_empty() {
142+
None
143+
} else {
144+
Some(f)
145+
}
146+
})
147+
.collect::<Vec<_>>();
148+
149+
for exercise in &exercises {
150+
let fname = exercise.path.to_string_lossy();
136151
let filter_cond = filters
137-
.split(',')
138-
.filter(|f| !f.trim().is_empty())
139-
.any(|f| e.name.contains(f) || fname.contains(f));
140-
let status = if e.looks_done() {
152+
.iter()
153+
.any(|f| exercise.name.contains(f) || fname.contains(f));
154+
let looks_done = exercise.looks_done();
155+
let status = if looks_done {
141156
exercises_done += 1;
142157
"Done"
143158
} else {
144159
"Pending"
145160
};
146-
let solve_cond = {
147-
(e.looks_done() && solved)
148-
|| (!e.looks_done() && unsolved)
149-
|| (!solved && !unsolved)
150-
};
161+
let solve_cond =
162+
(looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved);
151163
if solve_cond && (filter_cond || filter.is_none()) {
152164
let line = if paths {
153165
format!("{fname}\n")
154166
} else if names {
155-
format!("{}\n", e.name)
167+
format!("{}\n", exercise.name)
156168
} else {
157-
format!("{:<17}\t{fname:<46}\t{status:<7}\n", e.name)
169+
format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name)
158170
};
159171
// Somehow using println! leads to the binary panicking
160172
// when its output is piped.
@@ -170,7 +182,8 @@ fn main() {
170182
});
171183
}
172184
}
173-
});
185+
}
186+
174187
let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0;
175188
println!(
176189
"Progress: You completed {} / {} exercises ({:.1} %).",

src/verify.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ fn prompt_for_completion(
223223
let formatted_line = if context_line.important {
224224
format!("{}", style(context_line.line).bold())
225225
} else {
226-
context_line.line.to_string()
226+
context_line.line
227227
};
228228

229229
println!(

0 commit comments

Comments
 (0)