Skip to content

Commit b902f1f

Browse files
committed
Support tail calls in the interpreter
1 parent e3cd2de commit b902f1f

12 files changed

+316
-1
lines changed

compiler/rustc_const_eval/src/interpret/terminator.rs

+66-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,72 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
115115
}
116116
}
117117

118-
TailCall { func: _, args: _, fn_span: _ } => todo!(),
118+
TailCall { ref func, ref args, fn_span: _ } => {
119+
// FIXME(explicit_tail_calls): a lot of code here is duplicated with normal calls, can we refactor this?
120+
let old_frame_idx = self.frame_idx();
121+
let func = self.eval_operand(func, None)?;
122+
let args = self.eval_operands(args)?;
123+
124+
let fn_sig_binder = func.layout.ty.fn_sig(*self.tcx);
125+
let fn_sig =
126+
self.tcx.normalize_erasing_late_bound_regions(self.param_env, fn_sig_binder);
127+
let extra_args = &args[fn_sig.inputs().len()..];
128+
let extra_args =
129+
self.tcx.mk_type_list_from_iter(extra_args.iter().map(|arg| arg.layout.ty));
130+
131+
let (fn_val, fn_abi, with_caller_location) = match *func.layout.ty.kind() {
132+
ty::FnPtr(_sig) => {
133+
let fn_ptr = self.read_pointer(&func)?;
134+
let fn_val = self.get_ptr_fn(fn_ptr)?;
135+
(fn_val, self.fn_abi_of_fn_ptr(fn_sig_binder, extra_args)?, false)
136+
}
137+
ty::FnDef(def_id, substs) => {
138+
let instance = self.resolve(def_id, substs)?;
139+
(
140+
FnVal::Instance(instance),
141+
self.fn_abi_of_instance(instance, extra_args)?,
142+
instance.def.requires_caller_location(*self.tcx),
143+
)
144+
}
145+
_ => span_bug!(
146+
terminator.source_info.span,
147+
"invalid callee of type {:?}",
148+
func.layout.ty
149+
),
150+
};
151+
152+
// This is the "canonical" implementation of tails calls,
153+
// a pop of the current stack frame, followed by a normal call
154+
// which pushes a new stack frame, with the return address from
155+
// the popped stack frame.
156+
//
157+
// Note that we can't use `pop_stack_frame` as it "executes"
158+
// the goto to the return block, but we don't want to,
159+
// only the tail called function should return to the current
160+
// return block.
161+
let Some(prev_frame) = self.stack_mut().pop()
162+
else { span_bug!(terminator.source_info.span, "empty stack while evaluating this tail call") };
163+
164+
let StackPopCleanup::Goto { ret, unwind } = prev_frame.return_to_block
165+
else { span_bug!(terminator.source_info.span, "tail call with the root stack frame") };
166+
167+
self.eval_fn_call(
168+
fn_val,
169+
(fn_sig.abi, fn_abi),
170+
&args,
171+
with_caller_location,
172+
&prev_frame.return_place,
173+
ret,
174+
unwind,
175+
)?;
176+
177+
if self.frame_idx() != old_frame_idx {
178+
span_bug!(
179+
terminator.source_info.span,
180+
"evaluating this tail call pushed a new stack frame"
181+
);
182+
}
183+
}
119184

120185
Drop { place, target, unwind, replace: _ } => {
121186
let frame = self.frame();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#![allow(incomplete_features)]
2+
#![feature(explicit_tail_calls)]
3+
4+
pub const fn test(_: &Type) {
5+
const fn takes_borrow(_: &Type) {}
6+
7+
let local = Type;
8+
become takes_borrow(&local);
9+
//~^ error: `local` does not live long enough
10+
}
11+
12+
struct Type;
13+
14+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error[E0597]: `local` does not live long enough
2+
--> $DIR/ctfe-arg-bad-borrow.rs:8:25
3+
|
4+
LL | let local = Type;
5+
| ----- binding `local` declared here
6+
LL | become takes_borrow(&local);
7+
| ^^^^^^ borrowed value does not live long enough
8+
LL |
9+
LL | }
10+
| - `local` dropped here while still borrowed
11+
12+
error: aborting due to previous error
13+
14+
For more information about this error, try `rustc --explain E0597`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// check-pass
2+
#![allow(incomplete_features)]
3+
#![feature(explicit_tail_calls)]
4+
5+
pub const fn test(x: &Type) {
6+
const fn takes_borrow(_: &Type) {}
7+
8+
become takes_borrow(x);
9+
}
10+
11+
pub struct Type;
12+
13+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// check-pass
2+
#![allow(incomplete_features)]
3+
#![feature(explicit_tail_calls)]
4+
5+
pub const fn test(s: String) -> String {
6+
const fn takes_string(s: String) -> String { s }
7+
8+
become takes_string(s);
9+
}
10+
11+
struct Type;
12+
13+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// run-pass
2+
#![allow(incomplete_features)]
3+
#![feature(explicit_tail_calls)]
4+
5+
/// A very unnecessarily complicated "implementation" of the callatz conjecture.
6+
/// Returns the number of steps to reach `1`.
7+
///
8+
/// This is just a test for tail calls, which involves multiple functions calling each other.
9+
///
10+
/// Panics if `x == 0`.
11+
const fn collatz(x: u32) -> u32 {
12+
assert!(x > 0);
13+
14+
const fn switch(x: u32, steps: u32) -> u32 {
15+
match x {
16+
1 => steps,
17+
_ if x & 1 == 0 => become div2(x, steps + 1),
18+
_ => become mul3plus1(x, steps + 1),
19+
}
20+
}
21+
22+
const fn div2(x: u32, steps: u32) -> u32 {
23+
become switch(x >> 1, steps)
24+
}
25+
26+
const fn mul3plus1(x: u32, steps: u32) -> u32 {
27+
become switch(3*x + 1, steps)
28+
}
29+
30+
switch(x, 0)
31+
}
32+
33+
const ASSERTS: () = {
34+
assert!(collatz(1) == 0);
35+
assert!(collatz(2) == 1);
36+
assert!(collatz(3) == 7);
37+
assert!(collatz(4) == 2);
38+
assert!(collatz(6171) == 261);
39+
};
40+
41+
fn main() {
42+
let _ = ASSERTS;
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
error[E0080]: evaluation of constant value failed
2+
--> $DIR/ctfe-id-unlimited.rs:17:42
3+
|
4+
LL | #[cfg(r#return)] _ => return inner(acc + 1, n - 1),
5+
| ^^^^^^^^^^^^^^^^^^^^^ reached the configured maximum number of stack frames
6+
|
7+
note: inside `inner`
8+
--> $DIR/ctfe-id-unlimited.rs:17:42
9+
|
10+
LL | #[cfg(r#return)] _ => return inner(acc + 1, n - 1),
11+
| ^^^^^^^^^^^^^^^^^^^^^
12+
note: [... 125 additional calls inside `inner` ...]
13+
--> $DIR/ctfe-id-unlimited.rs:17:42
14+
|
15+
LL | #[cfg(r#return)] _ => return inner(acc + 1, n - 1),
16+
| ^^^^^^^^^^^^^^^^^^^^^
17+
note: inside `rec_id`
18+
--> $DIR/ctfe-id-unlimited.rs:22:5
19+
|
20+
LL | inner(0, n)
21+
| ^^^^^^^^^^^
22+
note: inside `ID_ED`
23+
--> $DIR/ctfe-id-unlimited.rs:29:20
24+
|
25+
LL | const ID_ED: u32 = rec_id(ORIGINAL);
26+
| ^^^^^^^^^^^^^^^^
27+
28+
note: erroneous constant used
29+
--> $DIR/ctfe-id-unlimited.rs:31:40
30+
|
31+
LL | const ASSERT: () = assert!(ORIGINAL == ID_ED);
32+
| ^^^^^
33+
34+
error: aborting due to previous error
35+
36+
For more information about this error, try `rustc --explain E0080`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// revisions: become return
2+
// [become] run-pass
3+
#![allow(incomplete_features)]
4+
#![feature(explicit_tail_calls)]
5+
6+
// This is an identity function (`|x| x`), but implemented using recursion.
7+
// Each step we increment accumulator and decrement the number.
8+
//
9+
// With normal calls this fails compilation because of the recursion limit,
10+
// but with tail calls/`become` we don't grow the stack/spend recursion limit
11+
// so this should compile.
12+
const fn rec_id(n: u32) -> u32 {
13+
const fn inner(acc: u32, n: u32) -> u32 {
14+
match n {
15+
0 => acc,
16+
#[cfg(r#become)] _ => become inner(acc + 1, n - 1),
17+
#[cfg(r#return)] _ => return inner(acc + 1, n - 1),
18+
//[return]~^ error: evaluation of constant value failed
19+
}
20+
}
21+
22+
inner(0, n)
23+
}
24+
25+
// Some relatively big number that is higher than recursion limit
26+
const ORIGINAL: u32 = 12345;
27+
// Original number, but with identity function applied
28+
// (this is the same, but requires execution of the recursion)
29+
const ID_ED: u32 = rec_id(ORIGINAL);
30+
// Assert to make absolutely sure the computation actually happens
31+
const ASSERT: () = assert!(ORIGINAL == ID_ED);
32+
33+
fn main() {
34+
let _ = ASSERT;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![allow(incomplete_features)]
2+
#![feature(explicit_tail_calls)]
3+
4+
pub const fn f() {
5+
become g();
6+
}
7+
8+
const fn g() {
9+
panic!()
10+
//~^ error: evaluation of constant value failed
11+
//~| note: in this expansion of panic!
12+
//~| note: inside `g`
13+
//~| note: in this expansion of panic!
14+
}
15+
16+
const _: () = f();
17+
//~^ note: inside `_`
18+
19+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
error[E0080]: evaluation of constant value failed
2+
--> $DIR/ctfe-tail-call-panic.rs:9:5
3+
|
4+
LL | panic!()
5+
| ^^^^^^^^ the evaluated program panicked at 'explicit panic', $DIR/ctfe-tail-call-panic.rs:9:5
6+
|
7+
note: inside `g`
8+
--> $DIR/ctfe-tail-call-panic.rs:9:5
9+
|
10+
LL | panic!()
11+
| ^^^^^^^^
12+
note: inside `_`
13+
--> $DIR/ctfe-tail-call-panic.rs:16:15
14+
|
15+
LL | const _: () = f();
16+
| ^^^
17+
= note: this error originates in the macro `$crate::panic::panic_2015` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info)
18+
19+
error: aborting due to previous error
20+
21+
For more information about this error, try `rustc --explain E0080`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#![allow(incomplete_features)]
2+
#![feature(explicit_tail_calls, const_trait_impl, const_mut_refs)]
3+
4+
pub const fn test(_: &View) {
5+
const fn takes_view(_: &View) {}
6+
7+
become takes_view(HasDrop.as_view());
8+
//~^ error: temporary value dropped while borrowed
9+
}
10+
11+
struct HasDrop;
12+
struct View;
13+
14+
impl HasDrop {
15+
const fn as_view(&self) -> &View {
16+
&View
17+
}
18+
}
19+
20+
impl const Drop for HasDrop {
21+
fn drop(&mut self) {}
22+
}
23+
24+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
error[E0716]: temporary value dropped while borrowed
2+
--> $DIR/ctfe-tmp-arg-drop.rs:7:23
3+
|
4+
LL | become takes_view(HasDrop.as_view());
5+
| ------------------^^^^^^^------------ temporary value is freed at the end of this statement
6+
| | |
7+
| | creates a temporary value which is freed while still in use
8+
| borrow later used here
9+
|
10+
help: consider using a `let` binding to create a longer lived value
11+
|
12+
LL ~ let binding = HasDrop;
13+
LL ~ become takes_view(binding.as_view());
14+
|
15+
16+
error: aborting due to previous error
17+
18+
For more information about this error, try `rustc --explain E0716`.

0 commit comments

Comments
 (0)