-
Notifications
You must be signed in to change notification settings - Fork 13k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor the way interpreters handle special functions #121273
Conversation
Some changes occurred to the CTFE / Miri engine cc @rust-lang/miri Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt The Miri subtree was changed cc @rust-lang/miri |
This comment has been minimized.
This comment has been minimized.
By reusing the `ExtraFnVal` type. This changes `find_mir_or_eval_fn` into `find_mir_or_extra_fn`, which helps structure code a little bit better. Having separate "find info for evaluation" and "evaluate" steps is nice on its own, but it's also required for tail calls.
Stack overflow happens on this example: #![feature(const_align_offset)]
const _: usize = (1 as *const ()).align_offset(2);
fn main() {} With the following infinite recursion: I'm not exactly sure how to resolve this... When handling I can add a |
0bd2727
to
586d704
Compare
@@ -191,22 +192,17 @@ pub trait Machine<'mir, 'tcx: 'mir>: Sized { | |||
/// nor just jump to `ret`, but instead push their own stack frame.) | |||
/// Passing `dest`and `ret` in the same `Option` proved very annoying when only one of them | |||
/// was used. | |||
fn find_mir_or_eval_fn( | |||
fn find_mir_or_extra_fn( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation of this function needs an update
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed it up, should be better now.
Athough it looks like there are other tests and comments mentioning __rust_maybe_catch_panic
, which doesn't seem to exist anymore?... Possibly someone needs to go over them and update them too.
@bors r=oli-obk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! I still have some concerns though. @bors r-
Having separate "find info for evaluation" and "evaluate" steps is nice on its own,
I buy this in the abstract, but looking at the PR I am not entirely sold:
- You had to expose a fairly low-level function (
eval_fn_call
) as fullypub
now, and add an even-lower-level helper (eval_body
) aspub(crate)
, indicating maybe the new structure is worse than the old one in terms of encapsulation. - The phase separation doesn't seem to be entirely working, given that in a bunch of cases
call_extra_fn
has to push a stack frame with MIR in it. So now we have two different code paths that do something similar:find_mir_or_extra_fn
returns MIR, or it returns anExtraFnVal
and thencall_extra_fn
gets the MIR and has to deal with it by itself. That's definitely worse than before. Is there a way to avoid this redundancy?
Maybe explain on a high level the problem you are trying to solve. We should look for a better design that solves your problem while avoiding introducing these issues.
// const eval extra fn which ends up here, so calling `eval_fn_call` | ||
// would cause infinite recursion and stack overflow. | ||
let body = ecx.load_mir(instance.def, None)?; | ||
ecx.eval_body( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having to introduce and expose a new very low-level function here, would it work to have find_mir_or_extra_fn
make the choice of whether to call the real function or the interpreter shim? Fundamentally the problem seems to be that we first said we want an ExtraFn but then later we decided we want MIR anyway.
OTOH, Miri also has that situation, I wonder how this is handled there.
pub enum ExtraFnVal { | ||
ForeignFn { link_name: Symbol }, | ||
DynSym(DynSym), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of these represent function pointers to a named global symbol. Why do we need to distinguish these variants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know. They were handled differently before and I just kept the same difference — DynSym
has an assert that makes sure that we actually used a shim, and not an exported symbol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before, this type had a clear meaning: it represented "things a function pointer can point to". The interpreter natively supports function pointers that point to an Instance
, but for dlsym
support we needed function pointers that point to something else, and that's what this extra-fn stuff is about.
But with your change, now there can be a function pointer that points to DynSym("functionname")
, and a function pointer that points to ForeignFn { link_name: "functionname" }
, and you made those be different things. That's not pre-existing, that's new in this PR. And I don't see why we need these two kinds of function pointers. The assert indicates that either these should be two different types, or one type that's just a Symbol
like DynSym
before. But I can't see why it would be a good idea to make this a sum type with two variants that conceptually represent the same thing.
Using ExtraFnVal as return type in find_mir_or_extra_fn
makes some sense, because it matches the same dichotomy: either a native Rust function (Instance
/MIR body), or "something else". Probably things could be cleaned up a bit by actually using FnVal<ExtraFnVal>
as return type, then the function pointer ("indirect call"/Ty::FnPtr
) codepath and the direct call (Ty::FnDef
) code path can be even more unified.
But that is something we can do later, currently there are bigger problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not pre-existing
I'm pretty sure it is. DynSym
s are handled in emulate_dyn_sym
here:
rust/src/tools/miri/src/shims/foreign_items.rs
Lines 156 to 157 in 0395fa3
let res = self.emulate_foreign_item(sym.0, abi, args, dest, ret, unwind)?; | |
assert!(res.is_none(), "DynSyms that delegate are not supported"); |
While usual foreign function are handled in a branch in find_mir_or_eval_fn
here:
rust/src/tools/miri/src/machine.rs
Line 960 in 0395fa3
return ecx.emulate_foreign_item(link_name, abi, &args, dest, ret, unwind); |
Those are slightly distinct (see the assert) even though they result in basically the same emulate_foreign_item
call.
I have nothing against having only a single type, but I'm not sure how to keep this assert then...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure it is. DynSyms are handled in emulate_dyn_sym here:
While usual foreign function are handled in a branch in find_mir_or_eval_fn here:
Yes, but that's not what I was talking about. We just had two separate codepaths for handling calls to Rust items vs dynamically loaded items. The "obvious" thing for you to do would be having two different types to represent this. You decided to unify these types, which I can get behind (in fact I think it's a great idea), but it's a bad idea to unify them by introducing redundancy in the value type of the interpreter.
Here's what I said: there were before not two ways to represent a function pointer. You then pointed at two functions doing related things. That's a category error as I was talking about representation! So any counterargument can only point at data in the interpreter, not interpreter functions.
Those are slightly distinct (see the assert) even though they result in basically the same emulate_foreign_item call.
Yeah, the dynsym handling doesn't currently support calling a MIR-implemented function. We simply didn't need this so far. It may be that with your patch, supporting this becomes trivial, so then there's no more reason for the assert.
| "ExitProcess" | ||
=> { | ||
let exp_abi = if link_name.as_str() == "exit" { | ||
Abi::C { unwind: false } | ||
} else { | ||
Abi::System { unwind: false } | ||
}; | ||
let [code] = this.check_shim(abi, exp_abi, link_name, args)?; | ||
let args = this.copy_fn_args(args)?; // FIXME: Should `InPlace` arguments be reset to uninit? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this duplicated so often now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because there is a code path which needs &[FnArg]
, but most need a &[OpTy]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's now more of these asserts than before though, I think? So what changed?
@@ -497,15 +498,15 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> { | |||
/// `with_caller_location` indicates whether the caller passed a caller location. Miri | |||
/// implements caller locations without argument passing, but to match `FnAbi` we need to know | |||
/// when those arguments are present. | |||
pub(crate) fn eval_fn_call( | |||
pub fn eval_fn_call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not happy with making this more public, this is a rather low-level function. Can this be avoided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can probably be avoided with the return Result<Option<Instance>>
scheme.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like returning Option<Instance>
is the same level of awkwardness...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR has the awkwardness of attempting to split "find the fn" from "call the fn", but then in quite a few cases "call the fn" ends up finding MIR elsewhere, completely breaking the structure.
If it is okay for call_extra_fn
to call eval_fn_call
, then we're just back to where we were before but with more messy code, I think? I don't see how you gained anything.
@@ -885,6 +709,198 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> { | |||
} | |||
} | |||
|
|||
pub(crate) fn eval_body( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a doc comment.
@bors r- |
@RalfJung the problem I'm trying to solve is needing to pass arguments to
I don't think so, no. This won't work because Although I guess we could implement both paths in a shim?... The problem is only the complexity of But other than that, I think all other cases can decide based on just the instance.
Yes, that's the awkward thing. It both causes my original problem (evaluating special function requires arguments, but getting mir shouldn't) and the awkwardness of this PR (everything calling into each other, Footnotes
|
Although. Mh. It looks like naive implementation of ( |
|
Ah okay so that's why you can't have For But the problem isn't solved then, Miri also has some cases where it wants to run a MIR body:
|
Makes sense, sure.
I think all of those are solved by allowing
|
How is In fact it gets even worse -- Miri has support for loading a .so file and dynamically using functions from that .so file to implement foreign functions. So knowing whether we support a function involves checking whether the .so file contains a function of the given name. |
Looking at the current align_offset handling (in master) -- woah that is messy.^^ Miri foreign item handling also has to sometimes push a function, which is similar to what And neither of these seem great, they're both accessing pretty low-level interpreter functionality. If I were to refactor this now I'd try to extract the common code between That doesn't achieve your desired split of "find fn" and "call fn" though, which I still need to understand better. |
Hm... but normal calls also already have that issue, don't they? We evaluate arguments after pushing the stack frame. And it does work fine. What changes when considering tail calls?
|
For intrinsics with fallback bodies we will need a similar scheme soon (fall back to a different instance than the one invoked) |
I don't understand the state of the discussion here anymore. We can figure out how to untangle align_offset, but that seems orthogonal to this PR IMO the PR I approved was a good refactoring. We could make it more codegen-like by not calling the bodies directly, but by bubbling the new instance outwards towards the regular call logic. But that's not strictly necessary |
IMO this PR is a strict decrease in code organization and maintainability. Currently we have a single clear entrypoint for "what happens on a fn call": With this PR we have one function ( Also if it is somehow necessary to find the MIR in I can think of various ways to clean this up, since I agree that improvements are possible here. I've mentioned some of them above. But I don't think this PR is an improvement. And I don't understand how tail calls factor in so I don't know which of the refactorings I have in my mind are prepared for tail calls. Without taking into account tail calls, the first refactor I would try is to notice that What I don't see is why splitting this new version of |
(I've kept editing the post above, please read on Github of you're usually just reading the email notifications.) |
☔ The latest upstream changes (presumably #121655) made this pull request unmergeable. Please resolve the merge conflicts. |
I think what I had in mind is that if Still, I agree that this PR is not a clear improvement. Let me try to explain the problem I'm trying to solve. So the problem is here ( rust/compiler/rustc_const_eval/src/interpret/terminator.rs Lines 170 to 189 in fb3375c
(this will be refactored into a This currently is completely broken.
The problem is that there is a circular dependency — My idea (granted, not fully implemented here) was to break the " LMK if something in this explanation is not clear. |
Uh, I'm not sure that will work. We have an optimized representation for local variables that never have their address taken: we store them not in an I once tried to get rid of this index and make it implicitly always the "current" stack frame, but it turns out when it comes to return values we actually do use cross-stack-frame indexing here: the callee stores the place in the caller that the return value should be put in, and that may be such an optimized place. We could make sure the return place is always a "proper" place in memory, and that would probably not cost a lot of performance. (The long-lived But anyway, taking a step back... in an idealized interpreter, we would evaluate all the arguments to values, then pop the stack frame (deallocating the backing memory for all its locals), then push the new one. In the rustc interpreter this does not directly work, since an evaluated argument ( So in other words:
|
Ah, it's slightly more complicated since evaluated arguments are not But what even are the semantics of in-place argument passing with tail calls? IOW, what does |
I'm not exactly sure, since I don't even know what precisely is From the backend perspective I think it means "move fn a(s: Struct) {
become b(String::new());
}
fn b(s: String) {} ==> fn a(s_ptr: *mut Struct) {
// evaluation of the arguments
let arg0 = String::new();
// drops of all the locals
drop_in_place(s_ptr);
// move of arguments to the appropriate place
ptr::write(s_ptr, arg0);
// reset everything to be as if the function is just called... somehow? idk
// jump to b
jump b;
}
fn b(s_ptr: *mut String) { ... } Note that we can always do that, because tail calls (as currently designed) always require signatures/abis to match. How to represent this in the Rust AM? I'm not sure... Somehow the arguments need to move from the replaced frame to the new frame (which is done in the backend by just reusing the same frame). |
It means that the compiler is allowed but not required to make the callee directly use the caller memory for this place. In the AM, what we do is we create a copy as usual but we also mark the caller memory as "inaccessible" so any attempt to observe or overwrite it causes UB, meaning the compiler can then optimize to reuse that memory since it can't be used for anything else. But with tail calls, the caller memory is deallocated before the callee even starts. So I think it just makes no sense to even have Well I guess it makes sense in your example because the actual location of the argument is even further up the stack, so it doesn't go away with the stack frame. Hm... I guess in principle we could change how arguments are evaluated to |
Yeah. The idea is that due to how tail call restrictions are currently done1 you can always find a stack frame where everything can live, because there was some original normal call to a function with the same signature, which had to pass arguments of the same types.
I have little idea what all of this means but 👍🏻 ahaha. Footnotes
|
Hm, interesting. But in the model I don't think we want to require that if a call was "move" then all tail calls it makes have to be "move" as well, so we can't exploit that. I would be curious about which ICEs you ran into. The code you posted already does |
@RalfJung so.. I was a bit misremembering and I'm getting errors, not ICEs: #![allow(incomplete_features, dead_code)]
#![feature(explicit_tail_calls)]
const fn f(x: u32) {
become f(x);
}
const _: () = f(0);
And looking at the mir it does contain a // MIR for `f` after built
fn f(_1: u32) -> () {
debug x => _1;
let mut _0: ();
let mut _2: !;
let mut _3: u32;
bb0: {
StorageLive(_3);
_3 = _1;
tailcall f(Spanned { node: move _3, span: ./t.rs:5:14: 5:15 (#0) });
}
bb1: {
StorageDead(_3);
unreachable;
}
bb2: {
return;
}
} |
Aha, found how to make an example without #![allow(incomplete_features, dead_code)]
#![feature(explicit_tail_calls)]
const fn f(_: u32) {
become f(1);
}
const _: () = f(1);
|
Okay so I think the first order of business would be to get rid of the That would require changes in if let Either::Left(mplace) = place.as_mplace_or_local() {
FnArg::InPlace(mplace)
} else {
// This argument doesn't live in memory, so there's no place
// to make inaccessible during the call.
// This is also crucial for tail calls, where we want the `FnArg` to
// stay valid when the old stack frame gets popped.
FnArg::Copy(self.place_to_op(place)?)
} |
Tweak the way we protect in-place function arguments in interpreters Use `MPlaceTy` instead of `PlaceTy` in `FnArg` and ignore (copy) locals in an earlier step ("Locals that don't have their address taken are as protected as they can ever be"). This seems to be crucial for tail call support (as they can't refer to caller's locals which are killed when replacing the stack frame). r? `@RalfJung` cc `@oli-obk` see rust-lang#121273 (comment)
Rollup merge of rust-lang#122076 - WaffleLapkin:mplace-args, r=RalfJung Tweak the way we protect in-place function arguments in interpreters Use `MPlaceTy` instead of `PlaceTy` in `FnArg` and ignore (copy) locals in an earlier step ("Locals that don't have their address taken are as protected as they can ever be"). This seems to be crucial for tail call support (as they can't refer to caller's locals which are killed when replacing the stack frame). r? `@RalfJung` cc `@oli-obk` see rust-lang#121273 (comment)
Tweak the way we protect in-place function arguments in interpreters Use `MPlaceTy` instead of `PlaceTy` in `FnArg` and ignore (copy) locals in an earlier step ("Locals that don't have their address taken are as protected as they can ever be"). This seems to be crucial for tail call support (as they can't refer to caller's locals which are killed when replacing the stack frame). r? `@RalfJung` cc `@oli-obk` see rust-lang/rust#121273 (comment)
... by reusing the
ExtraFnVal
type.This changes
find_mir_or_eval_fn
intofind_mir_or_extra_fn
, which helps structure code a little bit better. Having separate "find info for evaluation" and "evaluate" steps is nice on its own, but it's also required for tail calls.(this is needed for #113128, as otherwise correctly implementing tail calls in the interpreter creates a dependency loop)
r? @oli-obk