Introduce a ConstPropMachine
This allows us to avoid changing things directly in the miri engine just for const prop.
This commit is contained in:
parent
4e58e2e3a3
commit
dcc6c28c53
8 changed files with 177 additions and 39 deletions
|
@ -389,14 +389,6 @@ pub enum UnsupportedOpInfo<'tcx> {
|
||||||
/// Free-form case. Only for errors that are never caught!
|
/// Free-form case. Only for errors that are never caught!
|
||||||
Unsupported(String),
|
Unsupported(String),
|
||||||
|
|
||||||
/// Error used by the `ConstProp` pass when an attempt is made
|
|
||||||
/// to read an uninitialized local.
|
|
||||||
UninitializedLocal,
|
|
||||||
|
|
||||||
/// Error used by the `ConstProp` pass to prevent reading statics
|
|
||||||
/// while evaluating `const` items.
|
|
||||||
ReadOfStaticInConst,
|
|
||||||
|
|
||||||
/// FIXME(#64506) Error used to work around accessing projections of
|
/// FIXME(#64506) Error used to work around accessing projections of
|
||||||
/// uninhabited types.
|
/// uninhabited types.
|
||||||
UninhabitedValue,
|
UninhabitedValue,
|
||||||
|
@ -523,8 +515,6 @@ impl fmt::Debug for UnsupportedOpInfo<'tcx> {
|
||||||
addresses, e.g., comparing pointers into different allocations"),
|
addresses, e.g., comparing pointers into different allocations"),
|
||||||
DeadLocal =>
|
DeadLocal =>
|
||||||
write!(f, "tried to access a dead local variable"),
|
write!(f, "tried to access a dead local variable"),
|
||||||
UninitializedLocal =>
|
|
||||||
write!(f, "tried to access an uninitialized local variable"),
|
|
||||||
DerefFunctionPointer =>
|
DerefFunctionPointer =>
|
||||||
write!(f, "tried to dereference a function pointer"),
|
write!(f, "tried to dereference a function pointer"),
|
||||||
ExecuteMemory =>
|
ExecuteMemory =>
|
||||||
|
@ -566,8 +556,6 @@ impl fmt::Debug for UnsupportedOpInfo<'tcx> {
|
||||||
not a power of two"),
|
not a power of two"),
|
||||||
Unsupported(ref msg) =>
|
Unsupported(ref msg) =>
|
||||||
write!(f, "{}", msg),
|
write!(f, "{}", msg),
|
||||||
ReadOfStaticInConst =>
|
|
||||||
write!(f, "tried to read from a static during const evaluation"),
|
|
||||||
UninhabitedValue =>
|
UninhabitedValue =>
|
||||||
write!(f, "tried to use an uninhabited value"),
|
write!(f, "tried to use an uninhabited value"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ use syntax::source_map::{Span, DUMMY_SP};
|
||||||
|
|
||||||
use crate::interpret::{self,
|
use crate::interpret::{self,
|
||||||
PlaceTy, MPlaceTy, OpTy, ImmTy, Immediate, Scalar, Pointer,
|
PlaceTy, MPlaceTy, OpTy, ImmTy, Immediate, Scalar, Pointer,
|
||||||
RawConst, ConstValue,
|
RawConst, ConstValue, Machine,
|
||||||
InterpResult, InterpErrorInfo, GlobalId, InterpCx, StackPopCleanup,
|
InterpResult, InterpErrorInfo, GlobalId, InterpCx, StackPopCleanup,
|
||||||
Allocation, AllocId, MemoryKind, Memory,
|
Allocation, AllocId, MemoryKind, Memory,
|
||||||
snapshot, RefTracking, intern_const_alloc_recursive,
|
snapshot, RefTracking, intern_const_alloc_recursive,
|
||||||
|
@ -41,7 +41,7 @@ const DETECTOR_SNAPSHOT_PERIOD: isize = 256;
|
||||||
/// that inform us about the generic bounds of the constant. E.g., using an associated constant
|
/// that inform us about the generic bounds of the constant. E.g., using an associated constant
|
||||||
/// of a function's generic parameter will require knowledge about the bounds on the generic
|
/// of a function's generic parameter will require knowledge about the bounds on the generic
|
||||||
/// parameter. These bounds are passed to `mk_eval_cx` via the `ParamEnv` argument.
|
/// parameter. These bounds are passed to `mk_eval_cx` via the `ParamEnv` argument.
|
||||||
pub(crate) fn mk_eval_cx<'mir, 'tcx>(
|
fn mk_eval_cx<'mir, 'tcx>(
|
||||||
tcx: TyCtxt<'tcx>,
|
tcx: TyCtxt<'tcx>,
|
||||||
span: Span,
|
span: Span,
|
||||||
param_env: ty::ParamEnv<'tcx>,
|
param_env: ty::ParamEnv<'tcx>,
|
||||||
|
@ -169,7 +169,7 @@ fn eval_body_using_ecx<'mir, 'tcx>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum ConstEvalError {
|
pub enum ConstEvalError {
|
||||||
NeedsRfc(String),
|
NeedsRfc(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,8 +521,8 @@ pub fn const_variant_index<'tcx>(
|
||||||
/// Turn an interpreter error into something to report to the user.
|
/// Turn an interpreter error into something to report to the user.
|
||||||
/// As a side-effect, if RUSTC_CTFE_BACKTRACE is set, this prints the backtrace.
|
/// As a side-effect, if RUSTC_CTFE_BACKTRACE is set, this prints the backtrace.
|
||||||
/// Should be called only if the error is actually going to to be reported!
|
/// Should be called only if the error is actually going to to be reported!
|
||||||
pub fn error_to_const_error<'mir, 'tcx>(
|
pub fn error_to_const_error<'mir, 'tcx, M: Machine<'mir, 'tcx>>(
|
||||||
ecx: &InterpCx<'mir, 'tcx, CompileTimeInterpreter<'mir, 'tcx>>,
|
ecx: &InterpCx<'mir, 'tcx, M>,
|
||||||
mut error: InterpErrorInfo<'tcx>,
|
mut error: InterpErrorInfo<'tcx>,
|
||||||
) -> ConstEvalErr<'tcx> {
|
) -> ConstEvalErr<'tcx> {
|
||||||
error.print_backtrace();
|
error.print_backtrace();
|
||||||
|
|
|
@ -135,8 +135,7 @@ impl<'tcx, Tag: Copy + 'static> LocalState<'tcx, Tag> {
|
||||||
match self.value {
|
match self.value {
|
||||||
LocalValue::Dead => throw_unsup!(DeadLocal),
|
LocalValue::Dead => throw_unsup!(DeadLocal),
|
||||||
LocalValue::Uninitialized =>
|
LocalValue::Uninitialized =>
|
||||||
// this is reachable from ConstProp
|
bug!("The type checker should prevent reading from a never-written local"),
|
||||||
throw_unsup!(UninitializedLocal),
|
|
||||||
LocalValue::Live(val) => Ok(val),
|
LocalValue::Live(val) => Ok(val),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use rustc::ty::{self, Ty, TyCtxt};
|
||||||
use super::{
|
use super::{
|
||||||
Allocation, AllocId, InterpResult, Scalar, AllocationExtra,
|
Allocation, AllocId, InterpResult, Scalar, AllocationExtra,
|
||||||
InterpCx, PlaceTy, OpTy, ImmTy, MemoryKind, Pointer, Memory,
|
InterpCx, PlaceTy, OpTy, ImmTy, MemoryKind, Pointer, Memory,
|
||||||
|
Frame, Operand,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Whether this kind of memory is allowed to leak
|
/// Whether this kind of memory is allowed to leak
|
||||||
|
@ -184,6 +185,23 @@ pub trait Machine<'mir, 'tcx>: Sized {
|
||||||
dest: PlaceTy<'tcx, Self::PointerTag>,
|
dest: PlaceTy<'tcx, Self::PointerTag>,
|
||||||
) -> InterpResult<'tcx>;
|
) -> InterpResult<'tcx>;
|
||||||
|
|
||||||
|
/// Called to read the specified `local` from the `frame`.
|
||||||
|
fn access_local(
|
||||||
|
_ecx: &InterpCx<'mir, 'tcx, Self>,
|
||||||
|
frame: &Frame<'mir, 'tcx, Self::PointerTag, Self::FrameExtra>,
|
||||||
|
local: mir::Local,
|
||||||
|
) -> InterpResult<'tcx, Operand<Self::PointerTag>> {
|
||||||
|
frame.locals[local].access()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called before a `StaticKind::Static` value is read.
|
||||||
|
fn before_eval_static(
|
||||||
|
_ecx: &InterpCx<'mir, 'tcx, Self>,
|
||||||
|
_place_static: &mir::Static<'tcx>,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Called to initialize the "extra" state of an allocation and make the pointers
|
/// Called to initialize the "extra" state of an allocation and make the pointers
|
||||||
/// it contains (in relocations) tagged. The way we construct allocations is
|
/// it contains (in relocations) tagged. The way we construct allocations is
|
||||||
/// to always first construct it without extra and then add the extra.
|
/// to always first construct it without extra and then add the extra.
|
||||||
|
|
|
@ -458,7 +458,7 @@ impl<'mir, 'tcx, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
|
||||||
// Do not read from ZST, they might not be initialized
|
// Do not read from ZST, they might not be initialized
|
||||||
Operand::Immediate(Scalar::zst().into())
|
Operand::Immediate(Scalar::zst().into())
|
||||||
} else {
|
} else {
|
||||||
frame.locals[local].access()?
|
M::access_local(&self, frame, local)?
|
||||||
};
|
};
|
||||||
Ok(OpTy { op, layout })
|
Ok(OpTy { op, layout })
|
||||||
}
|
}
|
||||||
|
|
|
@ -601,15 +601,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
StaticKind::Static => {
|
StaticKind::Static => {
|
||||||
//if the first frame on the stack isn't a static item, then we shouldn't
|
M::before_eval_static(self, place_static)?;
|
||||||
//eval any static places (unless -Z unleash-the-miri-inside-of-you is on)
|
|
||||||
if let ty::InstanceDef::Item(item_def_id) = self.stack[0].instance.def {
|
|
||||||
if !self.tcx.is_static(item_def_id) &&
|
|
||||||
!self.tcx.sess.opts.debugging_opts.unleash_the_miri_inside_of_you {
|
|
||||||
trace!("eval_static_to_mplace: can't eval static in constant");
|
|
||||||
throw_unsup!(ReadOfStaticInConst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let ty = place_static.ty;
|
let ty = place_static.ty;
|
||||||
assert!(!ty.needs_subst());
|
assert!(!ty.needs_subst());
|
||||||
let layout = self.layout_of(ty)?;
|
let layout = self.layout_of(ty)?;
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
//! Propagates constants for early reporting of statically known
|
//! Propagates constants for early reporting of statically known
|
||||||
//! assertion failures
|
//! assertion failures
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use rustc::hir::def::DefKind;
|
use rustc::hir::def::DefKind;
|
||||||
|
use rustc::hir::def_id::DefId;
|
||||||
use rustc::mir::{
|
use rustc::mir::{
|
||||||
AggregateKind, Constant, Location, Place, PlaceBase, Body, Operand, Rvalue,
|
AggregateKind, Constant, Location, Place, PlaceBase, Body, Operand, Rvalue,
|
||||||
Local, NullOp, UnOp, StatementKind, Statement, LocalKind,
|
Local, NullOp, UnOp, StatementKind, Statement, LocalKind, Static,
|
||||||
TerminatorKind, Terminator, ClearCrossCrate, SourceInfo, BinOp,
|
TerminatorKind, Terminator, ClearCrossCrate, SourceInfo, BinOp,
|
||||||
SourceScope, SourceScopeLocalData, LocalDecl,
|
SourceScope, SourceScopeLocalData, LocalDecl, BasicBlock,
|
||||||
};
|
};
|
||||||
use rustc::mir::visit::{
|
use rustc::mir::visit::{
|
||||||
Visitor, PlaceContext, MutatingUseContext, MutVisitor, NonMutatingUseContext,
|
Visitor, PlaceContext, MutatingUseContext, MutVisitor, NonMutatingUseContext,
|
||||||
|
@ -17,6 +19,7 @@ use rustc::mir::interpret::{Scalar, InterpResult, PanicInfo};
|
||||||
use rustc::ty::{self, Instance, ParamEnv, Ty, TyCtxt};
|
use rustc::ty::{self, Instance, ParamEnv, Ty, TyCtxt};
|
||||||
use syntax_pos::{Span, DUMMY_SP};
|
use syntax_pos::{Span, DUMMY_SP};
|
||||||
use rustc::ty::subst::InternalSubsts;
|
use rustc::ty::subst::InternalSubsts;
|
||||||
|
use rustc_data_structures::fx::FxHashMap;
|
||||||
use rustc_data_structures::indexed_vec::IndexVec;
|
use rustc_data_structures::indexed_vec::IndexVec;
|
||||||
use rustc::ty::layout::{
|
use rustc::ty::layout::{
|
||||||
LayoutOf, TyLayout, LayoutError, HasTyCtxt, TargetDataLayout, HasDataLayout,
|
LayoutOf, TyLayout, LayoutError, HasTyCtxt, TargetDataLayout, HasDataLayout,
|
||||||
|
@ -24,11 +27,11 @@ use rustc::ty::layout::{
|
||||||
|
|
||||||
use crate::interpret::{
|
use crate::interpret::{
|
||||||
self, InterpCx, ScalarMaybeUndef, Immediate, OpTy,
|
self, InterpCx, ScalarMaybeUndef, Immediate, OpTy,
|
||||||
StackPopCleanup, LocalValue, LocalState,
|
StackPopCleanup, LocalValue, LocalState, AllocId, Frame,
|
||||||
};
|
Allocation, MemoryKind, ImmTy, Pointer, Memory, PlaceTy,
|
||||||
use crate::const_eval::{
|
Operand as InterpOperand,
|
||||||
CompileTimeInterpreter, error_to_const_error, mk_eval_cx,
|
|
||||||
};
|
};
|
||||||
|
use crate::const_eval::error_to_const_error;
|
||||||
use crate::transform::{MirPass, MirSource};
|
use crate::transform::{MirPass, MirSource};
|
||||||
|
|
||||||
pub struct ConstProp;
|
pub struct ConstProp;
|
||||||
|
@ -111,11 +114,149 @@ impl<'tcx> MirPass<'tcx> for ConstProp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ConstPropMachine;
|
||||||
|
|
||||||
|
impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for ConstPropMachine {
|
||||||
|
type MemoryKinds= !;
|
||||||
|
type PointerTag = ();
|
||||||
|
type ExtraFnVal = !;
|
||||||
|
|
||||||
|
type FrameExtra = ();
|
||||||
|
type MemoryExtra = ();
|
||||||
|
type AllocExtra = ();
|
||||||
|
|
||||||
|
type MemoryMap = FxHashMap<AllocId, (MemoryKind<!>, Allocation)>;
|
||||||
|
|
||||||
|
const STATIC_KIND: Option<!> = None;
|
||||||
|
|
||||||
|
const CHECK_ALIGN: bool = false;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn enforce_validity(_ecx: &InterpCx<'mir, 'tcx, Self>) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_fn(
|
||||||
|
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
|
||||||
|
_instance: ty::Instance<'tcx>,
|
||||||
|
_args: &[OpTy<'tcx>],
|
||||||
|
_dest: Option<PlaceTy<'tcx>>,
|
||||||
|
_ret: Option<BasicBlock>,
|
||||||
|
) -> InterpResult<'tcx, Option<&'mir Body<'tcx>>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_extra_fn(
|
||||||
|
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
|
||||||
|
fn_val: !,
|
||||||
|
_args: &[OpTy<'tcx>],
|
||||||
|
_dest: Option<PlaceTy<'tcx>>,
|
||||||
|
_ret: Option<BasicBlock>,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
match fn_val {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_intrinsic(
|
||||||
|
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
|
||||||
|
_instance: ty::Instance<'tcx>,
|
||||||
|
_args: &[OpTy<'tcx>],
|
||||||
|
_dest: PlaceTy<'tcx>,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
throw_unsup_format!("calling intrinsics isn't supported in ConstProp");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ptr_to_int(
|
||||||
|
_mem: &Memory<'mir, 'tcx, Self>,
|
||||||
|
_ptr: Pointer,
|
||||||
|
) -> InterpResult<'tcx, u64> {
|
||||||
|
throw_unsup_format!("ptr-to-int casts aren't supported in ConstProp");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary_ptr_op(
|
||||||
|
_ecx: &InterpCx<'mir, 'tcx, Self>,
|
||||||
|
_bin_op: BinOp,
|
||||||
|
_left: ImmTy<'tcx>,
|
||||||
|
_right: ImmTy<'tcx>,
|
||||||
|
) -> InterpResult<'tcx, (Scalar, bool, Ty<'tcx>)> {
|
||||||
|
// We can't do this because aliasing of memory can differ between const eval and llvm
|
||||||
|
throw_unsup_format!("pointer arithmetic or comparisons aren't supported in ConstProp");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_foreign_static(
|
||||||
|
_tcx: TyCtxt<'tcx>,
|
||||||
|
_def_id: DefId,
|
||||||
|
) -> InterpResult<'tcx, Cow<'tcx, Allocation<Self::PointerTag>>> {
|
||||||
|
throw_unsup!(ReadForeignStatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn tag_allocation<'b>(
|
||||||
|
_memory_extra: &(),
|
||||||
|
_id: AllocId,
|
||||||
|
alloc: Cow<'b, Allocation>,
|
||||||
|
_kind: Option<MemoryKind<!>>,
|
||||||
|
) -> (Cow<'b, Allocation<Self::PointerTag>>, Self::PointerTag) {
|
||||||
|
// We do not use a tag so we can just cheaply forward the allocation
|
||||||
|
(alloc, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn tag_static_base_pointer(
|
||||||
|
_memory_extra: &(),
|
||||||
|
_id: AllocId,
|
||||||
|
) -> Self::PointerTag {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_alloc(
|
||||||
|
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
|
||||||
|
_dest: PlaceTy<'tcx>,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
throw_unsup_format!("can't const prop `box` keyword");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_local(
|
||||||
|
_ecx: &InterpCx<'mir, 'tcx, Self>,
|
||||||
|
frame: &Frame<'mir, 'tcx, Self::PointerTag, Self::FrameExtra>,
|
||||||
|
local: Local,
|
||||||
|
) -> InterpResult<'tcx, InterpOperand<Self::PointerTag>> {
|
||||||
|
let l = &frame.locals[local];
|
||||||
|
|
||||||
|
if l.value == LocalValue::Uninitialized {
|
||||||
|
throw_unsup_format!("tried to access an uninitialized local");
|
||||||
|
}
|
||||||
|
|
||||||
|
l.access()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_eval_static(
|
||||||
|
_ecx: &InterpCx<'mir, 'tcx, Self>,
|
||||||
|
_place_static: &Static<'tcx>,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
throw_unsup_format!("can't eval statics in ConstProp");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_terminator(_ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn stack_push(_ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called immediately before a stack frame gets popped.
|
||||||
|
#[inline(always)]
|
||||||
|
fn stack_pop(_ecx: &mut InterpCx<'mir, 'tcx, Self>, _extra: ()) -> InterpResult<'tcx> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Const<'tcx> = OpTy<'tcx>;
|
type Const<'tcx> = OpTy<'tcx>;
|
||||||
|
|
||||||
/// Finds optimization opportunities on the MIR.
|
/// Finds optimization opportunities on the MIR.
|
||||||
struct ConstPropagator<'mir, 'tcx> {
|
struct ConstPropagator<'mir, 'tcx> {
|
||||||
ecx: InterpCx<'mir, 'tcx, CompileTimeInterpreter<'mir, 'tcx>>,
|
ecx: InterpCx<'mir, 'tcx, ConstPropMachine>,
|
||||||
tcx: TyCtxt<'tcx>,
|
tcx: TyCtxt<'tcx>,
|
||||||
source: MirSource<'tcx>,
|
source: MirSource<'tcx>,
|
||||||
can_const_prop: IndexVec<Local, bool>,
|
can_const_prop: IndexVec<Local, bool>,
|
||||||
|
@ -158,7 +299,7 @@ impl<'mir, 'tcx> ConstPropagator<'mir, 'tcx> {
|
||||||
let def_id = source.def_id();
|
let def_id = source.def_id();
|
||||||
let param_env = tcx.param_env(def_id);
|
let param_env = tcx.param_env(def_id);
|
||||||
let span = tcx.def_span(def_id);
|
let span = tcx.def_span(def_id);
|
||||||
let mut ecx = mk_eval_cx(tcx, span, param_env);
|
let mut ecx = InterpCx::new(tcx.at(span), param_env, ConstPropMachine, ());
|
||||||
let can_const_prop = CanConstProp::check(body);
|
let can_const_prop = CanConstProp::check(body);
|
||||||
|
|
||||||
ecx.push_stack_frame(
|
ecx.push_stack_frame(
|
||||||
|
|
|
@ -6,5 +6,4 @@ LL | static B: [u32; 1] = [0; A.len()];
|
||||||
|
|
||||||
error: aborting due to previous error
|
error: aborting due to previous error
|
||||||
|
|
||||||
Some errors have detailed explanations: E0013, E0080.
|
For more information about this error, try `rustc --explain E0013`.
|
||||||
For more information about an error, try `rustc --explain E0013`.
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue