Skip to content
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

Make str into a libcore struct (redux) #107939

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_cranelift/src/unsize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ pub(crate) fn size_and_align_of_dst<'tcx>(
// load size/align from vtable
(crate::vtable::size_of_obj(fx, info), crate::vtable::min_align_of_obj(fx, info))
}
ty::Slice(_) | ty::Str => {
ty::Slice(_) => {
let unit = layout.field(fx, 0);
// The info in this case is the length of the str, so the size is that
// times the unit size.
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_cranelift/src/value_and_place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fn codegen_field<'tcx>(
return simple(fx);
}
match field_layout.ty.kind() {
ty::Slice(..) | ty::Str | ty::Foreign(..) => simple(fx),
ty::Slice(..) | ty::Foreign(..) => simple(fx),
ty::Adt(def, _) if def.repr().packed() => {
assert_eq!(layout.align.abi.bytes(), 1);
simple(fx)
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_gcc/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl<'gcc, 'tcx> ConstMethods<'tcx> for CodegenCx<'gcc, 'tcx> {
.1;
let len = s.len();
let cs = self.const_ptrcast(str_global.get_address(None),
self.type_ptr_to(self.layout_of(self.tcx.types.str_).gcc_type(self, true)),
self.type_ptr_to(self.layout_of(self.tcx.mk_str()).gcc_type(self, true)),
);
(cs, self.const_usize(len as u64))
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_gcc/src/type_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub fn uncached_gcc_type<'gcc, 'tcx>(cx: &CodegenCx<'gcc, 'tcx>, layout: TyAndLa
// FIXME(eddyb) producing readable type names for trait objects can result
// in problematically distinct types due to HRTB and subtyping (see #47638).
// ty::Dynamic(..) |
ty::Adt(..) | ty::Closure(..) | ty::Foreign(..) | ty::Generator(..) | ty::Str
ty::Adt(..) | ty::Closure(..) | ty::Foreign(..) | ty::Generator(..)
if !cx.sess().fewer_names() =>
{
let mut name = with_no_trimmed_paths!(layout.ty.to_string());
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_llvm/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ impl<'ll, 'tcx> ConstMethods<'tcx> for CodegenCx<'ll, 'tcx> {
let len = s.len();
let cs = consts::ptrcast(
str_global,
self.type_ptr_to(self.layout_of(self.tcx.types.str_).llvm_type(self)),
self.type_ptr_to(self.layout_of(self.tcx.mk_str()).llvm_type(self)),
);
(cs, self.const_usize(len as u64))
}
Expand Down
3 changes: 1 addition & 2 deletions compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,6 @@ fn build_slice_type_di_node<'ll, 'tcx>(
) -> DINodeCreationResult<'ll> {
let element_type = match slice_type.kind() {
ty::Slice(element_type) => *element_type,
ty::Str => cx.tcx.types.u8,
_ => {
bug!(
"Only ty::Slice is valid for build_slice_type_di_node(). Found {:?} instead.",
Expand Down Expand Up @@ -440,7 +439,7 @@ pub fn type_di_node<'ll, 'tcx>(cx: &CodegenCx<'ll, 'tcx>, t: Ty<'tcx>) -> &'ll D
}
ty::Tuple(elements) if elements.is_empty() => build_basic_type_di_node(cx, t),
ty::Array(..) => build_fixed_size_array_di_node(cx, unique_type_id, t),
ty::Slice(_) | ty::Str => build_slice_type_di_node(cx, t, unique_type_id),
ty::Slice(_) => build_slice_type_di_node(cx, t, unique_type_id),
ty::Dynamic(..) => build_dyn_type_di_node(cx, t, unique_type_id),
ty::Foreign(..) => build_foreign_type_di_node(cx, t, unique_type_id),
ty::RawPtr(ty::TypeAndMut { ty: pointee_type, .. }) | ty::Ref(_, pointee_type, _) => {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_llvm/src/debuginfo/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub(crate) fn fat_pointer_kind<'ll, 'tcx>(
}

match *pointee_tail_ty.kind() {
ty::Str | ty::Slice(_) => Some(FatPtrKind::Slice),
ty::Slice(_) => Some(FatPtrKind::Slice),
ty::Dynamic(..) => Some(FatPtrKind::Dyn),
ty::Foreign(_) => {
// Assert that pointers to foreign types really are thin:
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_llvm/src/type_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn uncached_llvm_type<'a, 'tcx>(
// FIXME(eddyb) producing readable type names for trait objects can result
// in problematically distinct types due to HRTB and subtyping (see #47638).
// ty::Dynamic(..) |
ty::Adt(..) | ty::Closure(..) | ty::Foreign(..) | ty::Generator(..) | ty::Str
ty::Adt(..) | ty::Closure(..) | ty::Foreign(..) | ty::Generator(..)
// For performance reasons we use names only when emitting LLVM IR. Unless we are on
// LLVM < 14, where the use of unnamed types resulted in various issues, e.g., #76213,
// #79564, and #79246.
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_codegen_ssa/src/debuginfo/type_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ fn push_debuginfo_type_name<'tcx>(
match *t.kind() {
ty::Bool => output.push_str("bool"),
ty::Char => output.push_str("char"),
ty::Str => {
// FIXME(str): Do we need this? Or should we really just treat it like an ADT?
ty::Adt(def, _) if def.is_str() => {
if cpp_like_debuginfo {
output.push_str("str$")
} else {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_ssa/src/glue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub fn size_and_align_of_dst<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(

(size, align)
}
ty::Slice(_) | ty::Str => {
ty::Slice(_) => {
let unit = layout.field(bx, 0);
// The info in this case is the length of the str, so the size is that
// times the unit size.
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_ssa/src/mir/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl<'a, 'tcx, V: CodegenObject> PlaceRef<'tcx, V> {
return simple();
}
_ if field.is_sized() => return simple(),
ty::Slice(..) | ty::Str | ty::Foreign(..) => return simple(),
ty::Slice(..) | ty::Foreign(..) => return simple(),
ty::Adt(def, _) => {
if def.repr().packed() {
// FIXME(eddyb) generalize the adjustment when we
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_ssa/src/traits/type_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub trait DerivedTypeMethods<'tcx>: BaseTypeMethods<'tcx> + MiscMethods<'tcx> {
let tail = self.tcx().struct_tail_erasing_lifetimes(ty, param_env);
match tail.kind() {
ty::Foreign(..) => false,
ty::Str | ty::Slice(..) | ty::Dynamic(..) => true,
ty::Slice(..) | ty::Dynamic(..) => true,
_ => bug!("unexpected unsized tail: {:?}", tail),
}
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_const_eval/src/const_eval/eval_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub(super) fn op_to_const<'tcx>(
Abi::ScalarPair(..) => match op.layout.ty.kind() {
ty::Ref(_, inner, _) => match *inner.kind() {
ty::Slice(elem) => elem == ecx.tcx.types.u8,
ty::Str => true,
ty::Adt(def, _) => def.is_str(),
_ => false,
},
_ => false,
Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_const_eval/src/const_eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ pub(crate) fn deref_mir_constant<'tcx>(
MemPlaceMeta::None => mplace.layout.ty,
// In case of unsized types, figure out the real type behind.
MemPlaceMeta::Meta(scalar) => match mplace.layout.ty.kind() {
ty::Str => bug!("there's no sized equivalent of a `str`"),
ty::Slice(elem_ty) => tcx.mk_array(*elem_ty, scalar.to_machine_usize(&tcx).unwrap()),
_ => bug!(
"type {} should not have metadata, but had {:?}",
Expand Down
21 changes: 5 additions & 16 deletions compiler/rustc_const_eval/src/const_eval/valtrees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ pub(crate) fn const_to_valtree_inner<'tcx>(
const_to_valtree_inner(ecx, &derefd_place, num_nodes)
}

ty::Str | ty::Slice(_) | ty::Array(_, _) => {
ty::Slice(_) | ty::Array(_, _) => {
slice_branches(ecx, place, num_nodes)
}

// Trait objects are not allowed in type level constants, as we have no concept for
// resolving their backing type, even if we can do that at const eval time. We may
// hypothetically be able to allow `dyn StructuralEq` trait objects in the future,
Expand Down Expand Up @@ -187,16 +188,9 @@ fn get_info_on_unsized_field<'tcx>(
);
let unsized_inner_ty = match tail.kind() {
ty::Slice(t) => *t,
ty::Str => tail,
_ => bug!("expected Slice or Str"),
};

// Have to adjust type for ty::Str
let unsized_inner_ty = match unsized_inner_ty.kind() {
ty::Str => tcx.mk_ty(ty::Uint(ty::UintTy::U8)),
_ => unsized_inner_ty,
};

// Get the number of elements in the unsized field
let num_elems = last_valtree.unwrap_branch().len();

Expand All @@ -215,10 +209,6 @@ fn create_pointee_place<'tcx>(
// We need to create `Allocation`s for custom DSTs

let (unsized_inner_ty, num_elems) = get_info_on_unsized_field(ty, valtree, tcx);
let unsized_inner_ty = match unsized_inner_ty.kind() {
ty::Str => tcx.mk_ty(ty::Uint(ty::UintTy::U8)),
_ => unsized_inner_ty,
};
let unsized_inner_ty_size =
tcx.layout_of(ty::ParamEnv::empty().and(unsized_inner_ty)).unwrap().layout.size();
debug!(?unsized_inner_ty, ?unsized_inner_ty_size, ?num_elems);
Expand Down Expand Up @@ -317,7 +307,6 @@ pub fn valtree_to_const_value<'tcx>(
| ty::GeneratorWitnessMIR(..)
| ty::FnPtr(_)
| ty::RawPtr(_)
| ty::Str
| ty::Slice(_)
| ty::Dynamic(..) => bug!("no ValTree should have been created for type {:?}", ty.kind()),
}
Expand Down Expand Up @@ -353,7 +342,7 @@ fn valtree_into_mplace<'tcx>(
intern_const_alloc_recursive(ecx, InternKind::Constant, &pointee_place).unwrap();

let imm = match inner_ty.kind() {
ty::Slice(_) | ty::Str => {
ty::Slice(_) => {
let len = valtree.unwrap_branch().len();
let len_scalar = Scalar::from_machine_usize(len as u64, &tcx);

Expand All @@ -368,7 +357,7 @@ fn valtree_into_mplace<'tcx>(

ecx.write_immediate(imm, &place.into()).unwrap();
}
ty::Adt(_, _) | ty::Tuple(_) | ty::Array(_, _) | ty::Str | ty::Slice(_) => {
ty::Adt(_, _) | ty::Tuple(_) | ty::Array(_, _) | ty::Slice(_) => {
let branches = valtree.unwrap_branch();

// Need to downcast place for enums
Expand Down Expand Up @@ -396,7 +385,7 @@ fn valtree_into_mplace<'tcx>(
debug!(?i, ?inner_valtree);

let mut place_inner = match ty.kind() {
ty::Str | ty::Slice(_) => ecx.mplace_index(&place, i as u64).unwrap(),
ty::Slice(_) => ecx.mplace_index(&place, i as u64).unwrap(),
_ if !ty.is_sized(*ecx.tcx, ty::ParamEnv::empty())
&& i == branches.len() - 1 =>
{
Expand Down
16 changes: 15 additions & 1 deletion compiler/rustc_const_eval/src/interpret/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,20 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
return Ok(Some((layout.size, layout.align.abi)));
}
match layout.ty.kind() {
// FIXME(str): Do we need this?
ty::Adt(def, _) if def.is_str() => {
let len = metadata.unwrap_meta().to_machine_usize(self)?;
let elem = layout.field(self, 0);

// Make sure the slice is not too big.
let size = elem.size.bytes().saturating_mul(len); // we rely on `max_size_of_val` being smaller than `u64::MAX`.
let size = Size::from_bytes(size);
if size > self.max_size_of_val() {
throw_ub!(InvalidMeta("slice is bigger than largest supported object"));
}
Ok(Some((size, elem.align.abi)))
}

ty::Adt(..) | ty::Tuple(..) => {
// First get the size of all statically known fields.
// Don't use type_of::sizing_type_of because that expects t to be sized,
Expand Down Expand Up @@ -638,7 +652,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
Ok(Some(self.get_vtable_size_and_align(vtable)?))
}

ty::Slice(_) | ty::Str => {
ty::Slice(_) => {
let len = metadata.unwrap_meta().to_machine_usize(self)?;
let elem = layout.field(self, 0);

Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_const_eval/src/interpret/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ pub(crate) fn eval_nullary_intrinsic<'tcx>(
| ty::Uint(_)
| ty::Float(_)
| ty::Foreign(_)
| ty::Str
| ty::Array(_, _)
| ty::Slice(_)
| ty::RawPtr(_)
Expand Down
12 changes: 7 additions & 5 deletions compiler/rustc_const_eval/src/interpret/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ impl<'tcx, Prov: Provenance> MPlaceTy<'tcx, Prov> {
if self.layout.is_unsized() {
// We need to consult `meta` metadata
match self.layout.ty.kind() {
ty::Slice(..) | ty::Str => self.mplace.meta.unwrap_meta().to_machine_usize(cx),
ty::Slice(..) => self.mplace.meta.unwrap_meta().to_machine_usize(cx),
ty::Adt(adt, _) if adt.is_str() => {
self.mplace.meta.unwrap_meta().to_machine_usize(cx)
}
_ => bug!("len not supported on unsized type {:?}", self.layout.ty),
}
} else {
Expand Down Expand Up @@ -759,10 +762,9 @@ where
let meta = Scalar::from_machine_usize(u64::try_from(str.len()).unwrap(), self);
let mplace = MemPlace { ptr: ptr.into(), meta: MemPlaceMeta::Meta(meta) };

let ty = self.tcx.mk_ref(
self.tcx.lifetimes.re_static,
ty::TypeAndMut { ty: self.tcx.types.str_, mutbl },
);
let ty = self
.tcx
.mk_ref(self.tcx.lifetimes.re_static, ty::TypeAndMut { ty: self.tcx.mk_str(), mutbl });
let layout = self.layout_of(ty).unwrap();
Ok(MPlaceTy { mplace, layout, align: layout.align.abi })
}
Expand Down
5 changes: 3 additions & 2 deletions compiler/rustc_const_eval/src/interpret/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@ where
}
_ => span_bug!(
self.cur_span(),
"`mplace_index` called on non-array type {:?}",
base.layout.ty
"`mplace_index` called on non-array type {:?} with abi {:?}",
base.layout.ty,
base.layout.fields
),
}
}
Expand Down
12 changes: 1 addition & 11 deletions compiler/rustc_const_eval/src/interpret/validity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ impl<'rt, 'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValidityVisitor<'rt, 'mir, '
);
// FIXME: check if the type/trait match what ty::Dynamic says?
}
ty::Slice(..) | ty::Str => {
ty::Slice(..) => {
let _len = meta.unwrap_meta().to_machine_usize(self.ecx)?;
// We do not check that `len * elem_size <= isize::MAX`:
// that is only required for references, and there it falls out of the
Expand Down Expand Up @@ -590,7 +590,6 @@ impl<'rt, 'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValidityVisitor<'rt, 'mir, '
| ty::Tuple(..)
| ty::Array(..)
| ty::Slice(..)
| ty::Str
| ty::Dynamic(..)
| ty::Closure(..)
| ty::Generator(..) => Ok(false),
Expand Down Expand Up @@ -813,15 +812,6 @@ impl<'rt, 'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueVisitor<'mir, 'tcx, M>
fields: impl Iterator<Item = InterpResult<'tcx, Self::V>>,
) -> InterpResult<'tcx> {
match op.layout.ty.kind() {
ty::Str => {
let mplace = op.assert_mem_place(); // strings are unsized and hence never immediate
let len = mplace.len(self.ecx)?;
try_validation!(
self.ecx.read_bytes_ptr_strip_provenance(mplace.ptr, Size::from_bytes(len)),
self.path,
InvalidUninitBytes(..) => { "uninitialized data in `str`" },
);
}
ty::Array(tys, ..) | ty::Slice(tys)
// This optimization applies for types that can hold arbitrary bytes (such as
// integer and floating point types) or for structs or tuples with no fields.
Expand Down
4 changes: 3 additions & 1 deletion compiler/rustc_const_eval/src/util/type_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ impl<'tcx> Printer<'tcx> for AbsolutePathPrinter<'tcx> {
| ty::Int(_)
| ty::Uint(_)
| ty::Float(_)
| ty::Str
| ty::Array(_, _)
| ty::Slice(_)
| ty::RawPtr(_)
Expand All @@ -49,6 +48,9 @@ impl<'tcx> Printer<'tcx> for AbsolutePathPrinter<'tcx> {
| ty::Tuple(_)
| ty::Dynamic(_, _, _) => self.pretty_print_type(ty),

// FIXME(str): We could accept that it's an ADt, and print this like `std::str::Str`?
ty::Adt(def, _) if def.is_str() => self.pretty_print_type(ty),

// Placeholders (all printed as `_` to uniformize them).
ty::Param(_) | ty::Bound(..) | ty::Placeholder(_) | ty::Infer(_) | ty::Error(_) => {
write!(self, "_")?;
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_hir/src/lang_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ language_item_table! {
RangeTo, sym::RangeTo, range_to_struct, Target::Struct, GenericRequirement::None;

String, sym::String, string, Target::Struct, GenericRequirement::None;
Str, sym::str, str_type, Target::Struct, GenericRequirement::Exact(0);
}

pub enum GenericRequirement {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_hir_analysis/src/astconv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2819,7 +2819,7 @@ impl<'o, 'tcx> dyn AstConv<'tcx> + 'o {
hir::PrimTy::Int(it) => tcx.mk_mach_int(ty::int_ty(it)),
hir::PrimTy::Uint(uit) => tcx.mk_mach_uint(ty::uint_ty(uit)),
hir::PrimTy::Float(ft) => tcx.mk_mach_float(ty::float_ty(ft)),
hir::PrimTy::Str => tcx.types.str_,
hir::PrimTy::Str => tcx.mk_str(),
}
}
Res::Err => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ impl<'tcx> InherentCollect<'tcx> {
| ty::Int(_)
| ty::Uint(_)
| ty::Float(_)
| ty::Str
| ty::Array(..)
| ty::Slice(_)
| ty::RawPtr(_)
Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_hir_analysis/src/coherence/orphan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ fn do_orphan_check_impl<'tcx>(
| ty::Int(..)
| ty::Uint(..)
| ty::Float(..)
| ty::Str
| ty::Array(..)
| ty::Slice(..)
| ty::RawPtr(..)
Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_hir_analysis/src/variance/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ impl<'a, 'tcx> ConstraintContext<'a, 'tcx> {
| ty::Int(_)
| ty::Uint(_)
| ty::Float(_)
| ty::Str
| ty::Never
| ty::Foreign(..) => {
// leaf type -- noop
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_hir_typeck/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
}

Ok(match *t.kind() {
ty::Slice(_) | ty::Str => Some(PointerKind::Length),
ty::Slice(_) => Some(PointerKind::Length),
ty::Dynamic(ref tty, _, ty::Dyn) => Some(PointerKind::VTable(tty.principal_def_id())),
ty::Adt(def, substs) if def.is_struct() => match def.non_enum_variant().fields.last() {
None => Some(PointerKind::Thin),
Expand Down Expand Up @@ -1101,7 +1101,7 @@ impl<'a, 'tcx> CastCheck<'tcx> {
let derefed = fcx
.autoderef(self.expr_span, self.expr_ty)
.silence_errors()
.find(|t| matches!(t.0.kind(), ty::Str | ty::Slice(..)));
.find(|t| matches!(t.0.kind(), ty::Slice(..)) || t.0.is_str());

if let Some((deref_ty, _)) = derefed {
// Give a note about what the expr derefs to.
Expand Down
Loading