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

fallback to default toString() implementation #11

Closed
drsm opened this issue Apr 22, 2018 · 16 comments
Closed

fallback to default toString() implementation #11

drsm opened this issue Apr 22, 2018 · 16 comments
Assignees

Comments

@drsm
Copy link
Contributor

drsm commented Apr 22, 2018

in console.* & interactive shell

>> var x = Object.create(null);
undefined
>> x
TypeError: cannot evaluate an object's value
    at main (native)

>> console.log(x)
TypeError: cannot evaluate an object's value
    at console.log (native)
    at main (native)
    at main (native)

>> console.log(Object.prototype.toString(x))
[object Object]
undefined
>> Object.prototype.toString(x)
[object Object]

@drsm
Copy link
Contributor Author

drsm commented Apr 22, 2018

or maybe the better solution is to allow to override the default dumper routine used there. something like

if (njs) {
  njs.dump = function(arg) {
    // whatever
  }
}

@drsm
Copy link
Contributor Author

drsm commented Apr 24, 2018

some more on this:

>> [[1,1],[2,2],[3,3]]
1,1,2,2,3,3

@xeioex
Copy link
Contributor

xeioex commented Apr 24, 2018

@drsm Is it a good idea to use JSON.stringify(arg, undefined,1) as the default dumper function? It would solve the null proto issue as well.

something like this:

>>console.log([{a:1,b:{c:2}},1])
[
 {
  "a": 1,
  "b": {
   "c": 2
  }
 },
 1
]

@drsm
Copy link
Contributor Author

drsm commented Apr 24, 2018

@xeioex yes, i think this is good solution, but, please note that empty array items would incorrectly rendered here:

console.log(JSON.stringify([{a:1,b:{c:[,]}},1]))
[{"a":1,"b":{"c":[null]}},1]

and this will break tests, so some extension (internal) to JSON.stringify is also needed, i think

@xeioex xeioex self-assigned this Apr 24, 2018
@xeioex
Copy link
Contributor

xeioex commented Jul 13, 2018

Hi @drsm

Please, take a look at the following patch.

# HG changeset patch
# User Dmitry Volyntsev <xeioex@nginx.com>
# Date 1531505192 -10800
#      Fri Jul 13 21:06:32 2018 +0300
# Node ID d6c448e193cf6cd444bcfba42c761543df25491d
# Parent  00762256c67a9a899b573889fbc3d0bf9393784e
Added pretty string representation for values.

Interactive shell: dumping the pretty string representation of
the last expression.

>> [1, new Number(2), {a: new String('αβZγ')}, true, console.log,
    /^undef$/m, new Date(0)]
[
 1,
 [Number: 2],
 {
  a: [String: 'αβZγ']
 },
 true,
 [Function: native],
 /^undef$/m,
 1970-01-01T00:00:00.000Z
]

njs.dump(value, indent):
    Returns pretty string representation of a value.
    value - a value of any type.
    indent - number.
        A number of space characters per indentation level.

    njs.dump({a:[]}) -> '{a:[]}'
    njs.dump({a:[]}, 1) -> '{\n a: [\n  \n ]\n}'

This fixes #11 issue on GitHub.

diff --git a/njs/njs.h b/njs/njs.h
--- a/njs/njs.h
+++ b/njs/njs.h
@@ -217,6 +217,8 @@ NXT_EXPORT nxt_int_t njs_value_is_string
 NXT_EXPORT nxt_int_t njs_value_is_object(njs_value_t *value);
 NXT_EXPORT nxt_int_t njs_value_is_function(njs_value_t *value);
 
+NXT_EXPORT njs_ret_t njs_vm_value_dump(njs_vm_t *vm, nxt_str_t *retval,
+    const njs_value_t *value, nxt_uint_t indent);
 NXT_EXPORT njs_value_t *njs_vm_object_prop(njs_vm_t *vm, njs_value_t *value,
     const nxt_str_t *key);
 
diff --git a/njs/njs_builtin.c b/njs/njs_builtin.c
--- a/njs/njs_builtin.c
+++ b/njs/njs_builtin.c
@@ -1069,6 +1069,28 @@ njs_builtin_match_native_function(njs_vm
 }
 
 
+static njs_ret_t
+njs_dump_value(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
+    njs_index_t unused)
+{
+    nxt_str_t          str;
+    nxt_uint_t         n;
+    const njs_value_t  *value, *indent;
+
+    value = njs_arg(args, nargs, 1);
+    indent = njs_arg(args, nargs, 2);
+
+    n = indent->data.u.number;
+    n = nxt_min(n, 5);
+
+    if (njs_vm_value_dump(vm, &str, value, n) != NXT_OK) {
+        return NXT_ERROR;
+    }
+
+    return njs_string_new(vm, &vm->retval, str.start, str.length, 0);
+}
+
+
 static const njs_object_prop_t  njs_njs_object_properties[] =
 {
     {
@@ -1076,6 +1098,13 @@ static const njs_object_prop_t  njs_njs_
         .name = njs_string("version"),
         .value = njs_string(NJS_VERSION),
     },
+
+    {
+        .type = NJS_METHOD,
+        .name = njs_string("dump"),
+        .value = njs_native_function(njs_dump_value, 0,
+                                    NJS_SKIP_ARG, NJS_SKIP_ARG, NJS_NUMBER_ARG),
+    },
 };
 
 
