Skip to content

Commit 53a658c

Browse files
authored
Fail on deep recursion instead of overflowing stack (#3319)
1 parent bcf29d2 commit 53a658c

5 files changed

Lines changed: 43 additions & 1 deletion

File tree

src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ pub(crate) enum Error<'src> {
173173
min: usize,
174174
max: usize,
175175
},
176+
RecursionLimit {
177+
last: Name<'src>,
178+
},
176179
RegexCompile {
177180
source: regex::Error,
178181
},
@@ -721,6 +724,10 @@ impl ColorDisplay for Error<'_> {
721724
)?;
722725
}
723726
}
727+
RecursionLimit { last } => write!(
728+
f,
729+
"maximum recursion depth of {RECURSION_LIMIT} exceeded while calling function {last}"
730+
)?,
724731
RegexCompile { source } => write!(f, "{source}")?,
725732
RuntimeDirIo { io_error, path } => {
726733
write!(

src/evaluator.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub(crate) struct Evaluator<'src: 'run, 'run> {
77
is_dependency: bool,
88
non_const_assignments: Table<'src, Name<'src>>,
99
overrides: &'run HashMap<Number, String>,
10+
recursion_depth: usize,
1011
scope: Scope<'src, 'run>,
1112
}
1213

@@ -26,6 +27,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
2627
) -> RunResult<'src, Settings> {
2728
let mut evaluator = Self {
2829
assignments: Some(assignments),
30+
recursion_depth: 0,
2931
context: None,
3032
env: BTreeMap::new(),
3133
is_dependency: false,
@@ -176,6 +178,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
176178

177179
let mut evaluator = Self {
178180
assignments: Some(&module.assignments),
181+
recursion_depth: 0,
179182
context: Some(context),
180183
env: BTreeMap::new(),
181184
is_dependency: false,
@@ -240,6 +243,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
240243
function: &FunctionDefinition<'src>,
241244
arguments: &[Expression<'src>],
242245
) -> RunResult<'src, String> {
246+
let recursion_depth = self.recursion_depth + 1;
247+
248+
if recursion_depth == RECURSION_LIMIT {
249+
return Err(Error::RecursionLimit {
250+
last: function.name,
251+
});
252+
}
253+
243254
let context = *self.context.as_ref().unwrap();
244255

245256
let mut scope = Scope::root();
@@ -264,6 +275,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
264275
is_dependency: false,
265276
non_const_assignments: Table::new(),
266277
overrides: self.overrides,
278+
recursion_depth,
267279
scope,
268280
};
269281

@@ -614,6 +626,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
614626
is_dependency,
615627
non_const_assignments: Table::new(),
616628
overrides: context.overrides,
629+
recursion_depth: 0,
617630
scope: scope.child(),
618631
}
619632
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ type FunctionResult = Result<String, String>;
171171
type RunResult<'a, T = ()> = Result<T, Error<'a>>;
172172
type SearchResult<T> = Result<T, SearchError>;
173173

174+
const RECURSION_LIMIT: usize = if cfg!(windows) { 48 } else { 256 };
175+
174176
#[cfg(test)]
175177
#[macro_use]
176178
pub mod testing;

src/parser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ impl<'run, 'src> Parser<'run, 'src> {
653653

654654
/// Parse an expression, e.g. `1 + 2`
655655
fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> {
656-
if self.recursion_depth == if cfg!(windows) { 48 } else { 256 } {
656+
if self.recursion_depth == RECURSION_LIMIT {
657657
let token = self.next()?;
658658
return Err(CompileError::new(
659659
token,

tests/recursion_limit.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ fn bugfix() {
1212
.failure();
1313
}
1414

15+
#[test]
16+
fn user_defined_function_recursion_limit() {
17+
Test::new()
18+
.justfile(
19+
"
20+
set unstable
21+
22+
foo() := foo()
23+
24+
bar:
25+
echo {{foo()}}
26+
",
27+
)
28+
.stderr(format!(
29+
"error: maximum recursion depth of {} exceeded while calling function foo\n",
30+
if cfg!(windows) { 48 } else { 256 },
31+
))
32+
.failure();
33+
}
34+
1535
const RECURSION_LIMIT_REACHED: &str = if cfg!(windows) {
1636
"
1737
error: parsing recursion depth exceeded

0 commit comments

Comments
 (0)