Add stable MIR Projections support based on MIR structure

This commit includes richer projections for both Places and
UserTypeProjections. However, the tests only touch on Places. There are
also outstanding TODOs regarding how projections should be resolved to
produce Place types, and regarding if UserTypeProjections should just
contain ProjectionElem<(),()> objects as in MIR.
This commit is contained in:
Kirby Linvill 2023-11-02 09:05:35 -06:00
parent 0f44eb32f1
commit b1585983cc
No known key found for this signature in database
GPG Key ID: E304CE3F028E6E3F
3 changed files with 283 additions and 6 deletions

View File

@ -682,10 +682,39 @@ impl<'tcx> Stable<'tcx> for mir::ConstOperand<'tcx> {
impl<'tcx> Stable<'tcx> for mir::Place<'tcx> {
type T = stable_mir::mir::Place;
fn stable(&self, _: &mut Tables<'tcx>) -> Self::T {
fn stable(&self, tables: &mut Tables<'tcx>) -> Self::T {
stable_mir::mir::Place {
local: self.local.as_usize(),
projection: format!("{:?}", self.projection),
projection: self.projection.iter().map(|e| e.stable(tables)).collect(),
}
}
}
impl<'tcx> Stable<'tcx> for mir::PlaceElem<'tcx> {
type T = stable_mir::mir::ProjectionElem<stable_mir::mir::Local, stable_mir::ty::Ty>;
fn stable(&self, tables: &mut Tables<'tcx>) -> Self::T {
use mir::ProjectionElem::*;
match self {
Deref => stable_mir::mir::ProjectionElem::Deref,
Field(idx, ty) => {
stable_mir::mir::ProjectionElem::Field(idx.stable(tables), ty.stable(tables))
}
Index(local) => stable_mir::mir::ProjectionElem::Index(local.stable(tables)),
ConstantIndex { offset, min_length, from_end } => {
stable_mir::mir::ProjectionElem::ConstantIndex {
offset: *offset,
min_length: *min_length,
from_end: *from_end,
}
}
Subslice { from, to, from_end } => stable_mir::mir::ProjectionElem::Subslice {
from: *from,
to: *to,
from_end: *from_end,
},
Downcast(_, idx) => stable_mir::mir::ProjectionElem::Downcast(idx.stable(tables)),
OpaqueCast(ty) => stable_mir::mir::ProjectionElem::OpaqueCast(ty.stable(tables)),
Subtype(ty) => stable_mir::mir::ProjectionElem::Subtype(ty.stable(tables)),
}
}
}
@ -693,8 +722,40 @@ impl<'tcx> Stable<'tcx> for mir::Place<'tcx> {
impl<'tcx> Stable<'tcx> for mir::UserTypeProjection {
type T = stable_mir::mir::UserTypeProjection;
fn stable(&self, _: &mut Tables<'tcx>) -> Self::T {
UserTypeProjection { base: self.base.as_usize(), projection: format!("{:?}", self.projs) }
fn stable(&self, tables: &mut Tables<'tcx>) -> Self::T {
UserTypeProjection {
base: self.base.as_usize(),
projection: self.projs.iter().map(|e| e.stable(tables)).collect(),
}
}
}
// ProjectionKind is nearly identical to PlaceElem, except its generic arguments are units. We
// therefore don't need to resolve any arguments with the generic types.
impl<'tcx> Stable<'tcx> for mir::ProjectionKind {
type T = stable_mir::mir::ProjectionElem<(), ()>;
fn stable(&self, tables: &mut Tables<'tcx>) -> Self::T {
use mir::ProjectionElem::*;
match self {
Deref => stable_mir::mir::ProjectionElem::Deref,
Field(idx, ty) => stable_mir::mir::ProjectionElem::Field(idx.stable(tables), *ty),
Index(local) => stable_mir::mir::ProjectionElem::Index(*local),
ConstantIndex { offset, min_length, from_end } => {
stable_mir::mir::ProjectionElem::ConstantIndex {
offset: *offset,
min_length: *min_length,
from_end: *from_end,
}
}
Subslice { from, to, from_end } => stable_mir::mir::ProjectionElem::Subslice {
from: *from,
to: *to,
from_end: *from_end,
},
Downcast(_, idx) => stable_mir::mir::ProjectionElem::Downcast(idx.stable(tables)),
OpaqueCast(ty) => stable_mir::mir::ProjectionElem::OpaqueCast(*ty),
Subtype(ty) => stable_mir::mir::ProjectionElem::Subtype(*ty),
}
}
}

View File

@ -398,22 +398,133 @@ pub enum Operand {
pub struct Place {
pub local: Local,
/// projection out of a place (access a field, deref a pointer, etc)
pub projection: String,
pub projection: Vec<ProjectionElem<Local, Ty>>,
}
// TODO(klinvill): in MIR ProjectionElem is parameterized on the second Field argument and the Index
// argument. This is so it can be used for both the rust provided Places (for which the projection
// elements are of type ProjectionElem<Local, Ty>) and user-provided type annotations (for which the
// projection elements are of type ProjectionElem<(), ()>). Should we do the same thing in Stable MIR?
#[derive(Clone, Debug)]
pub enum ProjectionElem<V, T> {
/// Dereference projections (e.g. `*_1`) project to the address referenced by the base place.
Deref,
/// A field projection (e.g., `f` in `_1.f`) project to a field in the base place. The field is
/// referenced by source-order index rather than the name of the field. The fields type is also
/// given.
Field(FieldIdx, T),
/// Index into a slice/array. The value of the index is computed at runtime using the `V`
/// argument.
///
/// Note that this does not also dereference, and so it does not exactly correspond to slice
/// indexing in Rust. In other words, in the below Rust code:
///
/// ```rust
/// let x = &[1, 2, 3, 4];
/// let i = 2;
/// x[i];
/// ```
///
/// The `x[i]` is turned into a `Deref` followed by an `Index`, not just an `Index`. The same
/// thing is true of the `ConstantIndex` and `Subslice` projections below.
Index(V),
/// Index into a slice/array given by offsets.
///
/// These indices are generated by slice patterns. Easiest to explain by example:
///
/// ```ignore (illustrative)
/// [X, _, .._, _, _] => { offset: 0, min_length: 4, from_end: false },
/// [_, X, .._, _, _] => { offset: 1, min_length: 4, from_end: false },
/// [_, _, .._, X, _] => { offset: 2, min_length: 4, from_end: true },
/// [_, _, .._, _, X] => { offset: 1, min_length: 4, from_end: true },
/// ```
ConstantIndex {
/// index or -index (in Python terms), depending on from_end
offset: u64,
/// The thing being indexed must be at least this long. For arrays this
/// is always the exact length.
min_length: u64,
/// Counting backwards from end? This is always false when indexing an
/// array.
from_end: bool,
},
/// Projects a slice from the base place.
///
/// These indices are generated by slice patterns. If `from_end` is true, this represents
/// `slice[from..slice.len() - to]`. Otherwise it represents `array[from..to]`.
Subslice {
from: u64,
to: u64,
/// Whether `to` counts from the start or end of the array/slice.
from_end: bool,
},
/// "Downcast" to a variant of an enum or a coroutine.
//
// TODO(klinvill): MIR includes an Option<Symbol> argument that is the name of the variant, used
// for printing MIR. However I don't see it used anywhere. Is such a field needed or can we just
// include the VariantIdx which could be used to recover the field name if needed?
Downcast(VariantIdx),
/// Like an explicit cast from an opaque type to a concrete type, but without
/// requiring an intermediate variable.
OpaqueCast(T),
/// A `Subtype(T)` projection is applied to any `StatementKind::Assign` where
/// type of lvalue doesn't match the type of rvalue, the primary goal is making subtyping
/// explicit during optimizations and codegen.
///
/// This projection doesn't impact the runtime behavior of the program except for potentially changing
/// some type metadata of the interpreter or codegen backend.
Subtype(T),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserTypeProjection {
pub base: UserTypeAnnotationIndex,
pub projection: String,
/// `UserTypeProjection` projections need neither the `V` parameter for `Index` nor the `T` for
/// `Field`.
pub projection: Vec<ProjectionElem<(), ()>>,
}
pub type Local = usize;
pub const RETURN_LOCAL: Local = 0;
/// The source-order index of a field in a variant.
///
/// For example, in the following types,
/// ```rust
/// enum Demo1 {
/// Variant0 { a: bool, b: i32 },
/// Variant1 { c: u8, d: u64 },
/// }
/// struct Demo2 { e: u8, f: u16, g: u8 }
/// ```
/// `a`'s `FieldIdx` is `0`,
/// `b`'s `FieldIdx` is `1`,
/// `c`'s `FieldIdx` is `0`, and
/// `g`'s `FieldIdx` is `2`.
type FieldIdx = usize;
/// The source-order index of a variant in a type.
///
/// For example, in the following types,
/// ```rust
/// enum Demo1 {
/// Variant0 { a: bool, b: i32 },
/// Variant1 { c: u8, d: u64 },
/// }
/// struct Demo2 { e: u8, f: u16, g: u8 }
/// ```
/// `a` is in the variant with the `VariantIdx` of `0`,
/// `c` is in the variant with the `VariantIdx` of `1`, and
/// `g` is in the variant with the `VariantIdx` of `0`.
pub type VariantIdx = usize;
type UserTypeAnnotationIndex = usize;
@ -536,6 +647,8 @@ impl Constant {
}
impl Place {
// TODO(klinvill): What is the expected behavior of this function? Should it resolve down the
// chain of projections so that `*(_1.f)` would end up returning the type referenced by `f`?
pub fn ty(&self, locals: &[LocalDecl]) -> Ty {
let _start_ty = locals[self.local].ty;
todo!("Implement projection")

View File

@ -23,6 +23,7 @@ use rustc_hir::def::DefKind;
use rustc_middle::ty::TyCtxt;
use rustc_smir::rustc_internal;
use stable_mir::mir::mono::Instance;
use stable_mir::mir::{ProjectionElem, Rvalue, StatementKind};
use stable_mir::ty::{RigidTy, TyKind};
use std::assert_matches::assert_matches;
use std::io::Write;
@ -163,6 +164,99 @@ fn test_stable_mir(_tcx: TyCtxt<'_>) -> ControlFlow<()> {
stable_mir::ty::TyKind::RigidTy(stable_mir::ty::RigidTy::Bool)
);
let projections_fn = get_item(&items, (DefKind::Fn, "projections")).unwrap();
let body = projections_fn.body();
assert_eq!(body.blocks.len(), 4);
// The first statement assigns `&s.c` to a local. The projections include a deref for `s`, since
// `s` is passed as a reference argument, and a field access for field `c`.
match &body.blocks[0].statements[0].kind {
StatementKind::Assign(
stable_mir::mir::Place { local: _, projection: local_proj },
Rvalue::Ref(_, _, stable_mir::mir::Place { local: _, projection: r_proj }),
) => {
// We can't match on vecs, only on slices. Comparing for equality wouldn't be any easier
// since we'd then have to add in the expected local and region values instead of
// matching on wildcards.
assert_matches!(local_proj[..], []);
match &r_proj[..] {
// Similarly we can't match against a type, only against its kind.
[ProjectionElem::Deref, ProjectionElem::Field(2, ty)] => assert_matches!(
ty.kind(),
TyKind::RigidTy(RigidTy::Uint(stable_mir::ty::UintTy::U8))
),
other => panic!(
"Unable to match against expected rvalue projection. Expected the projection \
for `s.c`, which is a Deref and u8 Field. Got: {:?}",
other
),
};
}
other => panic!(
"Unable to match against expected Assign statement with a Ref rvalue. Expected the \
statement to assign `&s.c` to a local. Got: {:?}",
other
),
};
// This statement assigns `slice[1]` to a local. The projections include a deref for `slice`,
// since `slice` is a reference, and an index.
match &body.blocks[2].statements[0].kind {
StatementKind::Assign(
stable_mir::mir::Place { local: _, projection: local_proj },
Rvalue::Use(stable_mir::mir::Operand::Copy(stable_mir::mir::Place {
local: _,
projection: r_proj,
})),
) => {
// We can't match on vecs, only on slices. Comparing for equality wouldn't be any easier
// since we'd then have to add in the expected local values instead of matching on
// wildcards.
assert_matches!(local_proj[..], []);
assert_matches!(r_proj[..], [ProjectionElem::Deref, ProjectionElem::Index(_)]);
}
other => panic!(
"Unable to match against expected Assign statement with a Use rvalue. Expected the \
statement to assign `slice[1]` to a local. Got: {:?}",
other
),
};
// The first terminator gets a slice of an array via the Index operation. Specifically it
// performs `&vals[1..3]`. There are no projections in this case, the arguments are just locals.
match &body.blocks[0].terminator.kind {
stable_mir::mir::TerminatorKind::Call { args, .. } =>
// We can't match on vecs, only on slices. Comparing for equality wouldn't be any easier
// since we'd then have to add in the expected local values instead of matching on
// wildcards.
{
match &args[..] {
[
stable_mir::mir::Operand::Move(stable_mir::mir::Place {
local: _,
projection: arg1_proj,
}),
stable_mir::mir::Operand::Move(stable_mir::mir::Place {
local: _,
projection: arg2_proj,
}),
] => {
assert_matches!(arg1_proj[..], []);
assert_matches!(arg2_proj[..], []);
}
other => {
panic!(
"Unable to match against expected arguments to Index call. Expected two \
move operands. Got: {:?}",
other
)
}
}
}
other => panic!(
"Unable to match against expected Call terminator. Expected a terminator that calls \
the Index operation. Got: {:?}",
other
),
};
ControlFlow::Continue(())
}
@ -242,6 +336,15 @@ fn generate_input(path: &str) -> std::io::Result<()> {
}} else {{
'b'
}}
}}
pub struct Struct1 {{ _a: u8, _b: u16, c: u8 }}
pub fn projections(s: &Struct1) -> u8 {{
let v = &s.c;
let vals = [1, 2, 3, 4];
let slice = &vals[1..3];
v + slice[1]
}}"#
)?;
Ok(())