diff --git a/njs/njs_date.c b/njs/njs_date.c
--- a/njs/njs_date.c
+++ b/njs/njs_date.c
@@ -1017,6 +1017,13 @@ static njs_ret_t
 njs_date_prototype_to_iso_string(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused)
 {
+    return njs_date_to_string(vm, &vm->retval, &args[0]);
+}
+
+
+njs_ret_t
+njs_date_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *date)
+{
     int32_t    year;
     double     time;
     size_t     size;
@@ -1024,7 +1031,7 @@ njs_date_prototype_to_iso_string(njs_vm_
     u_char     buf[NJS_ISO_DATE_TIME_LEN];
     struct tm  tm;
 
-    time = args[0].data.u.date->time;
+    time = date->data.u.date->time;
 
     if (!isnan(time)) {
         clock = time / 1000;
@@ -1040,7 +1047,7 @@ njs_date_prototype_to_iso_string(njs_vm_
                         tm.tm_hour, tm.tm_min, tm.tm_sec,
                         (int) ((int64_t) time % 1000));
 
-        return njs_string_new(vm, &vm->retval, buf, size, size);
+        return njs_string_new(vm, retval, buf, size, size);
     }
 
     njs_range_error(vm, NULL);
diff --git a/njs/njs_date.h b/njs/njs_date.h
--- a/njs/njs_date.h
+++ b/njs/njs_date.h
@@ -11,6 +11,9 @@
 njs_ret_t njs_date_constructor(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
+njs_ret_t njs_date_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *date);
+
 
 extern const njs_object_init_t  njs_date_constructor_init;
 extern const njs_object_init_t  njs_date_prototype_init;
diff --git a/njs/njs_error.c b/njs/njs_error.c
--- a/njs/njs_error.c
+++ b/njs/njs_error.c
@@ -586,6 +586,18 @@ static njs_ret_t
 njs_error_prototype_to_string(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
     njs_index_t unused)
 {
+    if (nargs < 1 || !njs_is_object(&args[0])) {
+        njs_type_error(vm, "'this' argument is not an object");
+        return NXT_ERROR;
+    }
+
+    return njs_error_to_string(vm, &vm->retval, &args[0]);
+}
+
+
+njs_ret_t
+njs_error_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *error)
+{
     size_t              size;
     u_char              *p;
     nxt_str_t           name, message;
@@ -595,16 +607,11 @@ njs_error_prototype_to_string(njs_vm_t *
 
     static const njs_value_t  default_name = njs_string("Error");
 
-    if (nargs < 1 || !njs_is_object(&args[0])) {
-        njs_type_error(vm, "'this' argument is not an object");
-        return NXT_ERROR;
-    }
-
     lhq.key_hash = NJS_NAME_HASH;
     lhq.key = nxt_string_value("name");
     lhq.proto = &njs_object_hash_proto;
 
-    prop = njs_object_property(vm, args[0].data.u.object, &lhq);
+    prop = njs_object_property(vm, error->data.u.object, &lhq);
 
     if (prop != NULL) {
         name_value = &prop->value;
@@ -618,7 +625,7 @@ njs_error_prototype_to_string(njs_vm_t *
     lhq.key_hash = NJS_MESSAGE_HASH;
     lhq.key = nxt_string_value("message");
 
-    prop = njs_object_property(vm, args[0].data.u.object, &lhq);
+    prop = njs_object_property(vm, error->data.u.object, &lhq);
 
     if (prop != NULL) {
         message_value = &prop->value;
@@ -630,18 +637,18 @@ njs_error_prototype_to_string(njs_vm_t *
     njs_string_get(message_value, &message);
 
     if (name.length == 0) {
-        vm->retval = *message_value;
+        *retval = *message_value;
         return NJS_OK;
     }
 
     if (message.length == 0) {
-        vm->retval = *name_value;
+        *retval = *name_value;
         return NJS_OK;
     }
 
     size = name.length + message.length + 2;
 
-    p = njs_string_alloc(vm, &vm->retval, size, size);
+    p = njs_string_alloc(vm, retval, size, size);
 
     if (nxt_fast_path(p != NULL)) {
         p = nxt_cpymem(p, name.start, name.length);
@@ -653,6 +660,7 @@ njs_error_prototype_to_string(njs_vm_t *
     }
 
     njs_memory_error(vm);
+
     return NJS_ERROR;
 }
 
diff --git a/njs/njs_error.h b/njs/njs_error.h
--- a/njs/njs_error.h
+++ b/njs/njs_error.h
@@ -53,6 +53,8 @@ njs_ret_t njs_uri_error_constructor(njs_
 njs_ret_t njs_memory_error_constructor(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
+njs_ret_t njs_error_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *error);
 
 extern const njs_object_init_t  njs_error_constructor_init;
 extern const njs_object_init_t  njs_eval_error_constructor_init;
diff --git a/njs/njs_json.c b/njs/njs_json.c
--- a/njs/njs_json.c
+++ b/njs/njs_json.c
@@ -5,6 +5,9 @@
  */
 
 #include <njs_core.h>
+#include <njs_json.h>
+#include <njs_date.h>
+#include <njs_regexp.h>
 #include <stdio.h>
 #include <string.h>
 
@@ -127,16 +130,16 @@ static njs_ret_t njs_json_stringify_repl
 static njs_ret_t njs_json_stringify_array(njs_vm_t *vm,
     njs_json_stringify_t *stringify);
 static njs_json_state_t *njs_json_push_stringify_state(njs_vm_t *vm,
-    njs_json_stringify_t *stringify, njs_value_t *value);
+    njs_json_stringify_t *stringify, const njs_value_t *value);
 static njs_json_state_t *njs_json_pop_stringify_state(
     njs_json_stringify_t *stringify);
 
 static nxt_int_t njs_json_append_value(njs_json_stringify_t *stringify,
-    njs_value_t *value);
+    const njs_value_t *value);
 static nxt_int_t njs_json_append_string(njs_json_stringify_t *stringify,
-    njs_value_t *value);
+    const njs_value_t *value, char quote);
 static nxt_int_t njs_json_append_number(njs_json_stringify_t *stringify,
-    njs_value_t *value);
+    const njs_value_t *value);
 
 static njs_value_t *njs_json_wrap_value(njs_vm_t *vm, njs_value_t *value);
 
@@ -1162,7 +1165,7 @@ njs_json_parse_exception(njs_json_parse_
     }                                                                         \
                                                                               \
     state->written = 1;                                                       \
-    njs_json_append_string(stringify, key);                                   \
+    njs_json_append_string(stringify, key, '\"');                             \
     njs_json_stringify_append(":", 1);                                        \
     if (stringify->space.length != 0) {                                       \
         njs_json_stringify_append(" ", 1);                                    \
@@ -1174,7 +1177,7 @@ njs_json_parse_exception(njs_json_parse_
     ret = njs_json_append_value(stringify, value);                            \
     if (nxt_slow_path(ret != NXT_OK)) {                                       \
         if (ret == NXT_DECLINED) {                                            \
-            return NXT_ERROR;                                                 \
+            goto exception;                                                   \
         }                                                                     \
                                                                               \
         goto memory_error;                                                    \
@@ -1426,7 +1429,10 @@ memory_error:
 
     njs_memory_error(vm);
 
+exception:
+
     return NXT_ERROR;
+
 }
 
 
@@ -1621,7 +1627,7 @@ njs_json_stringify_array(njs_vm_t *vm, n
 
 static njs_json_state_t *
 njs_json_push_stringify_state(njs_vm_t *vm, njs_json_stringify_t *stringify,
-    njs_value_t *value)
+    const njs_value_t *value)
 {
     njs_json_state_t  *state;
 
@@ -1681,7 +1687,7 @@ njs_json_pop_stringify_state(njs_json_st
 
 
 static nxt_int_t
-njs_json_append_value(njs_json_stringify_t *stringify, njs_value_t *value)
+njs_json_append_value(njs_json_stringify_t *stringify, const njs_value_t *value)
 {
     switch (value->type) {
     case NJS_OBJECT_STRING:
@@ -1689,7 +1695,7 @@ njs_json_append_value(njs_json_stringify
         /* Fall through. */
 
     case NJS_STRING:
-        return njs_json_append_string(stringify, value);
+        return njs_json_append_string(stringify, value, '\"');
 
     case NJS_OBJECT_NUMBER:
         value = &value->data.u.object_value->value;
@@ -1724,7 +1730,8 @@ njs_json_append_value(njs_json_stringify
 
 
 static nxt_int_t
-njs_json_append_string(njs_json_stringify_t *stringify, njs_value_t *value)
+njs_json_append_string(njs_json_stringify_t *stringify,
+    const njs_value_t *value, char quote)
 {
     u_char             c, *dst, *dst_end;
     size_t             length;
@@ -1734,7 +1741,7 @@ njs_json_append_string(njs_json_stringif
     static char   hex2char[16] = { '0', '1', '2', '3', '4', '5', '6', '7',
                                    '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
 
-    (void) njs_string_prop(&str, value);
+    (void) njs_string_prop(&str, (njs_value_t *) value);
 
     p = str.start;
     end = p + str.size;
@@ -1747,7 +1754,7 @@ njs_json_append_string(njs_json_stringif
 
     dst_end = dst + 64;
 
-    *dst++ = '\"';
+    *dst++ = quote;
 
     while (p < end) {
 
@@ -1820,14 +1827,15 @@ njs_json_append_string(njs_json_stringif
     }
 
     njs_json_buf_written(stringify, dst - stringify->last->pos);
-    njs_json_buf_append(stringify, "\"", 1);
+    njs_json_buf_append(stringify, &quote, 1);
 
     return NXT_OK;
 }
 
 
 static nxt_int_t
-njs_json_append_number(njs_json_stringify_t *stringify, njs_value_t *value)
+njs_json_append_number(njs_json_stringify_t *stringify,
+    const njs_value_t *value)
 {
     u_char  *p;
     size_t  size;
@@ -2037,3 +2045,342 @@ const njs_object_init_t  njs_json_object
     njs_json_object_properties,
     nxt_nitems(njs_json_object_properties),
 };
+
+
+static nxt_int_t
+njs_dump_value(njs_json_stringify_t *stringify, const njs_value_t *value)
+{
+    size_t       len;
+    njs_ret_t    ret;
+    nxt_str_t    str;
+    njs_value_t  str_val;
+    char         buf[32];
+    njs_ret_t    (*to_string)(njs_vm_t *vm, njs_value_t *retval,
+                              const njs_value_t *value);
+
+    switch (value->type) {
+    case NJS_OBJECT_STRING:
+        value = &value->data.u.object_value->value;
+
+        njs_string_get(value, &str);
+
+        njs_json_buf_append(stringify, "[String: ", 9);
+        njs_json_append_string(stringify, value, '\'');
+        return njs_json_buf_append(stringify, "]", 1);
+
+    case NJS_STRING:
+        njs_string_get(value, &str);
+
+        return njs_json_append_string(stringify, value, '\'');
+
+    case NJS_OBJECT_NUMBER:
+        value = &value->data.u.object_value->value;
+
+        ret = njs_number_to_string(stringify->vm, &str_val, value);
+        if (nxt_slow_path(ret != NXT_OK)) {
+            return NXT_ERROR;
+        }
+
+        njs_string_get(&str_val, &str);
+
+        njs_json_buf_append(stringify, "[Number: ", 9);
+        njs_json_buf_append(stringify, (char *) str.start, str.length);
+        return njs_json_buf_append(stringify, "]", 1);
+
+    case NJS_OBJECT_BOOLEAN:
+        value = &value->data.u.object_value->value;
+
+        if (njs_is_true(value)) {
+            return njs_json_buf_append(stringify, "[Boolean: true]", 15);
+
+        } else {
+            return njs_json_buf_append(stringify, "[Boolean: false]", 16);
+        }
+
+    case NJS_BOOLEAN:
+        if (njs_is_true(value)) {
+            return njs_json_buf_append(stringify, "true", 4);
+
+        } else {
+            return njs_json_buf_append(stringify, "false", 5);
+        }
+
+    case NJS_VOID:
+        return njs_json_buf_append(stringify, "undefined", 9);
+
+    case NJS_NULL:
+        return njs_json_buf_append(stringify, "null", 4);
+
+    case NJS_INVALID:
+        return njs_json_buf_append(stringify, "<empty>", 7);
+
+    case NJS_EXTERNAL:
+        len = snprintf(buf, sizeof(buf), "[External:%p]",
+                       njs_extern_object(stringify->vm, value));
+        return njs_json_buf_append(stringify, buf, len);
+
+    case NJS_FUNCTION:
+        if (value->data.u.function->native) {
+            return njs_json_buf_append(stringify, "[Function: native]", 18);
+
+        } else {
+            return njs_json_buf_append(stringify, "[Function]", 10);
+        }
+
+    case NJS_NUMBER:
+    case NJS_REGEXP:
+    case NJS_DATE:
+    case NJS_OBJECT_ERROR:
+    case NJS_OBJECT_EVAL_ERROR:
+    case NJS_OBJECT_INTERNAL_ERROR:
+    case NJS_OBJECT_RANGE_ERROR:
+    case NJS_OBJECT_REF_ERROR:
+    case NJS_OBJECT_SYNTAX_ERROR:
+    case NJS_OBJECT_TYPE_ERROR:
+    case NJS_OBJECT_URI_ERROR:
+
+        switch (value->type) {
+        case NJS_NUMBER:
+            to_string = njs_number_to_string;
+            break;
+
+        case NJS_REGEXP:
+            to_string = njs_regexp_to_string;
+            break;
+
+        case NJS_DATE:
+            to_string = njs_date_to_string;
+            break;
+
+        default:
+            to_string = njs_error_to_string;
+        }
+
+        ret = to_string(stringify->vm, &str_val, value);
+        if (nxt_slow_path(ret != NXT_OK)) {
+            return NXT_ERROR;
+        }
+
+        njs_string_get(&str_val, &str);
+
+        return njs_json_buf_append(stringify, (char *) str.start, str.length);
+
+    default:
+
+        njs_type_error(stringify->vm, "Non-serializable object");
+        return NXT_DECLINED;
+    }
+}
+
+
+#define njs_dump_is_object(value)                                             \
+    (((value)->type == NJS_OBJECT)                                            \
+     || ((value)->type == NJS_ARRAY)                                          \
+     || ((value)->type == NJS_OBJECT_VALUE))
+
+
+#define njs_dump_append_value(value)                                          \
+    state->written = 1;                                                       \
+    ret = njs_dump_value(stringify, value);                                   \
+    if (nxt_slow_path(ret != NXT_OK)) {                                       \
+        if (ret == NXT_DECLINED) {                                            \
+            goto exception;                                                   \
+        }                                                                     \
+                                                                              \
+        goto memory_error;                                                    \
+    }
+
+
+njs_ret_t
+njs_vm_value_dump(njs_vm_t *vm, nxt_str_t *retval, const njs_value_t *value,
+    nxt_uint_t indent)
+{
+    nxt_int_t             i;
+    njs_ret_t             ret;
+    nxt_str_t             str;
+    njs_value_t           *key, *val;
+    njs_json_state_t      *state;
+    njs_object_prop_t     *prop;
+    nxt_lvlhsh_query_t    lhq;
+    njs_json_stringify_t  *stringify;
+
+    if (njs_is_error(value)) {
+        return njs_vm_value_to_ext_string(vm, retval, value, 1);
+    }
+
+    stringify = nxt_mem_cache_alloc(vm->mem_cache_pool,
+                                    sizeof(njs_json_stringify_t));
+
+    if (nxt_slow_path(stringify == NULL)) {
+        goto memory_error;
+    }
+
+    stringify->vm = vm;
+    stringify->pool = vm->mem_cache_pool;
+    stringify->nodes = NULL;
+    stringify->last = NULL;
+
+    if (!njs_dump_is_object(value)) {
+        ret = njs_dump_value(stringify, value);
+        if (nxt_slow_path(ret != NXT_OK)) {
+            if (ret == NXT_DECLINED) {
+                goto exception;
+            }
+
+            goto memory_error;
+        }
+
+        goto done;
+    }
+
+    stringify->space.length = indent;
+    stringify->space.start = nxt_mem_cache_alloc(vm->mem_cache_pool, indent);
+    if (nxt_slow_path(stringify->space.start == NULL)) {
+        goto memory_error;
+    }
+
+    memset(stringify->space.start, ' ', indent);
+
+    if (nxt_array_init(&stringify->stack, NULL, 4, sizeof(njs_json_state_t),
+                       &njs_array_mem_proto, vm->mem_cache_pool)
+        == NULL)
+    {
+        goto memory_error;
+    }
+
+    if (njs_json_push_stringify_state(vm, stringify, value) == NULL) {
+        goto memory_error;
+    }
+
+    state = stringify->state;
+
+    for ( ;; ) {
+        switch (state->type) {
+        case NJS_JSON_OBJECT_START:
+            njs_json_stringify_append("{", 1);
+            njs_json_stringify_indent(stringify->stack.items + 1);
+            state->type = NJS_JSON_OBJECT_CONTINUE;
+
+            /* Fall through. */
+
+        case NJS_JSON_OBJECT_CONTINUE:
+            if (state->index >= state->keys->length) {
+                njs_json_stringify_indent(stringify->stack.items);
+                njs_json_stringify_append("}", 1);
+
+                state = njs_json_pop_stringify_state(stringify);
+                if (state == NULL) {
+                    goto done;
+                }
+
+                break;
+            }
+
+            key = &state->keys->start[state->index++];
+            njs_string_get(key, &lhq.key);
+            lhq.key_hash = nxt_djb_hash(lhq.key.start, lhq.key.length);
+            lhq.proto = &njs_object_hash_proto;
+
+            ret = nxt_lvlhsh_find(&state->value.data.u.object->hash, &lhq);
+            if (nxt_slow_path(ret == NXT_DECLINED)) {
+                break;
+            }
+
+            prop = lhq.value;
+
+            if (!prop->enumerable || njs_is_invalid(&prop->value)) {
+                break;
+            }
+
+            if (state->written) {
+                njs_json_stringify_append(",", 1);
+                njs_json_stringify_indent(stringify->stack.items + 1);
+            }
+
+            state->written = 1;
+            njs_json_stringify_append((char *) lhq.key.start, lhq.key.length);
+            njs_json_stringify_append(":", 1);
+            if (stringify->space.length != 0) {
+                njs_json_stringify_append(" ", 1);
+            }
+
+            if (njs_dump_is_object(&prop->value)) {
+                state = njs_json_push_stringify_state(vm, stringify,
+                                                      &prop->value);
+                if (state == NULL) {
+                    goto exception;
+                }
+
+                break;
+            }
+
+            njs_dump_append_value(&prop->value);
+
+            break;
+
+        case NJS_JSON_ARRAY_START:
+            njs_json_stringify_append("[", 1);
+            njs_json_stringify_indent(stringify->stack.items + 1);
+            state->type = NJS_JSON_ARRAY_CONTINUE;
+
+            /* Fall through. */
+
+        case NJS_JSON_ARRAY_CONTINUE:
+            if (state->index >= state->value.data.u.array->length) {
+                njs_json_stringify_indent(stringify->stack.items);
+                njs_json_stringify_append("]", 1);
+
+                state = njs_json_pop_stringify_state(stringify);
+                if (state == NULL) {
+                    goto done;
+                }
+
+                break;
+            }
+
+            if (state->written) {
+                njs_json_stringify_append(",", 1);
+                njs_json_stringify_indent(stringify->stack.items + 1);
+            }
+
+            val = &state->value.data.u.array->start[state->index++];
+
+            if (njs_dump_is_object(val)) {
+                state = njs_json_push_stringify_state(vm, stringify, val);
+                if (state == NULL) {
+                    goto exception;
+                }
+
+                break;
+            }
+
+            njs_dump_append_value(val);
+
+            break;
+
+        default:
+            nxt_unreachable();
+        }
+    }
+
+done:
+
+    ret = njs_json_buf_pullup(stringify, &str);
+    if (nxt_slow_path(ret != NXT_OK)) {
+        goto memory_error;
+    }
+
+    *retval = str;
+
+    return NXT_OK;
+
+memory_error:
+
+    njs_memory_error(vm);
+
+exception:
+
+    njs_vm_value_to_ext_string(vm, retval, &vm->retval, 1);
+
+    return NXT_OK;
+}
diff --git a/njs/njs_regexp.c b/njs/njs_regexp.c
--- a/njs/njs_regexp.c
+++ b/njs/njs_regexp.c
@@ -527,22 +527,8 @@ static njs_ret_t
 njs_regexp_prototype_to_string(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused)
 {
-    u_char                *source;
-    int32_t               length;
-    uint32_t              size;
-    njs_value_t           *value;
-    njs_regexp_pattern_t  *pattern;
-
-    value = &args[0];
-
-    if (njs_is_regexp(value)) {
-        pattern = value->data.u.regexp->pattern;
-        source = pattern->source;
-
-        size = strlen((char *) source);
-        length = nxt_utf8_length(source, size);
-
-        return njs_regexp_string_create(vm, &vm->retval, source, size, length);
+    if (njs_is_regexp(&args[0])) {
+        return njs_regexp_to_string(vm, &vm->retval, &args[0]);
     }
 
     njs_type_error(vm, "'this' argument is not a regexp");
@@ -551,6 +537,25 @@ njs_regexp_prototype_to_string(njs_vm_t 
 }
 
 
+njs_ret_t
+njs_regexp_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *value)
+{
+    u_char                *source;
+    int32_t               length;
+    uint32_t              size;
+    njs_regexp_pattern_t  *pattern;
+
+    pattern = value->data.u.regexp->pattern;
+    source = pattern->source;
+
+    size = strlen((char *) source);
+    length = nxt_utf8_length(source, size);
+
+    return njs_regexp_string_create(vm, retval, source, size, length);
+}
+
+
 static njs_ret_t
 njs_regexp_prototype_test(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
     njs_index_t unused)
diff --git a/njs/njs_regexp.h b/njs/njs_regexp.h
--- a/njs/njs_regexp.h
+++ b/njs/njs_regexp.h
@@ -31,6 +31,8 @@ njs_regexp_t *njs_regexp_alloc(njs_vm_t 
 njs_ret_t njs_regexp_prototype_exec(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
+njs_ret_t njs_regexp_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *regexp);
 
 extern const njs_object_init_t  njs_regexp_constructor_init;
 extern const njs_object_init_t  njs_regexp_prototype_init;
diff --git a/njs/njs_shell.c b/njs/njs_shell.c
--- a/njs/njs_shell.c
+++ b/njs/njs_shell.c
@@ -423,7 +423,7 @@ njs_process_script(njs_vm_t *vm, njs_opt
         ret = njs_vm_run(vm);
     }
 
-    if (njs_vm_retval_to_ext_string(vm, out) != NXT_OK) {
+    if (njs_vm_value_dump(vm, out, njs_vm_retval(vm), 1) != NXT_OK) {
         *out = nxt_string_value("failed to get retval from VM");
         return NXT_ERROR;
     }
diff --git a/njs/njs_vm.h b/njs/njs_vm.h
--- a/njs/njs_vm.h
+++ b/njs/njs_vm.h
@@ -552,6 +552,10 @@ typedef njs_ret_t (*njs_vmcode_operation
     ((value)->type != NJS_INVALID)
 
 
+#define njs_is_invalid(value)                                                 \
+    ((value)->type == NJS_INVALID)
+
+
 #define njs_set_invalid(value)                                                \
     (value)->type = NJS_INVALID
 
diff --git a/njs/test/njs_expect_test.exp b/njs/test/njs_expect_test.exp
--- a/njs/test/njs_expect_test.exp
+++ b/njs/test/njs_expect_test.exp
@@ -187,12 +187,32 @@ njs_test {
      "console.log(console)\r\nTypeError:*at console.log (native)"}
 }
 
-# Exception in njs_vm_retval_to_ext_string()
+# dumper
 njs_test {
-    {"var o = { toString: function() { return [1] } }\r\n"
+    {"var o = {toString: function(){}, log: console.log}\r\n"
      "undefined\r\n>> "}
     {"o\r\n"
-     "TypeError: cannot evaluate an object's value"}
+     "o\r\n{\r\n toString: \\\[Function],\r\n log: \\\[Function: native]\r\n}"}
+}
+
+njs_test {
+    {"[1, new Number(2), 'a', new String('αβZγ'), true, new Boolean(false)]\r\n"
+     "\\\[\r\n 1,\r\n \\\[Number: 2],\r\n 'a',\r\n \\\[String: 'αβZγ'],\r\n true,\r\n \\\[Boolean: false]\r\n]"}
+}
+
+njs_test {
+    {"[undefined,,null,console]\r\n"
+     "\\\[\r\n undefined,\r\n <empty>,\r\n null,\r\n \\\[External:(nil)]\r\n]"}
+}
+
+njs_test {
+    {"[InternalError(),TypeError('msg'), new RegExp(), /^undef$/m, new Date(0)]\r\n"
+     "\\\[\r\n InternalError,\r\n TypeError: msg,\r\n /(?:)/,\r\n /^undef$/m,\r\n 1970-01-01T00:00:00.000Z\r\n]"}
+}
+
+njs_test {
+    {"[{a:1}]\r\n"
+     "\r\n\\\[\r\n {\r\n  a: 1\r\n }\r\n]"}
 }
 
 # Backtraces are reset between invocations
@@ -269,28 +289,28 @@ njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file').toString('base64')\r\n"
-     "zrHOslrOsw==\r\n>> "}
+     "'zrHOslrOsw=='\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file', 'utf8')[2]\r\n"
-     "Z\r\n>> "}
+     "'Z'\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file')[4]\r\n"
-     "Z\r\n>> "}
+     "'Z'\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file', {encoding:'utf8',flag:'r+'})\r\n"
-     "αβZγ\r\n>> "}
+     "'αβZγ'\r\n>> "}
 }
 
 njs_test {
@@ -368,7 +388,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 njs_test {
@@ -377,7 +397,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC', 'utf8')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 njs_test {
@@ -388,7 +408,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 njs_test {
@@ -397,7 +417,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC', {encoding:'utf8', mode:0o666})\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 exec rm -fr njs_wo_file
@@ -436,5 +456,5 @@ njs_test {
     {"fs.appendFileSync('njs_test_file2', 'ABC')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABCABC\r\n>> "}
+     "'ABCABC'\r\n>> "}
 }

@drsm
Copy link
Contributor Author

drsm commented Jul 13, 2018

Hi @xeioex!
Looks good to me.

>> Object.getOwnPropertyDescriptor(Object, 'getOwnPropertyDescriptor');
{
 value: [Function: native],
 configurable: false,
 enumerable: false,
 writable: false
}
>> console.log(Object.getOwnPropertyDescriptor(Object, 'getOwnPropertyDescriptor'));
[object Object]
undefined
>> console.log(njs.dump(Object.getOwnPropertyDescriptor(Object, 'getOwnPropertyDescriptor')));
{value:[Function: native],configurable:false,enumerable:false,writable:false}
undefined
>>

pretty awesome, thanks!

@drsm
Copy link
Contributor Author

drsm commented Jul 13, 2018

just a side note.
while there

njs/njs/njs_vm.c

Line 3093 in 5c1255d

njs_type_error(vm, "cannot evaluate an object's value");

maybe better to extend diagnostics here?

also, found a killing regression

>> var x = { toString: function() { throw "test"; } }
undefined
>> x
{
 toString: [Function]
}
>> console.log(x)
'test'
>> try { console.log(x) } catch (e) { console.log('err: ' + e)}
err: test
null
Segmentation fault

@xeioex
Copy link
Contributor

xeioex commented Jul 14, 2018

@drsm Thank you for the feedback. Will be fixed.

Another question is whether console.log() should output the default toString() representation of a value (as it is now) or the pretty version of it.

@drsm
Copy link
Contributor Author

drsm commented Jul 14, 2018

@xeioex i think, the best way is to leave console.log() as is, for now, and probably shortcut a console.log(njs.dump(arg1),njs.dump(arg2),...) to console.dump(...) for convenience.

@xeioex
Copy link
Contributor

xeioex commented Jul 16, 2018

found a killing regression

looks like an unrelated problem. It is better to create a separate issue.

@xeioex
Copy link
Contributor

xeioex commented Jul 16, 2018

This patch fixes try { console.log(x) } catch (e) { console.log('err: ' + e)} Segmentation fault

# HG changeset patch
# User Dmitry Volyntsev <xeioex@nginx.com>
# Date 1531755945 -10800
#      Mon Jul 16 18:45:45 2018 +0300
# Node ID 4466abb4f3fc44297c871b0a5248e67b82f2e194
# Parent  04dc301f7d5ae7bde1317579afad6bf5213b7b1d
Fixed exception handling in njs_vm_value_to_ext_string().

diff --git a/njs/njs_vm.c b/njs/njs_vm.c
--- a/njs/njs_vm.c
+++ b/njs/njs_vm.c
@@ -3273,8 +3273,9 @@ fail:
 static njs_ret_t
 njs_object_value_to_string(njs_vm_t *vm, njs_value_t *value)
 {
-    u_char     *current;
-    njs_ret_t  ret;
+    u_char              *current;
+    njs_ret_t           ret;
+    njs_native_frame_t  *previous;
 
     static const njs_vmcode_1addr_t  value_to_string[] = {
         { .code = { .operation = njs_vmcode_value_to_string,
@@ -3298,6 +3299,14 @@ njs_object_value_to_string(njs_vm_t *vm,
     njs_set_invalid(&vm->top_frame->trap_scratch);
     vm->top_frame->trap_values[0] = *value;
 
+    /*
+     * Prevent njs_vmcode_interpreter() to unwind the current frame if
+     * an exception happens.  It preserves the current frame state if
+     * njs_vm_value_to_ext_string() is called from within njs_vm_run().
+     */
+    previous = vm->top_frame->previous;
+    vm->top_frame->previous = NULL;
+
     ret = njs_vmcode_interpreter(vm);
 
     if (ret == NJS_STOP) {
@@ -3306,6 +3315,7 @@ njs_object_value_to_string(njs_vm_t *vm,
     }
 
     vm->current = current;
+    vm->top_frame->previous = previous;
 
     return ret;
 }
diff --git a/njs/test/njs_expect_test.exp b/njs/test/njs_expect_test.exp
--- a/njs/test/njs_expect_test.exp
+++ b/njs/test/njs_expect_test.exp
@@ -203,6 +203,13 @@ njs_test {
      "JSON.parse(Error()\r\nSyntaxError: Unexpected token \"\" in 1"}
 }
 
+njs_test {
+    {"try { console.log({ toString: function() { throw 'test'; } }) } catch (e) {}\r\n"
+     "undefined"}
+    {"function f() { throw 't' }; try { console.log({ toString: function() { return f() } }) } catch (e) {}\r\n"
+     "undefined"}
+}
+
 # Non-ASCII characters
 njs_test {
     {"'絵文字'\r\n"

@drsm
Copy link
Contributor Author

drsm commented Jul 17, 2018

looks like an unrelated problem. It is better to create a separate issue.

yeah, it is a separate bug. i just overlooked it...

@xeioex
Copy link
Contributor

xeioex commented Jul 17, 2018

@drsm just committed the patch above (Segmentation fault related).

@drsm
Copy link
Contributor Author

drsm commented Jul 17, 2018

Hi @xeioex !
After applying the dumper patch to the latest version committed, i found that there is an another issue with exception handling, which is not reproduced without the patch.

>> var fn = function() { throw 'test'; }
undefined
>> var x = { toString: fn, toJSON: fn }
undefined
>> x
{
 toString: [Function],
 toJSON: [Function]
}
>> console.log(x)
'test'
>> JSON.stringify(x)
'test'
>> var fn = function() { throw Error('test'); }
undefined
>> var x = { toString: fn, toJSON: fn }
undefined
>> JSON.stringify(x)
Error: test
    at anonymous (:1)
    at JSON.stringify (native)
    at main (native)

>> console.log(x)
Error: test
    at anonymous (:1)
    at console.log (native)
    at console.log (native)
    at main (native)
>> 

@xeioex xeioex closed this as completed Jul 26, 2018
@xeioex
Copy link
Contributor

xeioex commented Jul 26, 2018

here is the updated version of the patch

Now external objects like console can also be printed

>> console
{
 log: {type:"method",props:["method"]},
 dump: {type:"method",props:["method"]},
 help: {type:"method",props:["method"]}
}

console.log(val)
Prints to stdout the flat pretty string representation
of values (no new lines).

console.dump([value1[, values]])
Prints to stdout the pretty string representation of values.

# HG changeset patch
# User Dmitry Volyntsev <xeioex@nginx.com>
# Date 1532626366 -10800
#      Thu Jul 26 20:32:46 2018 +0300
# Node ID 70fe767cfc428383e1e9bde2b0d760b911432ed5
# Parent  766fcec15744a7bc86638868e211732fb4172d7c
Added the pretty string representation for values.

Interactive shell: dumping the pretty string representation of
the last expression.

:>> [1, new Number(2), {a: new String('αβZγ')}, true, console.log,
    /^undef$/m, new Date(0)]
[
 1,
 [Number: 2],
 {
  a: [String: 'αβZγ']
 },
 true,
 [Function: native],
 /^undef$/m,
 1970-01-01T00:00:00.000Z
]

njs.dump(value[, indent]):
    Returns the pretty string representation of a value.
    value - a value of any type.
    indent - a number.
        A number of space characters per indentation level.

    njs.dump({a:[]}) -> '{a:[]}'
    njs.dump({a:[]}, 1) -> '{\n a: [\n  \n ]\n}'

console.log([value1[, values]])
    Prints to stdout the flat pretty string representation
    of values (no new lines).

console.dump([value1[, values]])
    Prints to stdout the pretty string representation of values.

This fixes #11 issue on GitHub.

diff --git a/njs/njs.h b/njs/njs.h
--- a/njs/njs.h
+++ b/njs/njs.h
@@ -217,6 +217,8 @@ NXT_EXPORT nxt_int_t njs_value_is_string
 NXT_EXPORT nxt_int_t njs_value_is_object(njs_value_t *value);
 NXT_EXPORT nxt_int_t njs_value_is_function(njs_value_t *value);
 
+NXT_EXPORT njs_ret_t njs_vm_value_dump(njs_vm_t *vm, nxt_str_t *retval,
+    const njs_value_t *value, nxt_uint_t indent);
 NXT_EXPORT njs_value_t *njs_vm_object_prop(njs_vm_t *vm, njs_value_t *value,
     const nxt_str_t *key);
 
diff --git a/njs/njs_builtin.c b/njs/njs_builtin.c
--- a/njs/njs_builtin.c
+++ b/njs/njs_builtin.c
@@ -1069,6 +1069,28 @@ njs_builtin_match_native_function(njs_vm
 }
 
 
+static njs_ret_t
+njs_dump_value(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
+    njs_index_t unused)
+{
+    nxt_str_t          str;
+    nxt_uint_t         n;
+    const njs_value_t  *value, *indent;
+
+    value = njs_arg(args, nargs, 1);
+    indent = njs_arg(args, nargs, 2);
+
+    n = indent->data.u.number;
+    n = nxt_min(n, 5);
+
+    if (njs_vm_value_dump(vm, &str, value, n) != NXT_OK) {
+        return NXT_ERROR;
+    }
+
+    return njs_string_new(vm, &vm->retval, str.start, str.length, 0);
+}
+
+
 static const njs_object_prop_t  njs_njs_object_properties[] =
 {
     {
@@ -1076,6 +1098,13 @@ static const njs_object_prop_t  njs_njs_
         .name = njs_string("version"),
         .value = njs_string(NJS_VERSION),
     },
+
+    {
+        .type = NJS_METHOD,
+        .name = njs_string("dump"),
+        .value = njs_native_function(njs_dump_value, 0,
+                                    NJS_SKIP_ARG, NJS_SKIP_ARG, NJS_NUMBER_ARG),
+    },
 };
 
 
diff --git a/njs/njs_date.c b/njs/njs_date.c
--- a/njs/njs_date.c
+++ b/njs/njs_date.c
@@ -1019,6 +1019,13 @@ static njs_ret_t
 njs_date_prototype_to_iso_string(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused)
 {
+    return njs_date_to_string(vm, &vm->retval, &args[0]);
+}
+
+
+njs_ret_t
+njs_date_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *date)
+{
     int32_t    year;
     double     time;
     size_t     size;
@@ -1026,7 +1033,7 @@ njs_date_prototype_to_iso_string(njs_vm_
     u_char     buf[NJS_ISO_DATE_TIME_LEN];
     struct tm  tm;
 
-    time = args[0].data.u.date->time;
+    time = date->data.u.date->time;
 
     if (!isnan(time)) {
         clock = time / 1000;
@@ -1042,7 +1049,7 @@ njs_date_prototype_to_iso_string(njs_vm_
                         tm.tm_hour, tm.tm_min, tm.tm_sec,
                         (int) ((int64_t) time % 1000));
 
-        return njs_string_new(vm, &vm->retval, buf, size, size);
+        return njs_string_new(vm, retval, buf, size, size);
     }
 
     njs_range_error(vm, NULL);
diff --git a/njs/njs_date.h b/njs/njs_date.h
--- a/njs/njs_date.h
+++ b/njs/njs_date.h
@@ -11,6 +11,9 @@
 njs_ret_t njs_date_constructor(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
+njs_ret_t njs_date_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *date);
+
 
 extern const njs_object_init_t  njs_date_constructor_init;
 extern const njs_object_init_t  njs_date_prototype_init;
diff --git a/njs/njs_error.c b/njs/njs_error.c
--- a/njs/njs_error.c
+++ b/njs/njs_error.c
@@ -586,6 +586,18 @@ static njs_ret_t
 njs_error_prototype_to_string(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
     njs_index_t unused)
 {
+    if (nargs < 1 || !njs_is_object(&args[0])) {
+        njs_type_error(vm, "'this' argument is not an object");
+        return NXT_ERROR;
+    }
+
+    return njs_error_to_string(vm, &vm->retval, &args[0]);
+}
+
+
+njs_ret_t
+njs_error_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *error)
+{
     size_t              size;
     u_char              *p;
     nxt_str_t           name, message;
@@ -595,16 +607,11 @@ njs_error_prototype_to_string(njs_vm_t *
 
     static const njs_value_t  default_name = njs_string("Error");
 
-    if (nargs < 1 || !njs_is_object(&args[0])) {
-        njs_type_error(vm, "'this' argument is not an object");
-        return NXT_ERROR;
-    }
-
     lhq.key_hash = NJS_NAME_HASH;
     lhq.key = nxt_string_value("name");
     lhq.proto = &njs_object_hash_proto;
 
-    prop = njs_object_property(vm, args[0].data.u.object, &lhq);
+    prop = njs_object_property(vm, error->data.u.object, &lhq);
 
     if (prop != NULL) {
         name_value = &prop->value;
@@ -618,7 +625,7 @@ njs_error_prototype_to_string(njs_vm_t *
     lhq.key_hash = NJS_MESSAGE_HASH;
     lhq.key = nxt_string_value("message");
 
-    prop = njs_object_property(vm, args[0].data.u.object, &lhq);
+    prop = njs_object_property(vm, error->data.u.object, &lhq);
 
     if (prop != NULL) {
         message_value = &prop->value;
@@ -630,18 +637,18 @@ njs_error_prototype_to_string(njs_vm_t *
     njs_string_get(message_value, &message);
 
     if (name.length == 0) {
-        vm->retval = *message_value;
+        *retval = *message_value;
         return NJS_OK;
     }
 
     if (message.length == 0) {
-        vm->retval = *name_value;
+        *retval = *name_value;
         return NJS_OK;
     }
 
     size = name.length + message.length + 2;
 
-    p = njs_string_alloc(vm, &vm->retval, size, size);
+    p = njs_string_alloc(vm, retval, size, size);
 
     if (nxt_fast_path(p != NULL)) {
         p = nxt_cpymem(p, name.start, name.length);
@@ -653,6 +660,7 @@ njs_error_prototype_to_string(njs_vm_t *
     }
 
     njs_memory_error(vm);
+
     return NJS_ERROR;
 }
 
diff --git a/njs/njs_error.h b/njs/njs_error.h
--- a/njs/njs_error.h
+++ b/njs/njs_error.h
@@ -53,6 +53,8 @@ njs_ret_t njs_uri_error_constructor(njs_
 njs_ret_t njs_memory_error_constructor(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
+njs_ret_t njs_error_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *error);
 
 extern const njs_object_init_t  njs_error_constructor_init;
 extern const njs_object_init_t  njs_eval_error_constructor_init;
diff --git a/njs/njs_extern.c b/njs/njs_extern.c
--- a/njs/njs_extern.c
+++ b/njs/njs_extern.c
@@ -228,6 +228,60 @@ njs_vm_external_bind(njs_vm_t *vm, const
 }
 
 
+njs_array_t *
+njs_extern_keys_array(njs_vm_t *vm, const njs_extern_t *external)
+{
+    uint32_t            n, keys_length;
+    njs_ret_t           ret;
+    njs_array_t         *keys;
+    const nxt_lvlhsh_t  *hash;
+    nxt_lvlhsh_each_t   lhe;
+    const njs_extern_t  *ext;
+
+    keys_length = 0;
+
+    nxt_lvlhsh_each_init(&lhe, &njs_extern_hash_proto);
+
+    hash = &external->hash;
+
+    for ( ;; ) {
+        ext = nxt_lvlhsh_each(hash, &lhe);
+
+        if (ext == NULL) {
+            break;
+        }
+
+        keys_length++;
+    }
+
+    keys = njs_array_alloc(vm, keys_length, NJS_ARRAY_SPARE);
+    if (nxt_slow_path(keys == NULL)) {
+        return NULL;
+    }
+
+    n = 0;
+
+    nxt_lvlhsh_each_init(&lhe, &njs_extern_hash_proto);
+
+    for ( ;; ) {
+        ext = nxt_lvlhsh_each(hash, &lhe);
+
+        if (ext == NULL) {
+            break;
+        }
+
+        ret = njs_string_create(vm, &keys->start[n++], ext->name.start,
+                                ext->name.length, 0);
+
+        if (ret != NXT_OK) {
+            return NULL;
+        }
+    }
+
+    return keys;
+}
+
+
 njs_value_t *
 njs_parser_external(njs_vm_t *vm, njs_parser_t *parser)
 {
diff --git a/njs/njs_extern.h b/njs/njs_extern.h
--- a/njs/njs_extern.h
+++ b/njs/njs_extern.h
@@ -38,6 +38,7 @@ typedef struct {
 } njs_extern_value_t;
 
 
+njs_array_t *njs_extern_keys_array(njs_vm_t *vm, const njs_extern_t *external);
 nxt_int_t njs_external_match_native_function(njs_vm_t *vm,
     njs_function_native_t func, nxt_str_t *name);
 
diff --git a/njs/njs_json.c b/njs/njs_json.c
--- a/njs/njs_json.c
+++ b/njs/njs_json.c
@@ -5,6 +5,9 @@
  */
 
 #include <njs_core.h>
+#include <njs_json.h>
+#include <njs_date.h>
+#include <njs_regexp.h>
 #include <stdio.h>
 #include <string.h>
 
@@ -127,16 +130,16 @@ static njs_ret_t njs_json_stringify_repl
 static njs_ret_t njs_json_stringify_array(njs_vm_t *vm,
     njs_json_stringify_t *stringify);
 static njs_json_state_t *njs_json_push_stringify_state(njs_vm_t *vm,
-    njs_json_stringify_t *stringify, njs_value_t *value);
+    njs_json_stringify_t *stringify, const njs_value_t *value);
 static njs_json_state_t *njs_json_pop_stringify_state(
     njs_json_stringify_t *stringify);
 
 static nxt_int_t njs_json_append_value(njs_json_stringify_t *stringify,
-    njs_value_t *value);
+    const njs_value_t *value);
 static nxt_int_t njs_json_append_string(njs_json_stringify_t *stringify,
-    njs_value_t *value);
+    const njs_value_t *value, char quote);
 static nxt_int_t njs_json_append_number(njs_json_stringify_t *stringify,
-    njs_value_t *value);
+    const njs_value_t *value);
 
 static njs_value_t *njs_json_wrap_value(njs_vm_t *vm, njs_value_t *value);
 
@@ -1162,7 +1165,7 @@ njs_json_parse_exception(njs_json_parse_
     }                                                                         \
                                                                               \
     state->written = 1;                                                       \
-    njs_json_append_string(stringify, key);                                   \
+    njs_json_append_string(stringify, key, '\"');                             \
     njs_json_stringify_append(":", 1);                                        \
     if (stringify->space.length != 0) {                                       \
         njs_json_stringify_append(" ", 1);                                    \
@@ -1621,7 +1624,7 @@ njs_json_stringify_array(njs_vm_t *vm, n
 
 static njs_json_state_t *
 njs_json_push_stringify_state(njs_vm_t *vm, njs_json_stringify_t *stringify,
-    njs_value_t *value)
+    const njs_value_t *value)
 {
     njs_json_state_t  *state;
 
@@ -1654,7 +1657,13 @@ njs_json_push_stringify_state(njs_vm_t *
             state->keys = stringify->replacer.data.u.array;
 
         } else {
-            state->keys = njs_object_keys_array(vm, value);
+            if (njs_is_external(value)) {
+                state->keys = njs_extern_keys_array(vm, value->external.proto);
+
+            } else {
+                state->keys = njs_object_keys_array(vm, value);
+            }
+
             if (state->keys == NULL) {
                 return NULL;
             }
@@ -1681,7 +1690,7 @@ njs_json_pop_stringify_state(njs_json_st
 
 
 static nxt_int_t
-njs_json_append_value(njs_json_stringify_t *stringify, njs_value_t *value)
+njs_json_append_value(njs_json_stringify_t *stringify, const njs_value_t *value)
 {
     switch (value->type) {
     case NJS_OBJECT_STRING:
@@ -1689,7 +1698,7 @@ njs_json_append_value(njs_json_stringify
         /* Fall through. */
 
     case NJS_STRING:
-        return njs_json_append_string(stringify, value);
+        return njs_json_append_string(stringify, value, '\"');
 
     case NJS_OBJECT_NUMBER:
         value = &value->data.u.object_value->value;
@@ -1724,7 +1733,8 @@ njs_json_append_value(njs_json_stringify
 
 
 static nxt_int_t
-njs_json_append_string(njs_json_stringify_t *stringify, njs_value_t *value)
+njs_json_append_string(njs_json_stringify_t *stringify,
+    const njs_value_t *value, char quote)
 {
     u_char             c, *dst, *dst_end;
     size_t             length;
@@ -1734,7 +1744,7 @@ njs_json_append_string(njs_json_stringif
     static char   hex2char[16] = { '0', '1', '2', '3', '4', '5', '6', '7',
                                    '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
 
-    (void) njs_string_prop(&str, value);
+    (void) njs_string_prop(&str, (njs_value_t *) value);
 
     p = str.start;
     end = p + str.size;
@@ -1747,11 +1757,14 @@ njs_json_append_string(njs_json_stringif
 
     dst_end = dst + 64;
 
-    *dst++ = '\"';
+    *dst++ = quote;
 
     while (p < end) {
 
-        if (*p < ' ' || *p == '\"' || *p == '\\') {
+        if (*p < ' '
+            || *p == '\\'
+            || (*p == '\"' && quote == '\"'))
+        {
             c = (u_char) *p++;
             *dst++ = '\\';
 
@@ -1793,7 +1806,7 @@ njs_json_append_string(njs_json_stringif
          */
 
         while (p < end && (dst_end - dst) > 6) {
-            if (*p < ' ' || *p == '\"' || *p == '\\') {
+            if (*p < ' ' || (*p == '\"' && quote == '\"') || *p == '\\') {
                 break;
             }
 
@@ -1820,14 +1833,15 @@ njs_json_append_string(njs_json_stringif
     }
 
     njs_json_buf_written(stringify, dst - stringify->last->pos);
-    njs_json_buf_append(stringify, "\"", 1);
+    njs_json_buf_append(stringify, &quote, 1);
 
     return NXT_OK;
 }
 
 
 static nxt_int_t
-njs_json_append_number(njs_json_stringify_t *stringify, njs_value_t *value)
+njs_json_append_number(njs_json_stringify_t *stringify,
+    const njs_value_t *value)
 {
     u_char  *p;
     size_t  size;
@@ -2037,3 +2051,446 @@ const njs_object_init_t  njs_json_object
     njs_json_object_properties,
     nxt_nitems(njs_json_object_properties),
 };
+
+
+#define njs_dump(str)                                                         \
+    ret = njs_json_buf_append(stringify, str, nxt_length(str));               \
+    if (nxt_slow_path(ret != NXT_OK)) {                                       \
+        goto memory_error;                                                    \
+    }
+
+
+#define njs_dump_item(str)                                                    \
+    if (written) {                                                            \
+        njs_json_buf_append(stringify, ",", 1);                               \
+    }                                                                         \
+                                                                              \
+    written = 1;                                                              \
+    ret = njs_json_buf_append(stringify, str, nxt_length(str));               \
+    if (nxt_slow_path(ret != NXT_OK)) {                                       \
+        goto memory_error;                                                    \
+    }
+
+
+static nxt_int_t
+njs_dump_value(njs_json_stringify_t *stringify, const njs_value_t *value)
+{
+    size_t              len;
+    njs_ret_t           ret;
+    nxt_str_t           str;
+    nxt_uint_t          written;
+    njs_value_t         str_val;
+    const njs_extern_t  *ext_proto;
+    char                buf[32];
+
+    njs_ret_t    (*to_string)(njs_vm_t *vm, njs_value_t *retval,
+                              const njs_value_t *value);
+
+    switch (value->type) {
+    case NJS_OBJECT_STRING:
+        value = &value->data.u.object_value->value;
+
+        njs_string_get(value, &str);
+
+        njs_dump("[String: ");
+        njs_json_append_string(stringify, value, '\'');
+        njs_dump("]")
+        break;
+
+    case NJS_STRING:
+        njs_string_get(value, &str);
+        return njs_json_append_string(stringify, value, '\'');
+
+    case NJS_OBJECT_NUMBER:
+        value = &value->data.u.object_value->value;
+
+        ret = njs_number_to_string(stringify->vm, &str_val, value);
+        if (nxt_slow_path(ret != NXT_OK)) {
+            return NXT_ERROR;
+        }
+
+        njs_string_get(&str_val, &str);
+
+        njs_dump("[Number: ");
+        njs_json_buf_append(stringify, (char *) str.start, str.length);
+        njs_dump("]")
+        break;
+
+    case NJS_OBJECT_BOOLEAN:
+        value = &value->data.u.object_value->value;
+
+        if (njs_is_true(value)) {
+            njs_dump("[Boolean: true]");
+
+        } else {
+            njs_dump("[Boolean: false]");
+        }
+
+        break;
+
+    case NJS_BOOLEAN:
+        if (njs_is_true(value)) {
+            njs_dump("true");
+
+        } else {
+            njs_dump("false");
+        }
+
+        break;
+
+    case NJS_VOID:
+        njs_dump("undefined");
+        break;
+
+    case NJS_NULL:
+        njs_dump("null");
+        break;
+
+    case NJS_INVALID:
+        njs_dump("<empty>");
+        break;
+
+    case NJS_FUNCTION:
+        if (value->data.u.function->native) {
+            njs_dump("[Function: native]");
+
+        } else {
+            njs_dump("[Function]");
+        }
+
+        break;
+
+    case NJS_EXTERNAL:
+        ext_proto = value->external.proto;
+
+        written = 0;
+        njs_dump_item("{type:");
+
+        switch (ext_proto->type) {
+        case NJS_EXTERN_PROPERTY:
+            njs_dump("\"property\"");
+            break;
+        case NJS_EXTERN_METHOD:
+            njs_dump("\"method\"");
+            break;
+        case NJS_EXTERN_OBJECT:
+            njs_dump("\"object\"");
+            break;
+        case NJS_EXTERN_CASELESS_OBJECT:
+            njs_dump("\"caseless_object\"");
+            break;
+        }
+
+        njs_dump_item("props:[");
+        written = 0;
+
+        if (ext_proto->get != NULL) {
+            njs_dump_item("\"getter\"");
+        }
+
+        if (ext_proto->set != NULL) {
+            njs_dump_item("\"setter\"");
+        }
+
+        if (ext_proto->function != NULL) {
+            njs_dump_item("\"method\"");
+        }
+
+        if (ext_proto->find != NULL) {
+            njs_dump_item("\"find\"");
+        }
+
+        if (ext_proto->foreach != NULL) {
+            njs_dump_item("\"foreach\"");
+        }
+
+        if (ext_proto->next != NULL) {
+            njs_dump_item("\"next\"");
+        }
+
+        return njs_json_buf_append(stringify, "]}", 2);
+
+    case NJS_NUMBER:
+    case NJS_REGEXP:
+    case NJS_DATE:
+    case NJS_OBJECT_ERROR:
+    case NJS_OBJECT_EVAL_ERROR:
+    case NJS_OBJECT_INTERNAL_ERROR:
+    case NJS_OBJECT_RANGE_ERROR:
+    case NJS_OBJECT_REF_ERROR:
+    case NJS_OBJECT_SYNTAX_ERROR:
+    case NJS_OBJECT_TYPE_ERROR:
+    case NJS_OBJECT_URI_ERROR:
+
+        switch (value->type) {
+        case NJS_NUMBER:
+            to_string = njs_number_to_string;
+            break;
+
+        case NJS_REGEXP:
+            to_string = njs_regexp_to_string;
+            break;
+
+        case NJS_DATE:
+            to_string = njs_date_to_string;
+            break;
+
+        default:
+            to_string = njs_error_to_string;
+        }
+
+        ret = to_string(stringify->vm, &str_val, value);
+        if (nxt_slow_path(ret != NXT_OK)) {
+            return NXT_ERROR;
+        }
+
+        njs_string_get(&str_val, &str);
+
+        return njs_json_buf_append(stringify, (char *) str.start, str.length);
+
+    default:
+        len = snprintf(buf, sizeof(buf), "[Unknown value type:%d]",
+                       value->type);
+        return njs_json_buf_append(stringify, buf, len);
+    }
+
+    return ret;
+
+memory_error:
+
+    njs_memory_error(stringify->vm);
+
+    return NXT_ERROR;
+}
+
+
+#define njs_dump_is_object(value)                                             \
+    (((value)->type == NJS_OBJECT)                                            \
+     || ((value)->type == NJS_ARRAY)                                          \
+     || ((value)->type == NJS_OBJECT_VALUE)                                   \
+     || ((value)->type == NJS_EXTERNAL                                        \
+         && !nxt_lvlhsh_is_empty(&(value)->external.proto->hash)))
+
+
+#define njs_dump_append_value(value)                                          \
+    state->written = 1;                                                       \
+    ret = njs_dump_value(stringify, value);                                   \
+    if (nxt_slow_path(ret != NXT_OK)) {                                       \
+        if (ret == NXT_DECLINED) {                                            \
+            goto exception;                                                   \
+        }                                                                     \
+                                                                              \
+        goto memory_error;                                                    \
+    }
+
+
+njs_ret_t
+njs_vm_value_dump(njs_vm_t *vm, nxt_str_t *retval, const njs_value_t *value,
+    nxt_uint_t indent)
+{
+    nxt_int_t             i;
+    njs_ret_t             ret;
+    nxt_str_t             str;
+    njs_value_t           *key, *val, ext_val;
+    njs_json_state_t      *state;
+    njs_object_prop_t     *prop;
+    nxt_lvlhsh_query_t    lhq;
+    njs_json_stringify_t  *stringify;
+
+    if (njs_is_error(value)) {
+        goto exception;
+    }
+
+    stringify = nxt_mem_cache_alloc(vm->mem_cache_pool,
+                                    sizeof(njs_json_stringify_t));
+
+    if (nxt_slow_path(stringify == NULL)) {
+        goto memory_error;
+    }
+
+    stringify->vm = vm;
+    stringify->pool = vm->mem_cache_pool;
+    stringify->nodes = NULL;
+    stringify->last = NULL;
+
+    if (!njs_dump_is_object(value)) {
+        ret = njs_dump_value(stringify, value);
+        if (nxt_slow_path(ret != NXT_OK)) {
+            if (ret == NXT_DECLINED) {
+                goto exception;
+            }
+
+            goto memory_error;
+        }
+
+        goto done;
+    }
+
+    stringify->space.length = indent;
+    stringify->space.start = nxt_mem_cache_alloc(vm->mem_cache_pool, indent);
+    if (nxt_slow_path(stringify->space.start == NULL)) {
+        goto memory_error;
+    }
+
+    memset(stringify->space.start, ' ', indent);
+
+    if (nxt_array_init(&stringify->stack, NULL, 4, sizeof(njs_json_state_t),
+                       &njs_array_mem_proto, vm->mem_cache_pool)
+        == NULL)
+    {
+        goto memory_error;
+    }
+
+    if (njs_json_push_stringify_state(vm, stringify, value) == NULL) {
+        goto memory_error;
+    }
+
+    state = stringify->state;
+
+    for ( ;; ) {
+        switch (state->type) {
+        case NJS_JSON_OBJECT_START:
+            njs_json_stringify_append("{", 1);
+            njs_json_stringify_indent(stringify->stack.items + 1);
+            state->type = NJS_JSON_OBJECT_CONTINUE;
+
+            /* Fall through. */
+
+        case NJS_JSON_OBJECT_CONTINUE:
+            if (state->index >= state->keys->length) {
+                njs_json_stringify_indent(stringify->stack.items);
+                njs_json_stringify_append("}", 1);
+
+                state = njs_json_pop_stringify_state(stringify);
+                if (state == NULL) {
+                    goto done;
+                }
+
+                break;
+            }
+
+            key = &state->keys->start[state->index++];
+            njs_string_get(key, &lhq.key);
+            lhq.key_hash = nxt_djb_hash(lhq.key.start, lhq.key.length);
+
+            if (njs_is_external(&state->value)) {
+                lhq.proto = &njs_extern_hash_proto;
+
+                ret = nxt_lvlhsh_find(&state->value.external.proto->hash, &lhq);
+                if (nxt_slow_path(ret == NXT_DECLINED)) {
+                    break;
+                }
+
+                ext_val.type = NJS_EXTERNAL;
+                ext_val.data.truth = 1;
+                ext_val.external.proto = lhq.value;
+
+                val = &ext_val;
+
+            } else {
+                lhq.proto = &njs_object_hash_proto;
+
+                ret = nxt_lvlhsh_find(&state->value.data.u.object->hash, &lhq);
+                if (nxt_slow_path(ret == NXT_DECLINED)) {
+                    break;
+                }
+
+                prop = lhq.value;
+                val = &prop->value;
+
+                if (!prop->enumerable || njs_is_invalid(val)) {
+                    break;
+                }
+            }
+
+            if (state->written) {
+                njs_json_stringify_append(",", 1);
+                njs_json_stringify_indent(stringify->stack.items + 1);
+            }
+
+            state->written = 1;
+            njs_json_stringify_append((char *) lhq.key.start, lhq.key.length);
+            njs_json_stringify_append(":", 1);
+            if (stringify->space.length != 0) {
+                njs_json_stringify_append(" ", 1);
+            }
+
+            if (njs_dump_is_object(val)) {
+                state = njs_json_push_stringify_state(vm, stringify, val);
+                if (state == NULL) {
+                    goto exception;
+                }
+
+                break;
+            }
+
+            njs_dump_append_value(val);
+
+            break;
+
+        case NJS_JSON_ARRAY_START:
+            njs_json_stringify_append("[", 1);
+            njs_json_stringify_indent(stringify->stack.items + 1);
+            state->type = NJS_JSON_ARRAY_CONTINUE;
+
+            /* Fall through. */
+
+        case NJS_JSON_ARRAY_CONTINUE:
+            if (state->index >= state->value.data.u.array->length) {
+                njs_json_stringify_indent(stringify->stack.items);
+                njs_json_stringify_append("]", 1);
+
+                state = njs_json_pop_stringify_state(stringify);
+                if (state == NULL) {
+                    goto done;
+                }
+
+                break;
+            }
+
+            if (state->written) {
+                njs_json_stringify_append(",", 1);
+                njs_json_stringify_indent(stringify->stack.items + 1);
+            }
+
+            val = &state->value.data.u.array->start[state->index++];
+
+            if (njs_dump_is_object(val)) {
+                state = njs_json_push_stringify_state(vm, stringify, val);
+                if (state == NULL) {
+                    goto exception;
+                }
+
+                break;
+            }
+
+            njs_dump_append_value(val);
+
+            break;
+
+        default:
+            nxt_unreachable();
+        }
+    }
+
+done:
+
+    ret = njs_json_buf_pullup(stringify, &str);
+    if (nxt_slow_path(ret != NXT_OK)) {
+        goto memory_error;
+    }
+
+    *retval = str;
+
+    return NXT_OK;
+
+memory_error:
+
+    njs_memory_error(vm);
+
+exception:
+
+    njs_vm_value_to_ext_string(vm, retval, &vm->retval, 1);
+
+    return NXT_OK;
+}
diff --git a/njs/njs_regexp.c b/njs/njs_regexp.c
--- a/njs/njs_regexp.c
+++ b/njs/njs_regexp.c
@@ -527,22 +527,8 @@ static njs_ret_t
 njs_regexp_prototype_to_string(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused)
 {
-    u_char                *source;
-    int32_t               length;
-    uint32_t              size;
-    njs_value_t           *value;
-    njs_regexp_pattern_t  *pattern;
-
-    value = &args[0];
-
-    if (njs_is_regexp(value)) {
-        pattern = value->data.u.regexp->pattern;
-        source = pattern->source;
-
-        size = strlen((char *) source);
-        length = nxt_utf8_length(source, size);
-
-        return njs_regexp_string_create(vm, &vm->retval, source, size, length);
+    if (njs_is_regexp(&args[0])) {
+        return njs_regexp_to_string(vm, &vm->retval, &args[0]);
     }
 
     njs_type_error(vm, "'this' argument is not a regexp");
@@ -551,6 +537,25 @@ njs_regexp_prototype_to_string(njs_vm_t 
 }
 
 
+njs_ret_t
+njs_regexp_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *value)
+{
+    u_char                *source;
+    int32_t               length;
+    uint32_t              size;
+    njs_regexp_pattern_t  *pattern;
+
+    pattern = value->data.u.regexp->pattern;
+    source = pattern->source;
+
+    size = strlen((char *) source);
+    length = nxt_utf8_length(source, size);
+
+    return njs_regexp_string_create(vm, retval, source, size, length);
+}
+
+
 static njs_ret_t
 njs_regexp_prototype_test(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
     njs_index_t unused)
diff --git a/njs/njs_regexp.h b/njs/njs_regexp.h
--- a/njs/njs_regexp.h
+++ b/njs/njs_regexp.h
@@ -31,6 +31,8 @@ njs_regexp_t *njs_regexp_alloc(njs_vm_t 
 njs_ret_t njs_regexp_prototype_exec(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
+njs_ret_t njs_regexp_to_string(njs_vm_t *vm, njs_value_t *retval,
+    const njs_value_t *regexp);
 
 extern const njs_object_init_t  njs_regexp_constructor_init;
 extern const njs_object_init_t  njs_regexp_prototype_init;
diff --git a/njs/njs_shell.c b/njs/njs_shell.c
--- a/njs/njs_shell.c
+++ b/njs/njs_shell.c
@@ -58,6 +58,8 @@ static char *njs_completion_generator(co
 
 static njs_ret_t njs_ext_console_log(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
+static njs_ret_t njs_ext_console_dump(njs_vm_t *vm, njs_value_t *args,
+    nxt_uint_t nargs, njs_index_t unused);
 static njs_ret_t njs_ext_console_help(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused);
 
@@ -76,6 +78,18 @@ static njs_external_t  njs_ext_console[]
       njs_ext_console_log,
       0 },
 
+    { nxt_string("dump"),
+      NJS_EXTERN_METHOD,
+      NULL,
+      0,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      njs_ext_console_dump,
+      0 },
+
     { nxt_string("help"),
       NJS_EXTERN_METHOD,
       NULL,
@@ -431,7 +445,7 @@ njs_process_script(njs_vm_t *vm, njs_opt
         ret = njs_vm_run(vm);
     }
 
-    if (njs_vm_retval_to_ext_string(vm, out) != NXT_OK) {
+    if (njs_vm_value_dump(vm, out, njs_vm_retval(vm), 1) != NXT_OK) {
         *out = nxt_string_value("failed to get retval from VM");
         return NXT_ERROR;
     }
@@ -625,7 +639,38 @@ njs_ext_console_log(njs_vm_t *vm, njs_va
     n = 1;
 
     while (n < nargs) {
-        if (njs_vm_value_to_ext_string(vm, &msg, njs_argument(args, n), 0)
+        if (njs_vm_value_dump(vm, &msg, njs_argument(args, n), 0)
+            == NJS_ERROR)
+        {
+            return NJS_ERROR;
+        }
+
+        printf("%s%.*s", (n != 1) ? " " : "", (int) msg.length, msg.start);
+
+        n++;
+    }
+
+    if (nargs > 1) {
+        printf("\n");
+    }
+
+    vm->retval = njs_value_void;
+
+    return NJS_OK;
+}
+
+
+static njs_ret_t
+njs_ext_console_dump(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
+    njs_index_t unused)
+{
+    nxt_str_t   msg;
+    nxt_uint_t  n;
+
+    n = 1;
+
+    while (n < nargs) {
+        if (njs_vm_value_dump(vm, &msg, njs_argument(args, n), 1)
             == NJS_ERROR)
         {
             return NJS_ERROR;
diff --git a/njs/njs_vm.h b/njs/njs_vm.h
--- a/njs/njs_vm.h
+++ b/njs/njs_vm.h
@@ -562,6 +562,10 @@ typedef njs_ret_t (*njs_vmcode_operation
     ((value)->type != NJS_INVALID)
 
 
+#define njs_is_invalid(value)                                                 \
+    ((value)->type == NJS_INVALID)
+
+
 #define njs_set_invalid(value)                                                \
     (value)->type = NJS_INVALID
 
diff --git a/njs/test/njs_expect_test.exp b/njs/test/njs_expect_test.exp
--- a/njs/test/njs_expect_test.exp
+++ b/njs/test/njs_expect_test.exp
@@ -166,7 +166,13 @@ njs_test {
     {"console.log(1)\r\n"
      "console.log(1)\r\n1\r\nundefined\r\n>> "}
     {"console.log(1, 'a')\r\n"
-     "console.log(1, 'a')\r\n1 a\r\nundefined\r\n>> "}
+     "console.log(1, 'a')\r\n1 'a'\r\nundefined\r\n>> "}
+    {"console.dump()\r\n"
+     "console.dump()\r\nundefined\r\n>> "}
+    {"console.dump(1)\r\n"
+     "console.dump(1)\r\n1\r\nundefined\r\n>> "}
+    {"console.dump(1, 'a')\r\n"
+     "console.dump(1, 'a')\r\n1 'a'\r\nundefined\r\n>> "}
     {"console.help()\r\n"
      "console.help()\r\nVM built-in objects:"}
 }
@@ -178,21 +184,50 @@ njs_test {
 
 njs_test {
     {"var print = console.log.bind(console); print(1, 'a', [1, 2])\r\n"
-     "var print = console.log.bind(console); print(1, 'a', \\\[1, 2])\r\n1 a 1,2\r\nundefined\r\n>> "}
+     "1 'a' \\\[1,2]\r\nundefined\r\n>> "}
+    {"var print = console.dump.bind(console); print(1, 'a', [1, 2])\r\n"
+     "1 'a' \\\[\r\n 1,\r\n 2\r\n]\r\nundefined\r\n>> "}
 }
 
 # Backtraces for external objects
 njs_test {
-    {"console.log(console)\r\n"
-     "console.log(console)\r\nTypeError:*at console.log (native)"}
+    {"console.log(console.a.a)\r\n"
+     "console.log(console.a.a)\r\nTypeError:*at console.log (native)"}
 }
 
-# Exception in njs_vm_retval_to_ext_string()
+# dumper
 njs_test {
-    {"var o = { toString: function() { return [1] } }\r\n"
+    {"var o = {toString: function(){}, log: console.log}\r\n"
      "undefined\r\n>> "}
     {"o\r\n"
-     "TypeError: cannot evaluate an object's value"}
+     "o\r\n{\r\n toString: \\\[Function],\r\n log: \\\[Function: native]\r\n}"}
+}
+
+njs_test {
+    {"[1, new Number(2), 'a', new String('αβZγ'), true, new Boolean(false)]\r\n"
+     "\\\[\r\n 1,\r\n \\\[Number: 2],\r\n 'a',\r\n \\\[String: 'αβZγ'],\r\n true,\r\n \\\[Boolean: false]\r\n]"}
+}
+
+njs_test {
+    {"[undefined,,null]\r\n"
+     "\\\[\r\n undefined,\r\n <empty>,\r\n null\r\n]"}
+}
+
+njs_test {
+    {"[InternalError(),TypeError('msg'), new RegExp(), /^undef$/m, new Date(0)]\r\n"
+     "\\\[\r\n InternalError,\r\n TypeError: msg,\r\n /(?:)/,\r\n /^undef$/m,\r\n 1970-01-01T00:00:00.000Z\r\n]"}
+}
+
+# dumper excapes special characters as JSON.stringify()
+# except '\"'
+njs_test {
+    {"\"\\r\\0\\\"\"\r\n"
+     "'\\\\r\\\\u0000\"'"}
+}
+
+njs_test {
+    {"[{a:1}]\r\n"
+     "\r\n\\\[\r\n {\r\n  a: 1\r\n }\r\n]"}
 }
 
 # Backtraces are reset between invocations
@@ -237,21 +272,21 @@ njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFile('njs_test_file', 'utf8', function (e, data) {console.log(data[2]+data.length)})\r\n"
-     "Z4\r\nundefined\r\n>> "}
+     "'Z4'\r\nundefined\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFile('njs_test_file', function (e, data) {console.log(data[4]+data.length)})\r\n"
-     "Z7\r\nundefined\r\n>> "}
+     "'Z7'\r\nundefined\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFile('njs_test_file', {encoding:'utf8',flag:'r+'}, function (e, data) {console.log(data)})\r\n"
-     "αβZγ\r\nundefined\r\n>> "}
+     "'αβZγ'\r\nundefined\r\n>> "}
 }
 
 exec rm -fr njs_unknown_path
@@ -260,7 +295,7 @@ njs_test {
     {"var fs = require('fs'); \r\n"
      "undefined\r\n>> "}
     {"fs.readFile('njs_unknown_path', 'utf8', function (e) {console.log(JSON.stringify(e))})\r\n"
-     "{\"errno\":2,\"path\":\"njs_unknown_path\",\"syscall\":\"open\"}\r\nundefined\r\n>> "}
+     "'{\"errno\":2,\"path\":\"njs_unknown_path\",\"syscall\":\"open\"}'\r\nundefined\r\n>> "}
 }
 
 njs_test {
@@ -276,35 +311,35 @@ njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file').toString('base64')\r\n"
-     "zrHOslrOsw==\r\n>> "}
+     "'zrHOslrOsw=='\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file', 'utf8')[2]\r\n"
-     "Z\r\n>> "}
+     "'Z'\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file')[4]\r\n"
-     "Z\r\n>> "}
+     "'Z'\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file', {encoding:'utf8',flag:'r+'})\r\n"
-     "αβZγ\r\n>> "}
+     "'αβZγ'\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs'); \r\n"
      "undefined\r\n>> "}
     {"try { fs.readFileSync('njs_unknown_path')} catch (e) {console.log(JSON.stringify(e))}\r\n"
-     "{\"errno\":2,\"path\":\"njs_unknown_path\",\"syscall\":\"open\"}\r\nundefined\r\n>> "}
+     "'{\"errno\":2,\"path\":\"njs_unknown_path\",\"syscall\":\"open\"}'\r\nundefined\r\n>> "}
 }
 
 njs_test {
@@ -332,21 +367,21 @@ njs_test {
     {"function h1(e) {if (e) {throw e}; console.log(fs.readFileSync('njs_test_file2'))}\r\n"
      "undefined\r\n>> "}
     {"fs.writeFile('njs_test_file2', 'ABC', h1)\r\n"
-     "ABC\r\nundefined\r\n>> "}
+     "'ABC'\r\nundefined\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.writeFile('njs_test_file2', 'ABC', 'utf8', function (e) { if (e) {throw e}; console.log(fs.readFileSync('njs_test_file2'))})\r\n"
-     "ABC\r\nundefined\r\n>> "}
+     "'ABC'\r\nundefined\r\n>> "}
 }
 
 njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.writeFile('njs_test_file2', 'ABC', {encoding:'utf8', mode:0o666}, function (e) { if (e) {throw e}; console.log(fs.readFileSync('njs_test_file2'))})\r\n"
-     "ABC\r\nundefined\r\n>> "}
+     "'ABC'\r\nundefined\r\n>> "}
 }
 
 exec rm -fr njs_wo_file
@@ -362,7 +397,7 @@ njs_test {
     {"var fs = require('fs')\r\n"
      "undefined\r\n>> "}
     {"fs.writeFile('/invalid_path', 'ABC', function (e) { console.log(JSON.stringify(e))})\r\n"
-     "{\"errno\":13,\"path\":\"/invalid_path\",\"syscall\":\"open\"}\r\nundefined\r\n>> "}
+     "'{\"errno\":13,\"path\":\"/invalid_path\",\"syscall\":\"open\"}'\r\nundefined\r\n>> "}
 }
 
 # require('fs').writeFileSync()
@@ -375,7 +410,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 njs_test {
@@ -384,7 +419,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC', 'utf8')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 njs_test {
@@ -395,7 +430,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 njs_test {
@@ -404,7 +439,7 @@ njs_test {
     {"fs.writeFileSync('njs_test_file2', 'ABC', {encoding:'utf8', mode:0o666})\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABC\r\n>> "}
+     "'ABC'\r\n>> "}
 }
 
 exec rm -fr njs_wo_file
@@ -428,7 +463,7 @@ njs_test {
     {"function h2(e) {fs.appendFile('njs_test_file2', 'ABC', h1)}\r\n"
      "undefined\r\n>> "}
     {"fs.appendFile('njs_test_file2', 'ABC', h2)\r\n"
-     "ABCABC\r\nundefined\r\n>> "}
+     "'ABCABC'\r\nundefined\r\n>> "}
 }
 
 # require('fs').appendFileSync()
@@ -443,5 +478,5 @@ njs_test {
     {"fs.appendFileSync('njs_test_file2', 'ABC')\r\n"
      "undefined\r\n>> "}
     {"fs.readFileSync('njs_test_file2')\r\n"
-     "ABCABC\r\n>> "}
+     "'ABCABC'\r\n>> "}
 }
diff --git a/njs/test/njs_unit_test.c b/njs/test/njs_unit_test.c
--- a/njs/test/njs_unit_test.c
+++ b/njs/test/njs_unit_test.c
@@ -9258,6 +9258,17 @@ static njs_unit_test_t  njs_test[] =
     { nxt_string("var a = {}; a.a = a; JSON.stringify(a)"),
       nxt_string("TypeError: Nested too deep or a cyclic structure") },
 
+    /* njs.dump(). */
+
+    { nxt_string("njs.dump({a:1, b:[1,,2,{c:new Boolean(1)}]})"),
+      nxt_string("{a:1,b:[1,<empty>,2,{c:[Boolean: true]}]}") },
+
+    { nxt_string("njs.dump($r.props)"),
+      nxt_string("{a:{type:\"property\",props:[\"getter\"]},b:{type:\"property\",props:[\"getter\"]}}") },
+
+    { nxt_string("njs.dump($r.header)"),
+      nxt_string("{type:\"object\",props:[\"getter\",\"foreach\",\"next\"]}") },
+
     /* require(). */
 
     { nxt_string("require('unknown_module')"),

@xeioex xeioex reopened this Jul 26, 2018
@drsm
Copy link
Contributor Author

drsm commented Jul 26, 2018

Hi @xeioex
the patch works fine.
except a minor bug with missing stack trace, when non Error() instance was thrown:

>> (function() { throw 'test' })()
'test'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants