Rollup merge of #126833 - RalfJung:extern-type-field-ice, r=compiler-errors
don't ICE when encountering an extern type field during validation "extern type" is a pain that keeps on giving... Fixes https://github.com/rust-lang/rust/issues/126814 r? ```@oli-obk```
This commit is contained in:
commit
a9959bd1ab
@ -89,6 +89,8 @@ const_eval_exact_div_has_remainder =
|
|||||||
|
|
||||||
const_eval_extern_static =
|
const_eval_extern_static =
|
||||||
cannot access extern static ({$did})
|
cannot access extern static ({$did})
|
||||||
|
const_eval_extern_type_field = `extern type` field does not have a known offset
|
||||||
|
|
||||||
const_eval_fn_ptr_call =
|
const_eval_fn_ptr_call =
|
||||||
function pointers need an RFC before allowed to be called in {const_eval_const_context}s
|
function pointers need an RFC before allowed to be called in {const_eval_const_context}s
|
||||||
const_eval_for_loop_into_iter_non_const =
|
const_eval_for_loop_into_iter_non_const =
|
||||||
|
@ -386,33 +386,8 @@ fn eval_in_interpreter<'tcx, R: InterpretationResult<'tcx>>(
|
|||||||
CompileTimeMachine::new(CanAccessMutGlobal::from(is_static), CheckAlignment::Error),
|
CompileTimeMachine::new(CanAccessMutGlobal::from(is_static), CheckAlignment::Error),
|
||||||
);
|
);
|
||||||
let res = ecx.load_mir(cid.instance.def, cid.promoted);
|
let res = ecx.load_mir(cid.instance.def, cid.promoted);
|
||||||
res.and_then(|body| eval_body_using_ecx(&mut ecx, cid, body)).map_err(|error| {
|
res.and_then(|body| eval_body_using_ecx(&mut ecx, cid, body))
|
||||||
let (error, backtrace) = error.into_parts();
|
.map_err(|error| report_eval_error(&ecx, cid, error))
|
||||||
backtrace.print_backtrace();
|
|
||||||
|
|
||||||
let (kind, instance) = if ecx.tcx.is_static(cid.instance.def_id()) {
|
|
||||||
("static", String::new())
|
|
||||||
} else {
|
|
||||||
// If the current item has generics, we'd like to enrich the message with the
|
|
||||||
// instance and its args: to show the actual compile-time values, in addition to
|
|
||||||
// the expression, leading to the const eval error.
|
|
||||||
let instance = &cid.instance;
|
|
||||||
if !instance.args.is_empty() {
|
|
||||||
let instance = with_no_trimmed_paths!(instance.to_string());
|
|
||||||
("const_with_path", instance)
|
|
||||||
} else {
|
|
||||||
("const", String::new())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
super::report(
|
|
||||||
*ecx.tcx,
|
|
||||||
error,
|
|
||||||
DUMMY_SP,
|
|
||||||
|| super::get_span_and_frames(ecx.tcx, ecx.stack()),
|
|
||||||
|span, frames| ConstEvalError { span, error_kind: kind, instance, frame_notes: frames },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
@ -438,23 +413,60 @@ fn const_validate_mplace<'tcx>(
|
|||||||
ecx.const_validate_operand(&mplace.into(), path, &mut ref_tracking, mode)
|
ecx.const_validate_operand(&mplace.into(), path, &mut ref_tracking, mode)
|
||||||
// Instead of just reporting the `InterpError` via the usual machinery, we give a more targeted
|
// Instead of just reporting the `InterpError` via the usual machinery, we give a more targeted
|
||||||
// error about the validation failure.
|
// error about the validation failure.
|
||||||
.map_err(|error| report_validation_error(&ecx, error, alloc_id))?;
|
.map_err(|error| report_validation_error(&ecx, cid, error, alloc_id))?;
|
||||||
inner = true;
|
inner = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(never)]
|
||||||
fn report_validation_error<'tcx>(
|
fn report_eval_error<'tcx>(
|
||||||
ecx: &InterpCx<'tcx, CompileTimeMachine<'tcx>>,
|
ecx: &InterpCx<'tcx, CompileTimeMachine<'tcx>>,
|
||||||
|
cid: GlobalId<'tcx>,
|
||||||
error: InterpErrorInfo<'tcx>,
|
error: InterpErrorInfo<'tcx>,
|
||||||
alloc_id: AllocId,
|
|
||||||
) -> ErrorHandled {
|
) -> ErrorHandled {
|
||||||
let (error, backtrace) = error.into_parts();
|
let (error, backtrace) = error.into_parts();
|
||||||
backtrace.print_backtrace();
|
backtrace.print_backtrace();
|
||||||
|
|
||||||
let ub_note = matches!(error, InterpError::UndefinedBehavior(_)).then(|| {});
|
let (kind, instance) = if ecx.tcx.is_static(cid.instance.def_id()) {
|
||||||
|
("static", String::new())
|
||||||
|
} else {
|
||||||
|
// If the current item has generics, we'd like to enrich the message with the
|
||||||
|
// instance and its args: to show the actual compile-time values, in addition to
|
||||||
|
// the expression, leading to the const eval error.
|
||||||
|
let instance = &cid.instance;
|
||||||
|
if !instance.args.is_empty() {
|
||||||
|
let instance = with_no_trimmed_paths!(instance.to_string());
|
||||||
|
("const_with_path", instance)
|
||||||
|
} else {
|
||||||
|
("const", String::new())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
super::report(
|
||||||
|
*ecx.tcx,
|
||||||
|
error,
|
||||||
|
DUMMY_SP,
|
||||||
|
|| super::get_span_and_frames(ecx.tcx, ecx.stack()),
|
||||||
|
|span, frames| ConstEvalError { span, error_kind: kind, instance, frame_notes: frames },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn report_validation_error<'tcx>(
|
||||||
|
ecx: &InterpCx<'tcx, CompileTimeMachine<'tcx>>,
|
||||||
|
cid: GlobalId<'tcx>,
|
||||||
|
error: InterpErrorInfo<'tcx>,
|
||||||
|
alloc_id: AllocId,
|
||||||
|
) -> ErrorHandled {
|
||||||
|
if !matches!(error.kind(), InterpError::UndefinedBehavior(_)) {
|
||||||
|
// Some other error happened during validation, e.g. an unsupported operation.
|
||||||
|
return report_eval_error(ecx, cid, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (error, backtrace) = error.into_parts();
|
||||||
|
backtrace.print_backtrace();
|
||||||
|
|
||||||
let bytes = ecx.print_alloc_bytes_for_diagnostics(alloc_id);
|
let bytes = ecx.print_alloc_bytes_for_diagnostics(alloc_id);
|
||||||
let (size, align, _) = ecx.get_alloc_info(alloc_id);
|
let (size, align, _) = ecx.get_alloc_info(alloc_id);
|
||||||
@ -465,6 +477,6 @@ fn report_validation_error<'tcx>(
|
|||||||
error,
|
error,
|
||||||
DUMMY_SP,
|
DUMMY_SP,
|
||||||
|| crate::const_eval::get_span_and_frames(ecx.tcx, ecx.stack()),
|
|| crate::const_eval::get_span_and_frames(ecx.tcx, ecx.stack()),
|
||||||
move |span, frames| errors::ValidationFailure { span, ub_note, frames, raw_bytes },
|
move |span, frames| errors::ValidationFailure { span, ub_note: (), frames, raw_bytes },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -425,7 +425,7 @@ pub struct ValidationFailure {
|
|||||||
#[primary_span]
|
#[primary_span]
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
#[note(const_eval_validation_failure_note)]
|
#[note(const_eval_validation_failure_note)]
|
||||||
pub ub_note: Option<()>,
|
pub ub_note: (),
|
||||||
#[subdiagnostic]
|
#[subdiagnostic]
|
||||||
pub frames: Vec<FrameNote>,
|
pub frames: Vec<FrameNote>,
|
||||||
#[subdiagnostic]
|
#[subdiagnostic]
|
||||||
@ -825,6 +825,7 @@ fn diagnostic_message(&self) -> DiagMessage {
|
|||||||
use crate::fluent_generated::*;
|
use crate::fluent_generated::*;
|
||||||
match self {
|
match self {
|
||||||
UnsupportedOpInfo::Unsupported(s) => s.clone().into(),
|
UnsupportedOpInfo::Unsupported(s) => s.clone().into(),
|
||||||
|
UnsupportedOpInfo::ExternTypeField => const_eval_extern_type_field,
|
||||||
UnsupportedOpInfo::UnsizedLocal => const_eval_unsized_local,
|
UnsupportedOpInfo::UnsizedLocal => const_eval_unsized_local,
|
||||||
UnsupportedOpInfo::OverwritePartialPointer(_) => const_eval_partial_pointer_overwrite,
|
UnsupportedOpInfo::OverwritePartialPointer(_) => const_eval_partial_pointer_overwrite,
|
||||||
UnsupportedOpInfo::ReadPartialPointer(_) => const_eval_partial_pointer_copy,
|
UnsupportedOpInfo::ReadPartialPointer(_) => const_eval_partial_pointer_copy,
|
||||||
@ -845,7 +846,10 @@ fn add_args<G: EmissionGuarantee>(self, diag: &mut Diag<'_, G>) {
|
|||||||
// `ReadPointerAsInt(Some(info))` is never printed anyway, it only serves as an error to
|
// `ReadPointerAsInt(Some(info))` is never printed anyway, it only serves as an error to
|
||||||
// be further processed by validity checking which then turns it into something nice to
|
// be further processed by validity checking which then turns it into something nice to
|
||||||
// print. So it's not worth the effort of having diagnostics that can print the `info`.
|
// print. So it's not worth the effort of having diagnostics that can print the `info`.
|
||||||
UnsizedLocal | Unsupported(_) | ReadPointerAsInt(_) => {}
|
UnsizedLocal
|
||||||
|
| UnsupportedOpInfo::ExternTypeField
|
||||||
|
| Unsupported(_)
|
||||||
|
| ReadPointerAsInt(_) => {}
|
||||||
OverwritePartialPointer(ptr) | ReadPartialPointer(ptr) => {
|
OverwritePartialPointer(ptr) | ReadPartialPointer(ptr) => {
|
||||||
diag.arg("ptr", ptr);
|
diag.arg("ptr", ptr);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
throw_ub, throw_unsup_format, InterpCx, InterpResult, MPlaceTy, Machine, MemPlaceMeta, OpTy,
|
throw_ub, throw_unsup, InterpCx, InterpResult, MPlaceTy, Machine, MemPlaceMeta, OpTy,
|
||||||
Provenance, Scalar,
|
Provenance, Scalar,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -186,8 +186,8 @@ pub fn project_field<P: Projectable<'tcx, M::Provenance>>(
|
|||||||
(base_meta, offset)
|
(base_meta, offset)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// We don't know the alignment of this field, so we cannot adjust.
|
// We cannot know the alignment of this field, so we cannot adjust.
|
||||||
throw_unsup_format!("`extern type` does not have a known offset")
|
throw_unsup!(ExternTypeField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
//! to be const-safe.
|
//! to be const-safe.
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::hash::Hash;
|
||||||
use std::num::NonZero;
|
use std::num::NonZero;
|
||||||
|
|
||||||
use either::{Left, Right};
|
use either::{Left, Right};
|
||||||
@ -17,7 +18,8 @@
|
|||||||
use rustc_middle::bug;
|
use rustc_middle::bug;
|
||||||
use rustc_middle::mir::interpret::{
|
use rustc_middle::mir::interpret::{
|
||||||
ExpectedKind, InterpError, InvalidMetaKind, Misalignment, PointerKind, Provenance,
|
ExpectedKind, InterpError, InvalidMetaKind, Misalignment, PointerKind, Provenance,
|
||||||
ValidationErrorInfo, ValidationErrorKind, ValidationErrorKind::*,
|
UnsupportedOpInfo, ValidationErrorInfo,
|
||||||
|
ValidationErrorKind::{self, *},
|
||||||
};
|
};
|
||||||
use rustc_middle::ty::layout::{LayoutOf, TyAndLayout};
|
use rustc_middle::ty::layout::{LayoutOf, TyAndLayout};
|
||||||
use rustc_middle::ty::{self, Ty};
|
use rustc_middle::ty::{self, Ty};
|
||||||
@ -26,8 +28,6 @@
|
|||||||
Abi, FieldIdx, Scalar as ScalarAbi, Size, VariantIdx, Variants, WrappingRange,
|
Abi, FieldIdx, Scalar as ScalarAbi, Size, VariantIdx, Variants, WrappingRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::hash::Hash;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
err_ub, format_interp_error, machine::AllocMap, throw_ub, AllocId, AllocKind, CheckInAllocMsg,
|
err_ub, format_interp_error, machine::AllocMap, throw_ub, AllocId, AllocKind, CheckInAllocMsg,
|
||||||
GlobalAlloc, ImmTy, Immediate, InterpCx, InterpResult, MPlaceTy, Machine, MemPlaceMeta, OpTy,
|
GlobalAlloc, ImmTy, Immediate, InterpCx, InterpResult, MPlaceTy, Machine, MemPlaceMeta, OpTy,
|
||||||
@ -1028,7 +1028,9 @@ fn validate_operand_internal(
|
|||||||
Err(err)
|
Err(err)
|
||||||
if matches!(
|
if matches!(
|
||||||
err.kind(),
|
err.kind(),
|
||||||
err_ub!(ValidationError { .. }) | InterpError::InvalidProgram(_)
|
err_ub!(ValidationError { .. })
|
||||||
|
| InterpError::InvalidProgram(_)
|
||||||
|
| InterpError::Unsupported(UnsupportedOpInfo::ExternTypeField)
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
Err(err)
|
Err(err)
|
||||||
|
@ -520,6 +520,8 @@ pub enum UnsupportedOpInfo {
|
|||||||
Unsupported(String),
|
Unsupported(String),
|
||||||
/// Unsized local variables.
|
/// Unsized local variables.
|
||||||
UnsizedLocal,
|
UnsizedLocal,
|
||||||
|
/// Extern type field with an indeterminate offset.
|
||||||
|
ExternTypeField,
|
||||||
//
|
//
|
||||||
// The variants below are only reachable from CTFE/const prop, miri will never emit them.
|
// The variants below are only reachable from CTFE/const prop, miri will never emit them.
|
||||||
//
|
//
|
||||||
|
@ -311,7 +311,9 @@ pub fn report_error<'tcx>(
|
|||||||
ResourceExhaustion(_) => "resource exhaustion",
|
ResourceExhaustion(_) => "resource exhaustion",
|
||||||
Unsupported(
|
Unsupported(
|
||||||
// We list only the ones that can actually happen.
|
// We list only the ones that can actually happen.
|
||||||
UnsupportedOpInfo::Unsupported(_) | UnsupportedOpInfo::UnsizedLocal,
|
UnsupportedOpInfo::Unsupported(_)
|
||||||
|
| UnsupportedOpInfo::UnsizedLocal
|
||||||
|
| UnsupportedOpInfo::ExternTypeField,
|
||||||
) => "unsupported operation",
|
) => "unsupported operation",
|
||||||
InvalidProgram(
|
InvalidProgram(
|
||||||
// We list only the ones that can actually happen.
|
// We list only the ones that can actually happen.
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
error: unsupported operation: `extern type` does not have a known offset
|
error: unsupported operation: `extern type` field does not have a known offset
|
||||||
--> $DIR/extern-type-field-offset.rs:LL:CC
|
--> $DIR/extern-type-field-offset.rs:LL:CC
|
||||||
|
|
|
|
||||||
LL | let _field = &x.a;
|
LL | let _field = &x.a;
|
||||||
| ^^^^ `extern type` does not have a known offset
|
| ^^^^ `extern type` field does not have a known offset
|
||||||
|
|
|
|
||||||
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
|
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
|
||||||
= note: BACKTRACE:
|
= note: BACKTRACE:
|
||||||
|
@ -2,7 +2,7 @@ error[E0080]: evaluation of constant value failed
|
|||||||
--> $DIR/issue-91827-extern-types-field-offset.rs:38:17
|
--> $DIR/issue-91827-extern-types-field-offset.rs:38:17
|
||||||
|
|
|
|
||||||
LL | let field = &x.a;
|
LL | let field = &x.a;
|
||||||
| ^^^^ `extern type` does not have a known offset
|
| ^^^^ `extern type` field does not have a known offset
|
||||||
|
|
||||||
error: aborting due to 1 previous error
|
error: aborting due to 1 previous error
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
#![feature(extern_types)]
|
||||||
|
|
||||||
|
extern {
|
||||||
|
type Opaque;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThinDst {
|
||||||
|
x: u8,
|
||||||
|
tail: Opaque,
|
||||||
|
}
|
||||||
|
|
||||||
|
const C1: &ThinDst = unsafe { std::mem::transmute(b"d".as_ptr()) };
|
||||||
|
//~^ERROR: evaluation of constant value failed
|
||||||
|
|
||||||
|
fn main() {}
|
@ -0,0 +1,9 @@
|
|||||||
|
error[E0080]: evaluation of constant value failed
|
||||||
|
--> $DIR/validation-ice-extern-type-field.rs:12:1
|
||||||
|
|
|
||||||
|
LL | const C1: &ThinDst = unsafe { std::mem::transmute(b"d".as_ptr()) };
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ `extern type` field does not have a known offset
|
||||||
|
|
||||||
|
error: aborting due to 1 previous error
|
||||||
|
|
||||||
|
For more information about this error, try `rustc --explain E0080`.
|
@ -9,15 +9,11 @@ LL | const _: *const Foo = 0 as _;
|
|||||||
| ^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
|
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
|
||||||
|
|
||||||
error[E0080]: it is undefined behavior to use this value
|
error[E0080]: evaluation of constant value failed
|
||||||
--> $DIR/stack-overflow-trait-infer-98842.rs:15:1
|
--> $DIR/stack-overflow-trait-infer-98842.rs:15:1
|
||||||
|
|
|
|
||||||
LL | const _: *const Foo = 0 as _;
|
LL | const _: *const Foo = 0 as _;
|
||||||
| ^^^^^^^^^^^^^^^^^^^ a cycle occurred during layout computation
|
| ^^^^^^^^^^^^^^^^^^^ a cycle occurred during layout computation
|
||||||
|
|
|
||||||
= note: the raw bytes of the constant (size: 4, align: 4) {
|
|
||||||
00 00 00 00 │ ....
|
|
||||||
}
|
|
||||||
|
|
||||||
error: aborting due to 2 previous errors
|
error: aborting due to 2 previous errors
|
||||||
|
|
||||||
|
@ -9,15 +9,11 @@ LL | const _: *const Foo = 0 as _;
|
|||||||
| ^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
|
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
|
||||||
|
|
||||||
error[E0080]: it is undefined behavior to use this value
|
error[E0080]: evaluation of constant value failed
|
||||||
--> $DIR/stack-overflow-trait-infer-98842.rs:15:1
|
--> $DIR/stack-overflow-trait-infer-98842.rs:15:1
|
||||||
|
|
|
|
||||||
LL | const _: *const Foo = 0 as _;
|
LL | const _: *const Foo = 0 as _;
|
||||||
| ^^^^^^^^^^^^^^^^^^^ a cycle occurred during layout computation
|
| ^^^^^^^^^^^^^^^^^^^ a cycle occurred during layout computation
|
||||||
|
|
|
||||||
= note: the raw bytes of the constant (size: 8, align: 8) {
|
|
||||||
00 00 00 00 00 00 00 00 │ ........
|
|
||||||
}
|
|
||||||
|
|
||||||
error: aborting due to 2 previous errors
|
error: aborting due to 2 previous errors
|
||||||
|
|
||||||
|
@ -13,6 +13,6 @@
|
|||||||
// and it will infinitely recurse somewhere trying to figure out the
|
// and it will infinitely recurse somewhere trying to figure out the
|
||||||
// size of this pointer (is my guess):
|
// size of this pointer (is my guess):
|
||||||
const _: *const Foo = 0 as _;
|
const _: *const Foo = 0 as _;
|
||||||
//~^ ERROR it is undefined behavior to use this value
|
//~^ ERROR evaluation of constant value failed
|
||||||
|
|
||||||
pub fn main() {}
|
pub fn main() {}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
error[E0391]: cycle detected when computing layout of `Foo`
|
|
||||||
|
|
|
||||||
= note: ...which requires computing layout of `<&'static Foo as core::ops::deref::Deref>::Target`...
|
|
||||||
= note: ...which again requires computing layout of `Foo`, completing the cycle
|
|
||||||
note: cycle used when const-evaluating + checking `_`
|
|
||||||
--> $DIR/stack-overflow-trait-infer-98842.rs:13:1
|
|
||||||
|
|
|
||||||
LL | const _: *const Foo = 0 as _;
|
|
||||||
| ^^^^^^^^^^^^^^^^^^^
|
|
||||||
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
|
|
||||||
|
|
||||||
error[E0080]: it is undefined behavior to use this value
|
|
||||||
--> $DIR/stack-overflow-trait-infer-98842.rs:13:1
|
|
||||||
|
|
|
||||||
LL | const _: *const Foo = 0 as _;
|
|
||||||
| ^^^^^^^^^^^^^^^^^^^ a cycle occurred during layout computation
|
|
||||||
|
|
|
||||||
= note: the raw bytes of the constant (size: 8, align: 8) {
|
|
||||||
00 00 00 00 00 00 00 00 │ ........
|
|
||||||
}
|
|
||||||
|
|
||||||
error: aborting due to 2 previous errors
|
|
||||||
|
|
||||||
Some errors have detailed explanations: E0080, E0391.
|
|
||||||
For more information about an error, try `rustc --explain E0080`.
|
|
Loading…
Reference in New Issue
Block a user