mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-24 03:06:33 +00:00
Berry apply fixes from upstream (#18470)
* Berry apply fixes from upstream * Fix
This commit is contained in:
parent
a6aa8924ea
commit
01ba3d28cd
@ -75,7 +75,7 @@ be_extern_native_module(matter);
|
|||||||
/* user-defined modules declare end */
|
/* user-defined modules declare end */
|
||||||
|
|
||||||
/* module list declaration */
|
/* module list declaration */
|
||||||
BERRY_LOCAL const bntvmodule* const be_module_table[] = {
|
BERRY_LOCAL const bntvmodule_t* const be_module_table[] = {
|
||||||
/* default modules register */
|
/* default modules register */
|
||||||
#if BE_USE_STRING_MODULE
|
#if BE_USE_STRING_MODULE
|
||||||
&be_native_module(string),
|
&be_native_module(string),
|
||||||
|
@ -196,7 +196,7 @@ const bvector _name = { \
|
|||||||
}
|
}
|
||||||
|
|
||||||
#define be_define_const_native_module(_module) \
|
#define be_define_const_native_module(_module) \
|
||||||
const bntvmodule be_native_module(_module) = { \
|
const bntvmodule_t be_native_module(_module) = { \
|
||||||
.name = #_module, \
|
.name = #_module, \
|
||||||
.attrs = NULL, \
|
.attrs = NULL, \
|
||||||
.size = 0, \
|
.size = 0, \
|
||||||
@ -422,7 +422,7 @@ const bvector _name = { \
|
|||||||
}
|
}
|
||||||
|
|
||||||
#define be_define_const_native_module(_module) \
|
#define be_define_const_native_module(_module) \
|
||||||
const bntvmodule be_native_module_##_module = { \
|
const bntvmodule_t be_native_module_##_module = { \
|
||||||
#_module, \
|
#_module, \
|
||||||
0, 0, \
|
0, 0, \
|
||||||
(bmodule*)&(m_lib##_module) \
|
(bmodule*)&(m_lib##_module) \
|
||||||
|
@ -49,7 +49,7 @@ static const char* opc2str(bopcode op)
|
|||||||
|
|
||||||
void be_print_inst(binstruction ins, int pc, void* fout)
|
void be_print_inst(binstruction ins, int pc, void* fout)
|
||||||
{
|
{
|
||||||
char __lbuf[INST_BUF_SIZE];
|
char __lbuf[INST_BUF_SIZE + 1], __lbuf_tmp[INST_BUF_SIZE];
|
||||||
bopcode op = IGET_OP(ins);
|
bopcode op = IGET_OP(ins);
|
||||||
|
|
||||||
logbuf(" %.4X ", pc);
|
logbuf(" %.4X ", pc);
|
||||||
@ -137,7 +137,8 @@ void be_print_inst(binstruction ins, int pc, void* fout)
|
|||||||
logbuf("%s", opc2str(op));
|
logbuf("%s", opc2str(op));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
logbuf("%s\n", __lbuf);
|
memcpy(__lbuf_tmp, __lbuf, strlen(__lbuf));
|
||||||
|
logbuf("%s\n", __lbuf_tmp);
|
||||||
if (fout) {
|
if (fout) {
|
||||||
be_fwrite(fout, __lbuf, strlen(__lbuf));
|
be_fwrite(fout, __lbuf, strlen(__lbuf));
|
||||||
} else {
|
} else {
|
||||||
@ -253,7 +254,7 @@ void be_tracestack(bvm *vm)
|
|||||||
|
|
||||||
static void hook_callnative(bvm *vm, int mask)
|
static void hook_callnative(bvm *vm, int mask)
|
||||||
{
|
{
|
||||||
bhookinfo info;
|
bhookinfo_t info;
|
||||||
int top = be_top(vm);
|
int top = be_top(vm);
|
||||||
bcallframe *cf = vm->cf;
|
bcallframe *cf = vm->cf;
|
||||||
bclosure *cl = var_toobj(cf->func);
|
bclosure *cl = var_toobj(cf->func);
|
||||||
|
@ -202,6 +202,7 @@ static const char* parser_string(bvm *vm, const char *json)
|
|||||||
|
|
||||||
static const char* parser_field(bvm *vm, const char *json)
|
static const char* parser_field(bvm *vm, const char *json)
|
||||||
{
|
{
|
||||||
|
be_stack_require(vm, 2 + BE_STACK_FREE_MIN);
|
||||||
if (json && *json == '"') {
|
if (json && *json == '"') {
|
||||||
json = parser_string(vm, json);
|
json = parser_string(vm, json);
|
||||||
if (json) {
|
if (json) {
|
||||||
@ -368,6 +369,11 @@ static const char* parser_number(bvm *vm, const char *json)
|
|||||||
static const char* parser_value(bvm *vm, const char *json)
|
static const char* parser_value(bvm *vm, const char *json)
|
||||||
{
|
{
|
||||||
json = skip_space(json);
|
json = skip_space(json);
|
||||||
|
/*
|
||||||
|
Each value will push at least one thig to the stack, so we must ensure it's big enough.
|
||||||
|
We need to take special care to extend the stack in values which have variable length (arrays and objects)
|
||||||
|
*/
|
||||||
|
be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
switch (*json) {
|
switch (*json) {
|
||||||
case '{': /* object */
|
case '{': /* object */
|
||||||
return parser_object(vm, json);
|
return parser_object(vm, json);
|
||||||
@ -404,6 +410,7 @@ static int m_json_load(bvm *vm)
|
|||||||
static void make_indent(bvm *vm, int stridx, int indent)
|
static void make_indent(bvm *vm, int stridx, int indent)
|
||||||
{
|
{
|
||||||
if (indent) {
|
if (indent) {
|
||||||
|
be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
char buf[MAX_INDENT * INDENT_WIDTH + 1];
|
char buf[MAX_INDENT * INDENT_WIDTH + 1];
|
||||||
indent = (indent < MAX_INDENT ? indent : MAX_INDENT) * INDENT_WIDTH;
|
indent = (indent < MAX_INDENT ? indent : MAX_INDENT) * INDENT_WIDTH;
|
||||||
memset(buf, INDENT_CHAR, indent);
|
memset(buf, INDENT_CHAR, indent);
|
||||||
@ -417,6 +424,7 @@ static void make_indent(bvm *vm, int stridx, int indent)
|
|||||||
|
|
||||||
void string_dump(bvm *vm, int index)
|
void string_dump(bvm *vm, int index)
|
||||||
{
|
{
|
||||||
|
be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
be_tostring(vm, index); /* convert value to string */
|
be_tostring(vm, index); /* convert value to string */
|
||||||
be_toescape(vm, index, 'u');
|
be_toescape(vm, index, 'u');
|
||||||
be_pushvalue(vm, index);
|
be_pushvalue(vm, index);
|
||||||
@ -424,11 +432,14 @@ void string_dump(bvm *vm, int index)
|
|||||||
|
|
||||||
static void object_dump(bvm *vm, int *indent, int idx, int fmt)
|
static void object_dump(bvm *vm, int *indent, int idx, int fmt)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
be_stack_require(vm, 3 + BE_STACK_FREE_MIN); /* 3 pushes outside the loop */
|
||||||
be_getmember(vm, idx, ".p");
|
be_getmember(vm, idx, ".p");
|
||||||
be_pushstring(vm, fmt ? "{\n" : "{");
|
be_pushstring(vm, fmt ? "{\n" : "{");
|
||||||
be_pushiter(vm, -2); /* map iterator use 1 register */
|
be_pushiter(vm, -2); /* map iterator use 1 register */
|
||||||
*indent += fmt;
|
*indent += fmt;
|
||||||
while (be_iter_hasnext(vm, -3)) {
|
while (be_iter_hasnext(vm, -3)) {
|
||||||
|
be_stack_require(vm, 3 + BE_STACK_FREE_MIN); /* 3 pushes inside the loop */
|
||||||
make_indent(vm, -2, fmt ? *indent : 0);
|
make_indent(vm, -2, fmt ? *indent : 0);
|
||||||
be_iter_next(vm, -3);
|
be_iter_next(vm, -3);
|
||||||
/* key.tostring() */
|
/* key.tostring() */
|
||||||
@ -463,6 +474,7 @@ static void object_dump(bvm *vm, int *indent, int idx, int fmt)
|
|||||||
|
|
||||||
static void array_dump(bvm *vm, int *indent, int idx, int fmt)
|
static void array_dump(bvm *vm, int *indent, int idx, int fmt)
|
||||||
{
|
{
|
||||||
|
be_stack_require(vm, 3 + BE_STACK_FREE_MIN);
|
||||||
be_getmember(vm, idx, ".p");
|
be_getmember(vm, idx, ".p");
|
||||||
be_pushstring(vm, fmt ? "[\n" : "[");
|
be_pushstring(vm, fmt ? "[\n" : "[");
|
||||||
be_pushiter(vm, -2);
|
be_pushiter(vm, -2);
|
||||||
@ -473,6 +485,7 @@ static void array_dump(bvm *vm, int *indent, int idx, int fmt)
|
|||||||
value_dump(vm, indent, -1, fmt);
|
value_dump(vm, indent, -1, fmt);
|
||||||
be_strconcat(vm, -4);
|
be_strconcat(vm, -4);
|
||||||
be_pop(vm, 2);
|
be_pop(vm, 2);
|
||||||
|
be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
if (be_iter_hasnext(vm, -3)) {
|
if (be_iter_hasnext(vm, -3)) {
|
||||||
be_pushstring(vm, fmt ? ",\n" : ",");
|
be_pushstring(vm, fmt ? ",\n" : ",");
|
||||||
be_strconcat(vm, -3);
|
be_strconcat(vm, -3);
|
||||||
@ -494,13 +507,16 @@ static void array_dump(bvm *vm, int *indent, int idx, int fmt)
|
|||||||
|
|
||||||
static void value_dump(bvm *vm, int *indent, int idx, int fmt)
|
static void value_dump(bvm *vm, int *indent, int idx, int fmt)
|
||||||
{
|
{
|
||||||
|
// be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
if (is_object(vm, "map", idx)) { /* convert to json object */
|
if (is_object(vm, "map", idx)) { /* convert to json object */
|
||||||
object_dump(vm, indent, idx, fmt);
|
object_dump(vm, indent, idx, fmt);
|
||||||
} else if (is_object(vm, "list", idx)) { /* convert to json array */
|
} else if (is_object(vm, "list", idx)) { /* convert to json array */
|
||||||
array_dump(vm, indent, idx, fmt);
|
array_dump(vm, indent, idx, fmt);
|
||||||
} else if (be_isnil(vm, idx)) { /* convert to json null */
|
} else if (be_isnil(vm, idx)) { /* convert to json null */
|
||||||
|
be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
be_pushstring(vm, "null");
|
be_pushstring(vm, "null");
|
||||||
} else if (be_isnumber(vm, idx) || be_isbool(vm, idx)) { /* convert to json number and boolean */
|
} else if (be_isnumber(vm, idx) || be_isbool(vm, idx)) { /* convert to json number and boolean */
|
||||||
|
be_stack_require(vm, 1 + BE_STACK_FREE_MIN);
|
||||||
be_tostring(vm, idx);
|
be_tostring(vm, idx);
|
||||||
be_pushvalue(vm, idx); /* push to top */
|
be_pushvalue(vm, idx); /* push to top */
|
||||||
} else { /* convert to string */
|
} else { /* convert to string */
|
||||||
|
@ -36,14 +36,14 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern BERRY_LOCAL const bntvmodule* const be_module_table[];
|
extern BERRY_LOCAL const bntvmodule_t* const be_module_table[];
|
||||||
|
|
||||||
static bmodule* native_module(bvm *vm, const bntvmodule *nm, bvalue *dst);
|
static bmodule* native_module(bvm *vm, const bntvmodule_t *nm, bvalue *dst);
|
||||||
|
|
||||||
static const bntvmodule* find_native(bstring *path)
|
static const bntvmodule_t* find_native(bstring *path)
|
||||||
{
|
{
|
||||||
const bntvmodule *module;
|
const bntvmodule_t *module;
|
||||||
const bntvmodule* const *node = be_module_table;
|
const bntvmodule_t* const *node = be_module_table;
|
||||||
for (; (module = *node) != NULL; ++node) {
|
for (; (module = *node) != NULL; ++node) {
|
||||||
if (!strcmp(module->name, str(path))) {
|
if (!strcmp(module->name, str(path))) {
|
||||||
return module;
|
return module;
|
||||||
@ -52,11 +52,11 @@ static const bntvmodule* find_native(bstring *path)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void insert_attrs(bvm *vm, bmap *table, const bntvmodule *nm)
|
static void insert_attrs(bvm *vm, bmap *table, const bntvmodule_t *nm)
|
||||||
{
|
{
|
||||||
size_t i;
|
size_t i;
|
||||||
for (i = 0; i < nm->size; ++i) {
|
for (i = 0; i < nm->size; ++i) {
|
||||||
const bntvmodobj *node = nm->attrs + i;
|
const bntvmodobj_t *node = nm->attrs + i;
|
||||||
bstring *name = be_newstr(vm, node->name);
|
bstring *name = be_newstr(vm, node->name);
|
||||||
bvalue *v = be_map_insertstr(vm, table, name, NULL);
|
bvalue *v = be_map_insertstr(vm, table, name, NULL);
|
||||||
be_assert(node->type <= BE_CMODULE);
|
be_assert(node->type <= BE_CMODULE);
|
||||||
@ -88,7 +88,7 @@ static void insert_attrs(bvm *vm, bmap *table, const bntvmodule *nm)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bmodule* new_module(bvm *vm, const bntvmodule *nm)
|
static bmodule* new_module(bvm *vm, const bntvmodule_t *nm)
|
||||||
{
|
{
|
||||||
bgcobject *gco = be_gcnew(vm, BE_MODULE, bmodule);
|
bgcobject *gco = be_gcnew(vm, BE_MODULE, bmodule);
|
||||||
bmodule *obj = cast_module(gco);
|
bmodule *obj = cast_module(gco);
|
||||||
@ -105,7 +105,7 @@ static bmodule* new_module(bvm *vm, const bntvmodule *nm)
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bmodule* native_module(bvm *vm, const bntvmodule *nm, bvalue *dst)
|
static bmodule* native_module(bvm *vm, const bntvmodule_t *nm, bvalue *dst)
|
||||||
{
|
{
|
||||||
if (nm) {
|
if (nm) {
|
||||||
bmodule *obj;
|
bmodule *obj;
|
||||||
@ -221,7 +221,7 @@ static int load_package(bvm *vm, bstring *path)
|
|||||||
|
|
||||||
static int load_native(bvm *vm, bstring *path)
|
static int load_native(bvm *vm, bstring *path)
|
||||||
{
|
{
|
||||||
const bntvmodule *nm = find_native(path);
|
const bntvmodule_t *nm = find_native(path);
|
||||||
bmodule *mod = native_module(vm, nm, NULL);
|
bmodule *mod = native_module(vm, nm, NULL);
|
||||||
if (mod != NULL) {
|
if (mod != NULL) {
|
||||||
/* the pointer vm->top may be changed */
|
/* the pointer vm->top may be changed */
|
||||||
|
@ -16,7 +16,7 @@ typedef struct bmodule {
|
|||||||
bcommon_header;
|
bcommon_header;
|
||||||
bmap *table;
|
bmap *table;
|
||||||
union infodata {
|
union infodata {
|
||||||
const bntvmodule *native;
|
const bntvmodule_t *native;
|
||||||
const char *name;
|
const char *name;
|
||||||
const bstring *sname;
|
const bstring *sname;
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -109,31 +109,54 @@
|
|||||||
} \
|
} \
|
||||||
return res
|
return res
|
||||||
|
|
||||||
#define relop_rule(op) \
|
/* when running on ESP32 in IRAM, there is a bug in early chip revision */
|
||||||
bbool res; \
|
#ifdef ESP32
|
||||||
if (var_isint(a) && var_isint(b)) { \
|
#define relop_rule(op) \
|
||||||
res = ibinop(op, a, b); \
|
bbool res; \
|
||||||
} else if (var_isnumber(a) && var_isnumber(b)) { \
|
if (var_isint(a) && var_isint(b)) { \
|
||||||
/* res = var2real(a) op var2real(b); */ \
|
res = ibinop(op, a, b); \
|
||||||
union bvaldata x, y; /* TASMOTA workaround for ESP32 rev0 bug */ \
|
} else if (var_isnumber(a) && var_isnumber(b)) { \
|
||||||
x.i = a->v.i;\
|
/* res = var2real(a) op var2real(b); */ \
|
||||||
if (var_isint(a)) { x.r = (breal) x.i; }\
|
union bvaldata x, y; /* TASMOTA workaround for ESP32 rev0 bug */ \
|
||||||
y.i = b->v.i;\
|
x.i = a->v.i;\
|
||||||
if (var_isint(b)) { y.r = (breal) y.i; }\
|
if (var_isint(a)) { x.r = (breal) x.i; }\
|
||||||
res = x.r op y.r; \
|
y.i = b->v.i;\
|
||||||
} else if (var_isstr(a) && var_isstr(b)) { \
|
if (var_isint(b)) { y.r = (breal) y.i; }\
|
||||||
bstring *s1 = var_tostr(a), *s2 = var_tostr(b); \
|
res = x.r op y.r; \
|
||||||
res = be_strcmp(s1, s2) op 0; \
|
} else if (var_isstr(a) && var_isstr(b)) { \
|
||||||
} else if (var_isinstance(a)) { \
|
bstring *s1 = var_tostr(a), *s2 = var_tostr(b); \
|
||||||
binstance *obj = var_toobj(a); \
|
res = be_strcmp(s1, s2) op 0; \
|
||||||
object_binop(vm, #op, *a, *b); \
|
} else if (var_isinstance(a)) { \
|
||||||
check_bool(vm, obj, #op); \
|
binstance *obj = var_toobj(a); \
|
||||||
res = var_tobool(vm->top); \
|
object_binop(vm, #op, *a, *b); \
|
||||||
} else { \
|
check_bool(vm, obj, #op); \
|
||||||
binop_error(vm, #op, a, b); \
|
res = var_tobool(vm->top); \
|
||||||
res = bfalse; /* will not be executed */ \
|
} else { \
|
||||||
} \
|
binop_error(vm, #op, a, b); \
|
||||||
return res
|
res = bfalse; /* will not be executed */ \
|
||||||
|
} \
|
||||||
|
return res
|
||||||
|
#else // ESP32
|
||||||
|
#define relop_rule(op) \
|
||||||
|
bbool res; \
|
||||||
|
if (var_isint(a) && var_isint(b)) { \
|
||||||
|
res = ibinop(op, a, b); \
|
||||||
|
} else if (var_isnumber(a) && var_isnumber(b)) { \
|
||||||
|
res = var2real(a) op var2real(b); \
|
||||||
|
} else if (var_isstr(a) && var_isstr(b)) { \
|
||||||
|
bstring *s1 = var_tostr(a), *s2 = var_tostr(b); \
|
||||||
|
res = be_strcmp(s1, s2) op 0; \
|
||||||
|
} else if (var_isinstance(a)) { \
|
||||||
|
binstance *obj = var_toobj(a); \
|
||||||
|
object_binop(vm, #op, *a, *b); \
|
||||||
|
check_bool(vm, obj, #op); \
|
||||||
|
res = var_tobool(vm->top); \
|
||||||
|
} else { \
|
||||||
|
binop_error(vm, #op, a, b); \
|
||||||
|
res = bfalse; /* will not be executed */ \
|
||||||
|
} \
|
||||||
|
return res
|
||||||
|
#endif // ESP32
|
||||||
|
|
||||||
#define bitwise_block(op) \
|
#define bitwise_block(op) \
|
||||||
bvalue *dst = RA(), *a = RKB(), *b = RKC(); \
|
bvalue *dst = RA(), *a = RKB(), *b = RKC(); \
|
||||||
@ -264,7 +287,6 @@ static bbool obj2bool(bvm *vm, bvalue *var)
|
|||||||
binstance *obj = var_toobj(var);
|
binstance *obj = var_toobj(var);
|
||||||
bstring *tobool = str_literal(vm, "tobool");
|
bstring *tobool = str_literal(vm, "tobool");
|
||||||
/* get operator method */
|
/* get operator method */
|
||||||
// TODO what if `tobool` is static
|
|
||||||
int type = be_instance_member(vm, obj, tobool, vm->top);
|
int type = be_instance_member(vm, obj, tobool, vm->top);
|
||||||
if (type != BE_NONE && type != BE_NIL) {
|
if (type != BE_NONE && type != BE_NIL) {
|
||||||
vm->top[1] = *var; /* move self to argv[0] */
|
vm->top[1] = *var; /* move self to argv[0] */
|
||||||
@ -357,7 +379,6 @@ static bbool object_eqop(bvm *vm,
|
|||||||
bbool isself = var_isinstance(b) && o == var_toobj(b);
|
bbool isself = var_isinstance(b) && o == var_toobj(b);
|
||||||
/* first, try to call the overloaded operator of the object */
|
/* first, try to call the overloaded operator of the object */
|
||||||
int type = be_instance_member(vm, o, be_newstr(vm, op), vm->top);
|
int type = be_instance_member(vm, o, be_newstr(vm, op), vm->top);
|
||||||
// TODO check that method is not static
|
|
||||||
if (basetype(type) == BE_FUNCTION) { /* call method */
|
if (basetype(type) == BE_FUNCTION) { /* call method */
|
||||||
bvalue *top = vm->top;
|
bvalue *top = vm->top;
|
||||||
top[1] = self; /* move self to argv[0] */
|
top[1] = self; /* move self to argv[0] */
|
||||||
@ -615,13 +636,17 @@ newframe: /* a new call frame */
|
|||||||
if (var_isint(a) && var_isint(b)) {
|
if (var_isint(a) && var_isint(b)) {
|
||||||
var_setint(dst, ibinop(+, a, b));
|
var_setint(dst, ibinop(+, a, b));
|
||||||
} else if (var_isnumber(a) && var_isnumber(b)) {
|
} else if (var_isnumber(a) && var_isnumber(b)) {
|
||||||
|
#ifdef ESP32 /* when running on ESP32 in IRAM, there is a bug in early chip revision */
|
||||||
union bvaldata x, y; // TASMOTA workaround for ESP32 rev0 bug
|
union bvaldata x, y; // TASMOTA workaround for ESP32 rev0 bug
|
||||||
x.i = a->v.i;
|
x.i = a->v.i;
|
||||||
if (var_isint(a)) { x.r = (breal) x.i; }
|
if (var_isint(a)) { x.r = (breal) x.i; }
|
||||||
y.i = b->v.i;
|
y.i = b->v.i;
|
||||||
if (var_isint(b)) { y.r = (breal) y.i; }
|
if (var_isint(b)) { y.r = (breal) y.i; }
|
||||||
// breal x = var2real(a), y = var2real(b);
|
|
||||||
var_setreal(dst, x.r + y.r);
|
var_setreal(dst, x.r + y.r);
|
||||||
|
#else // ESP32
|
||||||
|
breal x = var2real(a), y = var2real(b);
|
||||||
|
var_setreal(dst, x + y);
|
||||||
|
#endif // ESP32
|
||||||
} else if (var_isstr(a) && var_isstr(b)) { /* strcat */
|
} else if (var_isstr(a) && var_isstr(b)) { /* strcat */
|
||||||
bstring *s = be_strcat(vm, var_tostr(a), var_tostr(b));
|
bstring *s = be_strcat(vm, var_tostr(a), var_tostr(b));
|
||||||
reg = vm->reg;
|
reg = vm->reg;
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user