diff --git a/Makefile b/Makefile index f27c0d68..9799e7a7 100644 --- a/Makefile +++ b/Makefile @@ -131,6 +131,7 @@ DUKTAPE_SOURCES_SEPARATE = \ $(DISTSRCSEP)/duk_bi_object.c \ $(DISTSRCSEP)/duk_bi_regexp.c \ $(DISTSRCSEP)/duk_bi_string.c \ + $(DISTSRCSEP)/duk_bi_proxy.c \ $(DISTSRCSEP)/duk_bi_buffer.c \ $(DISTSRCSEP)/duk_bi_pointer.c \ $(DISTSRCSEP)/duk_bi_logger.c \ @@ -185,9 +186,10 @@ CCOPTS_SHARED += -DDUK_OPT_DEBUG_BUFSIZE=512 #CCOPTS_SHARED += -DDUK_OPT_NO_JSONC #CCOPTS_SHARED += -DDUK_OPT_FUNC_NONSTD_CALLER_PROPERTY #CCOPTS_SHARED += -DDUK_OPT_FUNC_NONSTD_SOURCE_PROPERTY +#CCOPTS_SHARED += -DDUK_OPT_NO_ARRAY_SPLICE_NONSTD_DELCOUNT #CCOPTS_SHARED += -DDUK_OPT_NO_OBJECT_ES6_PROTO_PROPERTY #CCOPTS_SHARED += -DDUK_OPT_NO_OBJECT_ES6_SETPROTOTYPEOF -#CCOPTS_SHARED += -DDUK_OPT_NO_ARRAY_SPLICE_NONSTD_DELCOUNT +#CCOPTS_SHARED += -DDUK_OPT_NO_ES6_PROXY #CCOPTS_SHARED += -DDUK_OPT_NO_ZERO_BUFFER_DATA #CCOPTS_SHARED += -DDUK_CMDLINE_BAREBONES CCOPTS_NONDEBUG = $(CCOPTS_SHARED) -Os -fomit-frame-pointer diff --git a/RELEASES.txt b/RELEASES.txt index b66d6974..c63d55ab 100644 --- a/RELEASES.txt +++ b/RELEASES.txt @@ -306,6 +306,14 @@ Planned * Add Object.setPrototypeOf() and Object.prototype.__proto__, both borrowed from ES6 draft, to improve internal prototype handling +* Add proxy objects borrowed from ES6 draft to allow property virtualization + (subset implementation limited to 'get', 'set', and 'deleteProperty' + handler methods) + +* Fix a 'delete' bug: if delete target was a string and key was 'length' or + a valid character index, value stack was left in an inconsistent state in + non-strict mode (caused an assertion failure) + * C typing wrapped throughout to allow porting to more exotic platforms, e.g. platforms where "int" is a 16-bit type diff --git a/api-testcases/test-bug-push-buffer-maxsize.c b/api-testcases/test-bug-push-buffer-maxsize.c index 9d1d1b4d..31bf896f 100644 --- a/api-testcases/test-bug-push-buffer-maxsize.c +++ b/api-testcases/test-bug-push-buffer-maxsize.c @@ -1,25 +1,26 @@ /*=== *** test_1a fixed size buffer, maximum size_t (should fail) -rc=1, result='Error: failed to allocate buffer' +rc=1, result='RangeError: buffer too long' *** test_1b fixed size buffer, maximum size_t - 8 (should fail) -rc=1, result='Error: failed to allocate buffer' +rc=1, result='RangeError: buffer too long' *** test_2a dynamic size buffer, maximum size_t (should fail) -rc=1, result='Error: failed to allocate buffer' +rc=1, result='RangeError: buffer too long' *** test_2b dynamic size buffer, maximum size_t - 8 (should fail) -rc=1, result='Error: failed to allocate buffer' +rc=1, result='RangeError: buffer too long' ===*/ -/* Attempt to allocate a buffer of maximum size_t (or anything so close that - * when the heap header size is added, the result overflows) causes a spurious - * successful allocation now. The allocation will in fact be too little to - * even contain the heap header but will appear to succeed. +/* Before Duktape 0.9.0, an attempt to allocate a buffer of maximum size_t + * (or anything so close that when the heap header size is added, the result + * overflows) causes a spurious successful allocation now. The allocation + * will in fact be too little to even contain the heap header but will appear + * to succeed. * - * The fix is to check for a maximum size before adding the header size to - * the requested size. + * The proper behavior is to check for a maximum size before adding the header + * size to the requested size (this is done now). */ #ifndef SIZE_MAX @@ -96,4 +97,3 @@ void test(duk_context *ctx) { TEST(test_2a); TEST(test_2b); } - diff --git a/api-testcases/test-bug-push-string-maxsize.c b/api-testcases/test-bug-push-string-maxsize.c index a173cd8a..788a836c 100644 --- a/api-testcases/test-bug-push-string-maxsize.c +++ b/api-testcases/test-bug-push-string-maxsize.c @@ -1,10 +1,15 @@ /*=== -FIXME: check when fixed +*** test_1a +push string with maximum size_t (should fail) +rc=1, result='RangeError: string too long' +*** test_1b +push string with maximum size_t - 8 (should fail) +rc=1, result='RangeError: string too long' ===*/ /* Same as test-bug-push-buffer-maxsize.c but for string pushing. * - * There are actually two bugs in the current implementation: + * There were actually two bugs in the implementation previously: * (1) the size computation for the header plus string may overflow, * and (2) the string size is passed as a duk_u32 internally which * clamps incorrectly on 64-bit platforms. @@ -12,6 +17,9 @@ FIXME: check when fixed * The attempt to push a string of SIZE_MAX (or close) should fail * before the string data is actually read (there isn't enough data, * of course, if that were to happen). + * + * The fix, now implemented, is to check for string maximum size + * explicitly. */ #ifndef SIZE_MAX diff --git a/api-testcases/test-bug-set-top-wrap.c b/api-testcases/test-bug-set-top-wrap.c index ebd45a4d..4e46fde8 100644 --- a/api-testcases/test-bug-set-top-wrap.c +++ b/api-testcases/test-bug-set-top-wrap.c @@ -1,8 +1,8 @@ /*=== top=0 -rc=1, result=Error: invalid index: 536870912 +rc=1, result=Error: invalid index top=0 -rc=1, result=Error: invalid index: 357913942 +rc=1, result=Error: invalid index ===*/ int test_1(duk_context *ctx) { diff --git a/api-testcases/test-del-prop.c b/api-testcases/test-del-prop.c index 2af9c2a5..96cc4635 100644 --- a/api-testcases/test-del-prop.c +++ b/api-testcases/test-del-prop.c @@ -10,7 +10,7 @@ delete 'test_string'['5'] -> rc=0 delete 'test_string'.length -> rc=0 final object: {"bar":"barval"} final array: ["foo","bar",null] -final top: 7 +final top: 3 ==> rc=0, result='undefined' *** test_1b (duk_pcall) ==> rc=1, result='TypeError: property not configurable' @@ -33,16 +33,16 @@ delete 'test_string'['5'] -> rc=0 delete 'test_string'.length -> rc=0 final object: {"bar":"barval"} final array: ["foo","bar",null] -final top: 7 +final top: 3 ==> rc=0, result='undefined' *** test_2b (duk_pcall) ==> rc=1, result='TypeError: property not configurable' *** test_2c (duk_pcall) ==> rc=1, result='TypeError: property not configurable' *** test_2d (duk_safe_call) -==> rc=1, result='Error: invalid index: 234' +==> rc=1, result='Error: invalid index' *** test_2e (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' *** test_3a (duk_safe_call) delete obj[31337] -> rc=1 delete obj[123] -> rc=1 @@ -51,14 +51,14 @@ delete arr[2] -> rc=1 delete 'test_string'[5] -> rc=0 final object: {"foo":"fooval","bar":"barval"} final array: ["foo","bar",null] -final top: 5 +final top: 3 ==> rc=0, result='undefined' *** test_3b (duk_pcall) ==> rc=1, result='TypeError: property not configurable' *** test_3c (duk_safe_call) -==> rc=1, result='Error: invalid index: 234' +==> rc=1, result='Error: invalid index' *** test_3d (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ void prep(duk_context *ctx) { @@ -389,15 +389,11 @@ void test(duk_context *ctx) { TEST_PCALL(test_2b); TEST_PCALL(test_2c); TEST_SAFE_CALL(test_2d); - /* FIXME: currently error message contains the actual DUK_INVALID_INDEX - * value, nonportable */ TEST_SAFE_CALL(test_2e); TEST_SAFE_CALL(test_3a); TEST_PCALL(test_3b); TEST_SAFE_CALL(test_3c); - /* FIXME: currently error message contains the actual DUK_INVALID_INDEX - * value, nonportable */ TEST_SAFE_CALL(test_3d); } diff --git a/api-testcases/test-get-prop.c b/api-testcases/test-get-prop.c index c524df0a..7ada6c96 100644 --- a/api-testcases/test-get-prop.c +++ b/api-testcases/test-get-prop.c @@ -33,9 +33,9 @@ arr.length -> rc=1, result='3' final top: 3 ==> rc=0, result='undefined' *** test_2b (duk_safe_call) -==> rc=1, result='Error: invalid index: 234' +==> rc=1, result='Error: invalid index' *** test_2c (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' *** test_3a (duk_safe_call) obj[31337] -> rc=0, result='undefined' obj[123] -> rc=1, result='123val' @@ -45,9 +45,9 @@ arr[2] -> rc=1, result='quux' final top: 3 ==> rc=0, result='undefined' *** test_3b (duk_safe_call) -==> rc=1, result='Error: invalid index: 234' +==> rc=1, result='Error: invalid index' *** test_3c (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ void prep(duk_context *ctx) { @@ -334,14 +334,10 @@ void test(duk_context *ctx) { TEST_SAFE_CALL(test_2a); TEST_SAFE_CALL(test_2b); - /* FIXME: currently error message contains the actual DUK_INVALID_INDEX - * value, nonportable */ TEST_SAFE_CALL(test_2c); TEST_SAFE_CALL(test_3a); TEST_SAFE_CALL(test_3b); - /* FIXME: currently error message contains the actual DUK_INVALID_INDEX - * value, nonportable */ TEST_SAFE_CALL(test_3c); } diff --git a/api-testcases/test-has-prop.c b/api-testcases/test-has-prop.c index 5c0e3afa..71122b16 100644 --- a/api-testcases/test-has-prop.c +++ b/api-testcases/test-has-prop.c @@ -26,9 +26,9 @@ arr.length -> rc=1 final top: 3 ==> rc=0, result='undefined' *** test_2b (duk_safe_call) -==> rc=1, result='Error: invalid index: 234' +==> rc=1, result='Error: invalid index' *** test_2c (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' *** test_3a (duk_safe_call) obj[31337] -> rc=0 obj[123] -> rc=1 @@ -37,9 +37,9 @@ arr[2] -> rc=1 final top: 3 ==> rc=0, result='undefined' *** test_3b (duk_safe_call) -==> rc=1, result='Error: invalid index: 234' +==> rc=1, result='Error: invalid index' *** test_3c (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ void prep(duk_context *ctx) { @@ -260,14 +260,10 @@ void test(duk_context *ctx) { TEST_SAFE_CALL(test_2a); TEST_SAFE_CALL(test_2b); - /* FIXME: currently error message contains the actual DUK_INVALID_INDEX - * value, nonportable */ TEST_SAFE_CALL(test_2c); TEST_SAFE_CALL(test_3a); TEST_SAFE_CALL(test_3b); - /* FIXME: currently error message contains the actual DUK_INVALID_INDEX - * value, nonportable */ TEST_SAFE_CALL(test_3c); } diff --git a/api-testcases/test-normalize-index.c b/api-testcases/test-normalize-index.c index b11bcb47..b12d18ed 100644 --- a/api-testcases/test-normalize-index.c +++ b/api-testcases/test-normalize-index.c @@ -11,9 +11,9 @@ top=3, idx=3, duk_normalize_index -> DUK_INVALID_INDEX top=3, idx=4, duk_normalize_index -> DUK_INVALID_INDEX top=3, idx=5, duk_normalize_index -> DUK_INVALID_INDEX req_norm_idx: top 3 after popping arg -idx=-5 -> duk_require_normalize_index -> Error: invalid index: -5 +idx=-5 -> duk_require_normalize_index -> Error: invalid index req_norm_idx: top 3 after popping arg -idx=-4 -> duk_require_normalize_index -> Error: invalid index: -4 +idx=-4 -> duk_require_normalize_index -> Error: invalid index req_norm_idx: top 3 after popping arg idx=-3 -> duk_require_normalize_index -> 0 req_norm_idx: top 3 after popping arg @@ -27,11 +27,11 @@ idx=1 -> duk_require_normalize_index -> 1 req_norm_idx: top 3 after popping arg idx=2 -> duk_require_normalize_index -> 2 req_norm_idx: top 3 after popping arg -idx=3 -> duk_require_normalize_index -> Error: invalid index: 3 +idx=3 -> duk_require_normalize_index -> Error: invalid index req_norm_idx: top 3 after popping arg -idx=4 -> duk_require_normalize_index -> Error: invalid index: 4 +idx=4 -> duk_require_normalize_index -> Error: invalid index req_norm_idx: top 3 after popping arg -idx=5 -> duk_require_normalize_index -> Error: invalid index: 5 +idx=5 -> duk_require_normalize_index -> Error: invalid index ===*/ int req_norm_idx(duk_context *ctx) { diff --git a/api-testcases/test-pcall-prop.c b/api-testcases/test-pcall-prop.c index 3b769a69..4c002c51 100644 --- a/api-testcases/test-pcall-prop.c +++ b/api-testcases/test-pcall-prop.c @@ -21,7 +21,7 @@ rc=1, result='TypeError: invalid base reference for property read' rc=1, result='RangeError: getter error' ==> rc=0, result='undefined' *** test_7 (duk_safe_call) -rc=1, result='Error: invalid index: -6' +rc=1, result='Error: invalid index' ==> rc=0, result='undefined' *** test_8 (duk_safe_call) rc=1, result='TypeError: call target not callable' diff --git a/api-testcases/test-proxy-basic.c b/api-testcases/test-proxy-basic.c new file mode 100644 index 00000000..e7f5748d --- /dev/null +++ b/api-testcases/test-proxy-basic.c @@ -0,0 +1,155 @@ +/* + * ES6 Proxy handlers can also be native Duktape/C functions. + * + * Just a very basic test to ensure proxy handlers work as expected. + */ + +/*=== +*** test_1 (duk_safe_call) +top: 0 +top: 2 +handle_get: key=getTest +get result: rc=1, value=123 +top: 2 +handle_get: key=_getTest +get result: rc=1, value=fake_value +top: 2 +handle_set: key=setTest, val=testValue +set result: rc=1 +top: 2 +handle_set: key=_setTest, val=testValue +set result: rc=0 +top: 2 +handle_delete: key=deleteTest +delete result: rc=1 +top: 2 +handle_delete: key=_deleteTest +delete result: rc=0 +top: 2 +final top: 0 +==> rc=0, result='undefined' +===*/ + +static int handle_get(duk_context *ctx) { + /* 'this' binding: handler + * [0]: target + * [1]: key + * [2]: receiver (proxy) + */ + + const char *key = duk_to_string(ctx, 1); + + printf("handle_get: key=%s\n", key); + + if (key != NULL && key[0] == '_') { + /* Provide a fake value for properties beginning with an underscore. */ + duk_push_string(ctx, "fake_value"); + } else { + /* For others, read from target. */ + duk_dup(ctx, 1); + duk_get_prop(ctx, 0); + } + return 1; +} + +static int handle_set(duk_context *ctx) { + /* 'this' binding: handler + * [0]: target + * [1]: key + * [2]: val + * [3]: receiver (proxy) + */ + + const char *key = duk_to_string(ctx, 1); + const char *val = duk_to_string(ctx, 2); + + printf("handle_set: key=%s, val=%s\n", key, val); + + if (key != NULL && key[0] == '_') { + /* Indicate set failure for properties beginning with underscore. */ + duk_push_false(ctx); + } else { + duk_push_true(ctx); + } + return 1; +} + +static int handle_delete(duk_context *ctx) { + /* 'this' binding: handler + * [0]: target + * [1]: key + */ + + const char *key = duk_to_string(ctx, 1); + + printf("handle_delete: key=%s\n", key); + + if (key != NULL && key[0] == '_') { + /* Indicate delete failure for properties beginning with underscore. */ + duk_push_false(ctx); + } else { + duk_push_true(ctx); + } + return 1; +} + +static const duk_function_list_entry handler_funcs[] = { + { "get", handle_get, 3 }, + { "set", handle_set, 4 }, + { "deleteProperty", handle_delete, 2 }, + { NULL, NULL, 0 } +}; + +static int test_1(duk_context *ctx) { + int rc; + + printf("top: %d\n", duk_get_top(ctx)); + + /* new Proxy(target, handler) */ + duk_push_global_object(ctx); + duk_get_prop_string(ctx, -1, "Proxy"); + duk_push_object(ctx); /* target */ + duk_push_number(ctx, 123); + duk_put_prop_string(ctx, -2, "getTest"); + duk_push_object(ctx); /* handler */ + duk_put_function_list(ctx, -1, handler_funcs); + duk_new(ctx, 2); /* [ global Proxy target handler ] -> [ global proxy_object ] */ + + printf("top: %d\n", duk_get_top(ctx)); + rc = duk_get_prop_string(ctx, -1, "getTest"); + printf("get result: rc=%d, value=%s\n", rc, duk_to_string(ctx, -1)); + duk_pop(ctx); + + printf("top: %d\n", duk_get_top(ctx)); + rc = duk_get_prop_string(ctx, -1, "_getTest"); + printf("get result: rc=%d, value=%s\n", rc, duk_to_string(ctx, -1)); + duk_pop(ctx); + + printf("top: %d\n", duk_get_top(ctx)); + duk_push_string(ctx, "testValue"); + rc = duk_put_prop_string(ctx, -2, "setTest"); + printf("set result: rc=%d\n", rc); + + printf("top: %d\n", duk_get_top(ctx)); + duk_push_string(ctx, "testValue"); + rc = duk_put_prop_string(ctx, -2, "_setTest"); + printf("set result: rc=%d\n", rc); + + printf("top: %d\n", duk_get_top(ctx)); + rc = duk_del_prop_string(ctx, -1, "deleteTest"); + printf("delete result: rc=%d\n", rc); + + printf("top: %d\n", duk_get_top(ctx)); + rc = duk_del_prop_string(ctx, -1, "_deleteTest"); + printf("delete result: rc=%d\n", rc); + + printf("top: %d\n", duk_get_top(ctx)); + duk_pop_2(ctx); + printf("final top: %d\n", duk_get_top(ctx)); + + return 0; +} + +void test(duk_context *ctx) { + TEST_SAFE_CALL(test_1); +} diff --git a/api-testcases/test-substring.c b/api-testcases/test-substring.c index a1d8eab9..e51e0fbc 100644 --- a/api-testcases/test-substring.c +++ b/api-testcases/test-substring.c @@ -9,9 +9,9 @@ final top: 1 *** test_2 (duk_safe_call) ==> rc=1, result='TypeError: incorrect type, expected tag 5' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2' +==> rc=1, result='Error: invalid index' *** test_4 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ void dump_string(duk_context *ctx) { @@ -112,6 +112,6 @@ void test(duk_context *ctx) { TEST_SAFE_CALL(test_1); TEST_SAFE_CALL(test_2); TEST_SAFE_CALL(test_3); - TEST_SAFE_CALL(test_4); /* FIXME: exposes value of DUK_INVALID_INDEX */ + TEST_SAFE_CALL(test_4); } diff --git a/api-testcases/test-to-boolean.c b/api-testcases/test-to-boolean.c index eb6ca664..6f82ef9b 100644 --- a/api-testcases/test-to-boolean.c +++ b/api-testcases/test-to-boolean.c @@ -21,9 +21,9 @@ index 16, boolean: 0 index 17, boolean: 1 ==> rc=0, result='undefined' *** test_2 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ int test_1(duk_context *ctx) { diff --git a/api-testcases/test-to-buffer.c b/api-testcases/test-to-buffer.c index 1745620e..dbc02526 100644 --- a/api-testcases/test-to-buffer.c +++ b/api-testcases/test-to-buffer.c @@ -41,9 +41,9 @@ index 18, type 8 -> 7, ptr-is-NULL 0, size 10 buffer: dynamic=0, size=10: 0xdeadbeef ==> rc=0, result='undefined' *** test_2 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ void dump_buffer(duk_context *ctx) { diff --git a/api-testcases/test-to-defaultvalue.c b/api-testcases/test-to-defaultvalue.c index fdb1df46..d44fdfe3 100644 --- a/api-testcases/test-to-defaultvalue.c +++ b/api-testcases/test-to-defaultvalue.c @@ -8,9 +8,9 @@ index 2, type 6 -> 3, result: true *** test_2 (duk_safe_call) ==> rc=1, result='TypeError: not object' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_4 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ /* FIXME: this test is missing a lot of coverage, like different hints, diff --git a/api-testcases/test-to-fixed-buffer.c b/api-testcases/test-to-fixed-buffer.c index 01628c85..b5139701 100644 --- a/api-testcases/test-to-fixed-buffer.c +++ b/api-testcases/test-to-fixed-buffer.c @@ -13,9 +13,9 @@ final top: 1 *** test_3 (duk_safe_call) ==> rc=1, result='TypeError: incorrect type, expected tag 7' *** test_4 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_5 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ void dump_buffer(duk_context *ctx) { @@ -106,6 +106,6 @@ void test(duk_context *ctx) { TEST_SAFE_CALL(test_2); TEST_SAFE_CALL(test_3); TEST_SAFE_CALL(test_4); - TEST_SAFE_CALL(test_5); /* FIXME: exposes DUK_INVALID_INDEX number constant */ + TEST_SAFE_CALL(test_5); } diff --git a/api-testcases/test-to-lstring.c b/api-testcases/test-to-lstring.c index e1253d9e..bbec6f8c 100644 --- a/api-testcases/test-to-lstring.c +++ b/api-testcases/test-to-lstring.c @@ -43,9 +43,9 @@ index 19, string: '0xdeadbeef', length 10 index 19, string: '0xdeadbeef' ==> rc=0, result='undefined' *** test_2 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ int test_1(duk_context *ctx) { diff --git a/api-testcases/test-to-object.c b/api-testcases/test-to-object.c index 555e5456..7c9da92f 100644 --- a/api-testcases/test-to-object.c +++ b/api-testcases/test-to-object.c @@ -35,9 +35,9 @@ index 0 OK index 0 OK ==> rc=0, result='undefined' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_4 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ int test_1(duk_context *ctx) { @@ -151,9 +151,5 @@ void test(duk_context *ctx) { TEST_SAFE_CALL(test_2g); TEST_SAFE_CALL(test_2h); TEST_SAFE_CALL(test_3); - - /* FIXME: this testcase currently exposes the DUK_INVALID_INDEX - * constant in the error message and is thus not portable. - */ TEST_SAFE_CALL(test_4); } diff --git a/api-testcases/test-to-pointer.c b/api-testcases/test-to-pointer.c index 67eb9ad3..7e7c2373 100644 --- a/api-testcases/test-to-pointer.c +++ b/api-testcases/test-to-pointer.c @@ -23,9 +23,9 @@ index 17, ptr-is-NULL: 0, type: 8 -> 8 pointer: 0xdeadbeef ==> rc=0, result='undefined' *** test_2 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ int test_1(duk_context *ctx) { @@ -89,9 +89,5 @@ int test_3(duk_context *ctx) { void test(duk_context *ctx) { TEST_SAFE_CALL(test_1); TEST_SAFE_CALL(test_2); - - /* FIXME: this test now results in an error string containing the - * exact index, which is platform dependent. - */ TEST_SAFE_CALL(test_3); } diff --git a/api-testcases/test-to-primitive.c b/api-testcases/test-to-primitive.c index 1d553675..b495fc72 100644 --- a/api-testcases/test-to-primitive.c +++ b/api-testcases/test-to-primitive.c @@ -22,9 +22,9 @@ index 17, ToString(result): 'null', type: 8 -> 8 index 18, ToString(result): '0xdeadbeef', type: 8 -> 8 ==> rc=0, result='undefined' *** test_2 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ /* FIXME: coverage is pretty poor, e.g. different hints are not tested. @@ -89,10 +89,6 @@ int test_3(duk_context *ctx) { void test(duk_context *ctx) { TEST_SAFE_CALL(test_1); TEST_SAFE_CALL(test_2); - - /* FIXME: this test now results in an error string containing the - * exact index, which is platform dependent. - */ TEST_SAFE_CALL(test_3); } diff --git a/api-testcases/test-to-string.c b/api-testcases/test-to-string.c index ead8d9da..d0f6c478 100644 --- a/api-testcases/test-to-string.c +++ b/api-testcases/test-to-string.c @@ -23,9 +23,9 @@ index 18, string: 'null' index 19, string: '0xdeadbeef' ==> rc=0, result='undefined' *** test_2 (duk_safe_call) -==> rc=1, result='Error: invalid index: 3' +==> rc=1, result='Error: invalid index' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ int test_1(duk_context *ctx) { @@ -86,10 +86,6 @@ int test_3(duk_context *ctx) { void test(duk_context *ctx) { TEST_SAFE_CALL(test_1); TEST_SAFE_CALL(test_2); - - /* FIXME: this testcase currently exposes the DUK_INVALID_INDEX - * constant in the error message and is thus not portable. - */ TEST_SAFE_CALL(test_3); } diff --git a/api-testcases/test-trim.c b/api-testcases/test-trim.c index 9afbd875..2d258e10 100644 --- a/api-testcases/test-trim.c +++ b/api-testcases/test-trim.c @@ -7,9 +7,9 @@ final top: 2 *** test_2 (duk_safe_call) ==> rc=1, result='TypeError: incorrect type, expected tag 5' *** test_3 (duk_safe_call) -==> rc=1, result='Error: invalid index: 4' +==> rc=1, result='Error: invalid index' *** test_4 (duk_safe_call) -==> rc=1, result='Error: invalid index: -2147483648' +==> rc=1, result='Error: invalid index' ===*/ int test_1(duk_context *ctx) { @@ -84,7 +84,5 @@ void test(duk_context *ctx) { TEST_SAFE_CALL(test_1); TEST_SAFE_CALL(test_2); TEST_SAFE_CALL(test_3); - - /* FIXME: this test prints DUK_INVALID_INDEX value in its error string now */ TEST_SAFE_CALL(test_4); } diff --git a/api-testcases/test-validate-index.c b/api-testcases/test-validate-index.c index 1b9356c4..03ac0a31 100644 --- a/api-testcases/test-validate-index.c +++ b/api-testcases/test-validate-index.c @@ -11,9 +11,9 @@ top=3, idx=3, duk_is_valid_index -> 0 top=3, idx=4, duk_is_valid_index -> 0 top=3, idx=5, duk_is_valid_index -> 0 req_valid_idx: top 3 after popping arg -idx=-5, duk_require_valid_index -> Error: invalid index: -5 +idx=-5, duk_require_valid_index -> Error: invalid index req_valid_idx: top 3 after popping arg -idx=-4, duk_require_valid_index -> Error: invalid index: -4 +idx=-4, duk_require_valid_index -> Error: invalid index req_valid_idx: top 3 after popping arg idx=-3, duk_require_valid_index -> true req_valid_idx: top 3 after popping arg @@ -27,11 +27,11 @@ idx=1, duk_require_valid_index -> true req_valid_idx: top 3 after popping arg idx=2, duk_require_valid_index -> true req_valid_idx: top 3 after popping arg -idx=3, duk_require_valid_index -> Error: invalid index: 3 +idx=3, duk_require_valid_index -> Error: invalid index req_valid_idx: top 3 after popping arg -idx=4, duk_require_valid_index -> Error: invalid index: 4 +idx=4, duk_require_valid_index -> Error: invalid index req_valid_idx: top 3 after popping arg -idx=5, duk_require_valid_index -> Error: invalid index: 5 +idx=5, duk_require_valid_index -> Error: invalid index ===*/ int req_valid_idx(duk_context *ctx) { diff --git a/ecmascript-testcases/test-bi-proxy-internal-keys.js b/ecmascript-testcases/test-bi-proxy-internal-keys.js new file mode 100644 index 00000000..3684aa99 --- /dev/null +++ b/ecmascript-testcases/test-bi-proxy-internal-keys.js @@ -0,0 +1,85 @@ +/* + * The current ES6 Proxy subset behavior is skipped entirely for Duktape + * internal keys. Any property read/write/delete operations on internal + * keys behave as if the proxy handler did not exist, so that the operations + * are applied to the target object instead. + * + * Duktape accesses internal properties like _finalizer with ordinary + * property reads and writes, which causes some side effects when combining + * proxies and finalizers. In essence, you can only set a finalizer to the + * target object. + */ + +/*=== +get for key: "foo" +target finalized (2) +===*/ + +function test1() { + var target = {}; + var proxy = new Proxy(target, { + get: function(targ, key, receiver) { + print('get for key:', Duktape.enc('jsonx', key)); + return targ[key]; + } + }); + + Duktape.fin(target, function () { print('target finalized (1)'); }); + + // Because there is no 'set' handler, the _finalizer write will go into + // the target object and overwrites the previous finalizer. + Duktape.fin(proxy, function () { print('target finalized (2)'); }); + + // When proxy is made unreachable, there is no _finalizer read because + // the *proxy* does not have a finalizer property from Duktape's perspective + // (there is no proxy handler for property existence check now, and the + // current finalizer code uses duk_hobject_hasprop_raw() which ignores proxies). + target = null; // reachable through proxy + void proxy.foo; + proxy = null; +} + +try { + test1(); +} catch (e) { + print(e); +} + +/*=== +target finalized (2) +===*/ + +function test2() { + var target = {}; + var proxy = new Proxy(target, { + set: function(targ, key, val, receiver) { + print('set for key:', Duktape.enc('jsonx', key)); + targ[key] = val; + return true; + } + }); + + Duktape.fin(target, function () { print('target finalized (1)'); }); + + // The 'set' handler is skipped for internal keys, so the finalizer is set + // into the target again, overwriting the previous finalizer. Nothing is + // logged by the 'set' handler. + Duktape.fin(proxy, function () { print('target finalized (2)'); }); + + // Like in test1(), no finalizer read for proxy. + target = null; // reachable through proxy + void proxy.foo; + proxy = null; +} + +try { + test2(); +} catch (e) { + print(e); +} + +/*=== +finished +===*/ + +print('finished'); diff --git a/ecmascript-testcases/test-bi-proxy-subset.js b/ecmascript-testcases/test-bi-proxy-subset.js new file mode 100644 index 00000000..579b1be8 --- /dev/null +++ b/ecmascript-testcases/test-bi-proxy-subset.js @@ -0,0 +1,816 @@ +/* + * Proxy (ES6 draft) subset + */ + +/*=== +proxy existence +Proxy exists: true function +Proxy.length: 2 +Proxy.name: Proxy +Proxy desc: writable=true enumerable=false configurable=true +Proxy.revocable exists: false undefined +===*/ + +function proxyExistenceTest() { + var pd; + + print('Proxy exists:', 'Proxy' in this, typeof this.Proxy); + print('Proxy.length:', this.Proxy.length); + print('Proxy.name:', this.Proxy.name); + pd = Object.getOwnPropertyDescriptor(this, 'Proxy'); + if (pd) { + print('Proxy desc:', 'writable=' + pd.writable, 'enumerable=' + pd.enumerable, + 'configurable=' + pd.configurable); + } + + print('Proxy.revocable exists:', 'revocable' in this.Proxy, typeof this.Proxy.revocable); +/* + print('Proxy.revocable.length:', this.Proxy.revocable.length); + print('Proxy.revocable.name:', this.Proxy.revocable.name); + pd = Object.getOwnPropertyDescriptor(this.Proxy, 'revocable'); + if (pd) { + print('Proxy.revocable desc:', 'writable=' + pd.writable, 'enumerable=' + pd.enumerable, + 'configurable=' + pd.configurable); + } +*/ +} + +print('proxy existence'); + +try { + proxyExistenceTest(); +} catch (e) { + print(e); +} + +/*=== +subset proxy +object +[object Object] +get foo: 123 +get 1000: thousand +get special: specialValue +counts: get=0 set=0 del=0 +handler.get: true true string foo true +get foo: fake-for-key-foo +handler.get: true true string bar true +get bar: fake-for-key-bar +handler.get: true true string 1000 true +get 1000 (string): fake-for-key-1000 +handler.get: true true number 1000 true +get 1000 (number): 2000 +counts: get=4 set=0 del=0 +handler.get: true true string special true +get special: specialValue +handler.get: true true string special true +get special: uncaughtValue +target.special: uncaughtValue +counts: get=6 set=0 del=0 +handler.set: true true string foo number 1001 true +handler.set: true true string bar number 1002 true +handler.set: true true string quux number 1003 true +handler.set: true true number 123 string foo true +handler.set: true true string rejectSet1 string reject true +handler.set: true true string rejectSet2 string reject true +handler.set: true true string rejectSet1 string throw true +TypeError +handler.set: true true string rejectSet2 string throw true +TypeError +counts: get=6 set=8 del=0 +target.foo: 123 +target.bar: 1002 +target.quux: 1003 +target[123]: foo +target.rejectSet1: undefined +target.rejectSet2: undefined +counts: get=6 set=8 del=0 +true +target.foo: undefined +target.bar: 1002 +counts: get=6 set=8 del=0 +handler.deleteProperty: true true string rejectDel1 +false +handler.deleteProperty: true true string rejectDel2 +false +handler.deleteProperty: true true string rejectDel1 +TypeError +handler.deleteProperty: true true string rejectDel2 +TypeError +handler.deleteProperty: true true string foo +true +handler.deleteProperty: true true number 1234 +true +counts: get=6 set=8 del=6 +target.rejectDel1: reject1 +target.rejectDel2: reject2 +target.foo 123 +target[1234] undefined +===*/ + +/* Test simple usage of the current Proxy subset. Does not exercise the + * hook behaviors related to checking for conflicting properties in the + * target object. + */ +function subsetProxyTest() { + var getCount = 0; + var setCount = 0; + var deleteCount = 0; + var target = { foo: 123, '1000': 'thousand', special: 'specialValue' }; + var handler = {}; + var proxy = new Proxy(target, handler); + + function printCounts() { + print('counts:', 'get=' + getCount, 'set=' + setCount, 'del=' + deleteCount); + } + + print(typeof proxy); + print(Object.prototype.toString.call(proxy)); // XXX: now class is 'Object' + + // without a 'get' hook, reads go through + print('get foo:', proxy.foo); + print('get 1000:', proxy[1000]); + print('get special:', proxy.special); + + // handler 'get' hook + handler.get = function(targ, key, receiver) { + print('handler.get:', this === handler, targ === target, typeof key, key, receiver === proxy); + getCount++; + if (typeof key === 'number') { + return 2 * (+key); + } else if (key === 'special') { + return targ.special; + } else { + return 'fake-for-key-' + key; + } + }; + + // Get tests + printCounts(); + print('get foo:', proxy.foo); + print('get bar:', proxy.bar); + print('get 1000 (string):', proxy['1000']); + print('get 1000 (number):', proxy[1000]); + printCounts(); + + // without a 'set' hook, writes go through + print('get special:', proxy.special); + proxy.special = 'uncaughtValue'; // goes into 'target' + print('get special:', proxy.special); + print('target.special:', target.special); + + // handler 'set' hook + handler.set = function(targ, key, val, receiver) { + print('handler.set:', this === handler, targ === target, typeof key, key, typeof val, val, receiver === proxy); + setCount++; + if (key === 'rejectSet1') { + // False: indicate that property set is rejected (TypeError in strict mode). + // No change happens to the target object. + return false; + } + if (key === 'rejectSet2') { + // Same for any 'falsy' value. + return 0; + } + if (key === 'foo') { + // True: indicate that property set is allowed, but no change happens + // to the target object if we don't do it explicitly here. + return true; + } + + // Setting to target must be done explicitly. + targ[key] = val; + return true; + }; + + // Set tests + printCounts(); + proxy.foo = 1001; + proxy.bar = 1002; + proxy.quux = 1003; + proxy[123] = 'foo'; + proxy.rejectSet1 = 'reject'; // reject silently in non-strict mode + proxy.rejectSet2 = 'reject'; + try { + (function () { 'use strict'; proxy.rejectSet1 = 'throw'; })(); + } catch (e) { + print(e.name); + } + try { + (function () { 'use strict'; proxy.rejectSet2 = 'throw'; })(); + } catch (e) { + print(e.name); + } + printCounts(); + print('target.foo:', target.foo); + print('target.bar:', target.bar); + print('target.quux:', target.quux); + print('target[123]:', target[123]); + print('target.rejectSet1:', target.rejectSet1); + print('target.rejectSet2:', target.rejectSet2); + + // without a 'deleteProperty' hook, deletes go through + printCounts(); + print(delete proxy.foo); + print('target.foo:', target.foo); + print('target.bar:', target.bar); + printCounts(); + + // handler 'deleteProperty' hook + handler.deleteProperty = function(targ, key) { + print('handler.deleteProperty:', this === handler, targ === target, typeof key, key); + deleteCount++; + if (key === 'rejectDel1') { + // False return code indicates delete is rejected. + return false; + } + if (key === 'rejectDel2') { + // Same for any 'falsy' value. + return 0; + } + if (key === 'foo') { + // True return code indicates delete is accepted (but it has no + // effect on the target unless we delete the property from the + // target here). + return true; + } + + // Deletion to target must be done explicitly. + delete targ[key]; + return true; + }; + + target.rejectDel1 = 'reject1'; + target.rejectDel2 = 'reject2'; + target.foo = 123; + target[1234] = 4321; + print(delete proxy.rejectDel1); + print(delete proxy.rejectDel2); + try { + (function () { 'use strict'; print(delete proxy.rejectDel1); })(); + } catch (e) { + print(e.name); + } + try { + (function () { 'use strict'; print(delete proxy.rejectDel2); })(); + } catch (e) { + print(e.name); + } + print(delete proxy.foo); // allowed, but no effect on target + print(delete proxy[1234]); // allowed, deletes value on target + printCounts(); + print('target.rejectDel1:', target.rejectDel1); + print('target.rejectDel2:', target.rejectDel2); + print('target.foo', target.foo); + print('target[1234]', target[1234]); +} + +print('subset proxy'); + +try { + subsetProxyTest(); +} catch (e) { + print(e); +} + +/*=== +hook post-checks +get hook +property1: success, value: whatever +property2: success, value: whatever +property3: success, value: whatever +property4: success, value: whatever +property5: success, value: 42 +property6: success, value: NaN +property7: success, value: 0 +property1: success, value: whatever +property2: success, value: whatever +property3: success, value: whatever +property4: success, value: whatever +property5: TypeError +property6: TypeError +property7: TypeError +accessor1: success, value: undefined +accessor2: success, value: undefined +accessor3: success, value: undefined +accessor4: success, value: undefined +accessor5: success, value: undefined +accessor6: success, value: undefined +accessor7: success, value: undefined +accessor1: success, value: 123 +accessor2: success, value: 123 +accessor3: success, value: 123 +accessor4: success, value: 123 +accessor5: TypeError +accessor6: TypeError +accessor7: success, value: 123 +set hook +property1: success, property1 set to 42 +property2: success, property2 set to 42 +property3: success, property3 set to 42 +property4: success, property4 set to 42 +property5: success, property5 set to 42 +property6: success, property6 set to NaN +property7: success, property7 set to 0 +property1: success, property1 set to 142 +property2: success, property2 set to 142 +property3: success, property3 set to 142 +property4: success, property4 set to 142 +property5: TypeError trying to set value to 142 +property6: TypeError trying to set value to Infinity +property7: TypeError trying to set value to 0 +accessor1: success, accessor1 set to whatever +accessor2: success, accessor2 set to whatever +accessor3: success, accessor3 set to whatever +accessor4: success, accessor4 set to whatever +accessor5: TypeError trying to set value to whatever +accessor6: success, accessor6 set to whatever +accessor7: TypeError trying to set value to whatever +deleteProperty hook +property1: success, result: true +property2: success, result: true +property3: TypeError +property4: TypeError +property5: TypeError +property6: TypeError +property7: TypeError +accessor1: success, result: true +accessor2: success, result: true +accessor3: TypeError +accessor4: success, result: true +accessor5: TypeError +accessor6: TypeError +accessor7: TypeError +===*/ + +/* If a hook exists and is successfully called, ES6 specifies interesting + * post-hook behavior where a TypeError may be raised if the hook return + * value conflicts in some way with a property of the same name in the + * target object. + */ + +function makeDataTestObject() { + var obj = {}; + + Object.defineProperties(obj, { + // Various properties whose behavior to compare against the hooks + + property1: { + writable: true, enumerable: true, configurable: true, value: 42 + }, + property2: { + writable: false, enumerable: true, configurable: true, value: 42 + }, + property3: { + writable: true, enumerable: true, configurable: false, value: 42 + }, + property4: { + writable: true, enumerable: false, configurable: false, value: 42 + }, + property5: { + writable: false, enumerable: true, configurable: false, value: 42 + }, + property6: { + writable: false, enumerable: true, configurable: false, value: NaN + }, + property7: { + writable: false, enumerable: true, configurable: false, value: +0 + } + }); + + return obj; +} + +function makeAccessorTestObject() { + var obj = {}; + + function getter() { + print('getter called'); + return 'getter-value'; + } + + function setter(v) { + print('setter called:', v); + } + + Object.defineProperties(obj, { + // Various properties whose behavior to compare against the hooks + accessor1: { + enumerable: true, configurable: true, set: setter, get: getter + }, + accessor2: { + enumerable: false, configurable: true, set: setter, get: getter + }, + accessor3: { + enumerable: true, configurable: false, set: setter, get: getter + }, + accessor4: { + enumerable: true, configurable: true, set: undefined, get: undefined + }, + accessor5: { + enumerable: true, configurable: false, set: undefined, get: undefined + }, + accessor6: { + enumerable: true, configurable: false, set: setter, get: undefined + }, + accessor7: { + enumerable: true, configurable: false, set: undefined, get: getter + } + }); + + return obj; +} + +function getHookPostChecksTest() { + var target, handler, proxy; + + function getTest(proxy, propname) { + try { + var val = proxy[propname]; + print(propname + ': success, value:', val); + } catch (e) { + print(propname + ':', e.name); + } + } + + /* 'get' return value is rejected if the target has a property of the + * same name and the property: + * + * - Is a data property, is not configurable, is not writable, and + * hook provided value does not match with the current value + * (as compared with SameValue). + * + * - Is an accessor property, is not configurable, getter is not + * defined, and hook provided value is not undefined. + */ + + /* + * Data properties + */ + + target = makeDataTestObject(); + + handler = {}; + handler.get = function (targ, key, receiver) { + if (key === 'property5') { return 42; } // same value, no error + if (key === 'property6') { return NaN; } // same value, no error + if (key === 'property7') { return +0; } // same value, no error + return 'whatever'; // differs from all values + }; + proxy = new Proxy(target, handler); + Object.getOwnPropertyNames(target).forEach(function (propname) { + getTest(proxy, propname); + }); + + handler = {}; + handler.get = function (targ, key, receiver) { + if (key === 'property5') { return 41; } // different value, error + if (key === 'property6') { return undefined; } // different value, error + if (key === 'property7') { return -0; } // different value, error + return 'whatever'; // differs from all values + }; + proxy = new Proxy(target, handler); + Object.getOwnPropertyNames(target).forEach(function (propname) { + getTest(proxy, propname); + }); + + /* + * Accessor properties + */ + + target = makeAccessorTestObject(); + + handler = {}; + proxy = new Proxy(target, handler); + handler.get = function (targ, key, receiver) { + // If trapResult is undefined, post-hook checks always pass + return undefined; + } + Object.getOwnPropertyNames(target).forEach(function (propname) { + getTest(proxy, propname); + }); + + handler = {}; + proxy = new Proxy(target, handler); + handler.get = function (targ, key, receiver) { + // If trapResult is not undefined, post-hook checks cause a TypeError + // if property is non-configurable and getter is undefined. + return 123; + } + Object.getOwnPropertyNames(target).forEach(function (propname) { + getTest(proxy, propname); + }); +} + +function setHookPostChecksTest() { + var target, handler, proxy; + + function setTest(proxy, propname, propvalue) { + try { + proxy[propname] = propvalue; + print(propname + ': success,', propname, 'set to', propvalue); + } catch (e) { + print(propname + ':', e.name, 'trying to set value to', propvalue); + } + } + + /* 'set' is rejected if the target has a property of the same name + * and the property: + * + * - Is a data property, is not configurable, is not writable, and + * the assignment value does not match with the current value + * (as compared with SameValue). + * + * - Is an accessor property, is not configurable, and setter is not + * defined. Unlike for 'get' the value does not matter. + */ + + /* + * Data properties + */ + + target = makeDataTestObject(); + + handler = {}; + proxy = new Proxy(target, handler); + handler.set = function (targ, key, val, receiver) { + // If 'false' is returned, property write is rejected and the post-hook + // behavior doesn't activate at all, so always return true here. + return true; + } + Object.getOwnPropertyNames(target).forEach(function (propname) { + // Choose test value to match current value (with SameValue). + // No TypeError is triggered even if other conditions are met. + + var propval = { + property1: 42, + property2: 42, + property3: 42, + property4: 42, + property5: 42, + property6: NaN, + property7: +0 + }[propname]; + + setTest(proxy, propname, propval); + }); + + handler.set = function (targ, key, val, receiver) { + return true; + }; + Object.getOwnPropertyNames(target).forEach(function (propname) { + // Choose test value to match current value (with SameValue). + // No TypeError is triggered even if other conditions are met. + + var propval = { + property1: 142, + property2: 142, + property3: 142, + property4: 142, + property5: 142, + property6: 1/0, + property7: -0 // SameValue, even sign matters + }[propname]; + + setTest(proxy, propname, propval); + }); + + /* + * Accessor properties + */ + + target = makeAccessorTestObject(); + + handler = {}; + proxy = new Proxy(target, handler); + handler.set = function (targ, key, val, receiver) { + // If 'false' is returned, property write is rejected and the post-hook + // behavior doesn't activate at all, so always return true here. + return true; + } + Object.getOwnPropertyNames(target).forEach(function (propname) { + // For accessor + 'set' hook, property value does not matter. + setTest(proxy, propname, 'whatever'); + }); +} + +function deleteHookPostChecksTest() { + var target, handler, proxy; + + function deleteTest(proxy, propname) { + try { + print(propname + ': success, result:', delete proxy[propname]); + } catch (e) { + print(propname + ':', e.name); + } + } + + /* 'deleteProperty' is rejected if the target has a property of the + * same name and the property: + * + * - Is not configurable + */ + + /* + * Data properties + */ + + target = makeDataTestObject(); + + handler = {}; + proxy = new Proxy(target, handler); + handler.deleteProperty = function (targ, key, val, receiver) { + // If 'false' is returned, property delete is rejected and the post-hook + // behavior doesn't activate at all, so always return true here. + return true; + } + Object.getOwnPropertyNames(target).forEach(function (propname) { + deleteTest(proxy, propname); + }); + + /* + * Accessor properties + */ + + target = makeAccessorTestObject(); + + handler = {}; + proxy = new Proxy(target, handler); + handler.deleteProperty = function (targ, key, val, receiver) { + // If 'false' is returned, property delete is rejected and the post-hook + // behavior doesn't activate at all, so always return true here. + return true; + } + Object.getOwnPropertyNames(target).forEach(function (propname) { + deleteTest(proxy, propname); + }); +} + +print('hook post-checks'); + +try { + print('get hook'); + getHookPostChecksTest(); + print('set hook'); + setHookPostChecksTest(); + print('deleteProperty hook'); + deleteHookPostChecksTest(); +} catch (e) { + print(e); +} + +/*=== +recursive proxies +TypeError +TypeError +===*/ + +/* Currently Duktape doesn't allow a proxy as either a handler or a target. + * This makes it easier to implement because there is no arbitrary depth C + * recursion when doing proxy lookups. + */ + +function proxyHandlerTest() { + var target = { foo: 123 }; + var handler = new Proxy({}, {}); + var proxy = new Proxy(target, handler); +} + +function proxyTargetTest() { + var target = new Proxy({}, {}); + var handler = {}; + var proxy = new Proxy(target, handler); +} + +print('recursive proxies'); + +try { + proxyHandlerTest(); +} catch (e) { + print(e.name); +} + +try { + proxyTargetTest(); +} catch (e) { + print(e.name); +} + +/*=== +getter handler +handler.get: true true foo true +proxy.foo: dummy-value +===*/ + +/* A getter as a handler property. No reason why this wouldn't work but + * test just in case. + */ + +function getterHandlerTest() { + var target = { foo: 123 }; + var handler = {}; + var proxy = new Proxy(target, handler); + + Object.defineProperty(handler, 'get', { + enumerable: true, + configurable: true, + get: function () { + return function (targ, key, receiver) { + print('handler.get:', this === handler, targ === target, key, receiver === proxy); + return 'dummy-value'; + }; + } + }); + + print('proxy.foo:', proxy.foo); +} + +print('getter handler'); + +try { + getterHandlerTest(); +} catch (e) { + print(e); +} + +/*=== +non-callable handler +TypeError +===*/ + +/* A non-callable handler property. This is not checked during proxy creation + * and should cause a TypeError. + */ + +function nonCallableHandlerTest() { + var target = { foo: 123 }; + var handler = { get: 'dummy' }; + var proxy = new Proxy(target, handler); + print('proxy.foo:', proxy.foo); +} + +print('non-callable handler'); + +try { + nonCallableHandlerTest(); +} catch (e) { + print(e.name); +} + +/*=== +throwing handler +handler.get: about to throw +URIError: fake error +===*/ + +/* Handler function throws. Nothing special here. */ + +function throwingHandlerTest() { + var target = { foo: 123 }; + var handler = { + get: function() { + print('handler.get: about to throw'); + throw new URIError('fake error'); + } + }; + var proxy = new Proxy(target, handler); + print('proxy.foo:', proxy.foo); + +} + +print('throwing handler'); + +try { + throwingHandlerTest(); +} catch (e) { + print(e); +} + +/*=== +proxy revocation +handler.get: true true foo true +proxy.foo: dummy-value +===*/ + +/* Revoked proxy. */ + +function proxyRevocationTest() { + var target = { foo: 123 }; + var handler = { + get: function(targ, key, receiver) { + print('handler.get:', this === handler, targ === target, key, receiver === proxy); + return 'dummy-value'; + } + }; + var proxy = new Proxy(target, handler); + print('proxy.foo:', proxy.foo); + + // FIXME: unimplemented +} + +print('proxy revocation'); + +try { + proxyRevocationTest(); +} catch (e) { + print(e.name); +} diff --git a/ecmascript-testcases/test-bug-rejected-delete-property.js b/ecmascript-testcases/test-bug-rejected-delete-property.js new file mode 100644 index 00000000..00a1f92e --- /dev/null +++ b/ecmascript-testcases/test-bug-rejected-delete-property.js @@ -0,0 +1,28 @@ +/* + * Rejected delete property had a bug which caused the value stack to be out + * of balance when returning to the bytecode executor. This test used to fail + * with assertions enabled, when the base object for deletion was a string + * (e.g. attempt to delete str.length or a virtual index). + */ + +/*=== +true +false +false +===*/ + +function test() { + // must be non-strict so that delete failure is silent + + var str = 'foo'; + + print(delete str.nonexistent); + print(delete str.length); + print(delete str[1]); +} + +try { + test(); +} catch (e) { + print(e); +} diff --git a/src/duk_bi_protos.h b/src/duk_bi_protos.h index 21fac34e..674958cd 100644 --- a/src/duk_bi_protos.h +++ b/src/duk_bi_protos.h @@ -174,6 +174,12 @@ duk_ret_t duk_bi_string_prototype_trim(duk_context *ctx); #ifdef DUK_USE_SECTION_B duk_ret_t duk_bi_string_prototype_substr(duk_context *ctx); #endif + +duk_ret_t duk_bi_proxy_constructor(duk_context *ctx); +#if 0 /* unimplemented now */ +duk_ret_t duk_bi_proxy_constructor_revocable(duk_context *ctx); +#endif + duk_ret_t duk_bi_thread_constructor(duk_context *ctx); duk_ret_t duk_bi_thread_resume(duk_context *ctx); duk_ret_t duk_bi_thread_yield(duk_context *ctx); diff --git a/src/duk_bi_proxy.c b/src/duk_bi_proxy.c new file mode 100644 index 00000000..bea6e476 --- /dev/null +++ b/src/duk_bi_proxy.c @@ -0,0 +1,60 @@ +/* + * Proxy built-in (ES6 draft) + */ + +#include "duk_internal.h" + +#if defined(DUK_USE_ES6_PROXY) +duk_ret_t duk_bi_proxy_constructor(duk_context *ctx) { + duk_hobject *h_target; + duk_hobject *h_handler; + + if (!duk_is_constructor_call(ctx)) { + return DUK_RET_TYPE_ERROR; + } + + /* Reject a proxy object as the target because it would need + * special handler in property lookups. (ES6 has no such restriction) + */ + h_target = duk_require_hobject(ctx, 0); + DUK_ASSERT(h_target != NULL); + if (DUK_HOBJECT_HAS_SPECIAL_PROXYOBJ(h_target)) { + return DUK_RET_TYPE_ERROR; + } + + /* Reject a proxy object as the handler because it would cause + * potentially unbounded recursion. (ES6 has no such restriction) + */ + h_handler = duk_require_hobject(ctx, 1); + DUK_ASSERT(h_handler != NULL); + if (DUK_HOBJECT_HAS_SPECIAL_PROXYOBJ(h_handler)) { + return DUK_RET_TYPE_ERROR; + } + + /* XXX: the returned value is exotic in ES6 (draft), but we use a + * simple object here with no prototype. Without a prototype, + * [[DefaultValue]] coercion fails which is abit confusing. + * No callable check/handling in the current Proxy subset. + */ + (void) duk_push_object_helper_proto(ctx, + DUK_HOBJECT_FLAG_SPECIAL_PROXYOBJ | + DUK_HOBJECT_CLASS_AS_FLAGS(DUK_HOBJECT_CLASS_OBJECT), + NULL); + DUK_ASSERT_TOP(ctx, 3); + + /* Proxy target */ + duk_dup(ctx, 0); + duk_def_prop_stridx(ctx, -2, DUK_STRIDX_INT_TARGET, DUK_PROPDESC_FLAGS_WC); + + /* Proxy handler */ + duk_dup(ctx, 1); + duk_def_prop_stridx(ctx, -2, DUK_STRIDX_INT_HANDLER, DUK_PROPDESC_FLAGS_WC); + + return 1; /* replacement handler */ +} +#else /* DUK_USE_ES6_PROXY */ +duk_ret_t duk_bi_proxy_constructor(duk_context *ctx) { + DUK_UNREF(ctx); + return DUK_RET_UNSUPPORTED_ERROR; +} +#endif /* DUK_USE_ES6_PROXY */ diff --git a/src/duk_features.h b/src/duk_features.h index 407fb19d..5a90954f 100644 --- a/src/duk_features.h +++ b/src/duk_features.h @@ -1723,11 +1723,19 @@ typedef FILE duk_file; #undef DUK_USE_OBJECT_ES6_SETPROTOTYPEOF #endif +/* ES6 Proxy object (subset for now). */ +#define DUK_USE_ES6_PROXY +#if defined(DUK_OPT_NO_ES6_PROXY) +#undef DUK_USE_ES6_PROXY +#endif + +/* Record pc-to-line information. */ #define DUK_USE_PC2LINE #if defined(DUK_OPT_NO_PC2LINE) #undef DUK_USE_PC2LINE #endif +/* Non-standard function 'source' property. */ #undef DUK_USE_FUNC_NONSTD_SOURCE_PROPERTY #if defined(DUK_OPT_FUNC_NONSTD_SOURCE_PROPERTY) #define DUK_USE_FUNC_NONSTD_SOURCE_PROPERTY diff --git a/src/duk_hobject.h b/src/duk_hobject.h index 12ced667..5548c04e 100644 --- a/src/duk_hobject.h +++ b/src/duk_hobject.h @@ -50,7 +50,7 @@ #define DUK_HOBJECT_FLAG_SPECIAL_ARGUMENTS DUK_HEAPHDR_USER_FLAG(15) /* 'Arguments' object and has arguments special behavior (non-strict callee) */ #define DUK_HOBJECT_FLAG_SPECIAL_DUKFUNC DUK_HEAPHDR_USER_FLAG(16) /* Duktape/C (nativefunction) object, special 'length' */ #define DUK_HOBJECT_FLAG_SPECIAL_BUFFEROBJ DUK_HEAPHDR_USER_FLAG(17) /* 'Buffer' object, array index special behavior, virtual 'length' */ -/* bit 18 unused */ +#define DUK_HOBJECT_FLAG_SPECIAL_PROXYOBJ DUK_HEAPHDR_USER_FLAG(18) /* 'Proxy' object */ /* bit 19 unused */ /* bit 20 unused */ @@ -113,7 +113,8 @@ DUK_HOBJECT_FLAG_SPECIAL_ARGUMENTS | \ DUK_HOBJECT_FLAG_SPECIAL_STRINGOBJ | \ DUK_HOBJECT_FLAG_SPECIAL_DUKFUNC | \ - DUK_HOBJECT_FLAG_SPECIAL_BUFFEROBJ) + DUK_HOBJECT_FLAG_SPECIAL_BUFFEROBJ | \ + DUK_HOBJECT_FLAG_SPECIAL_PROXYOBJ) #define DUK_HOBJECT_HAS_SPECIAL_BEHAVIOR(h) DUK_HEAPHDR_CHECK_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_SPECIAL_BEHAVIOR_FLAGS) @@ -134,6 +135,7 @@ #define DUK_HOBJECT_HAS_SPECIAL_ARGUMENTS(h) DUK_HEAPHDR_CHECK_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_ARGUMENTS) #define DUK_HOBJECT_HAS_SPECIAL_DUKFUNC(h) DUK_HEAPHDR_CHECK_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_DUKFUNC) #define DUK_HOBJECT_HAS_SPECIAL_BUFFEROBJ(h) DUK_HEAPHDR_CHECK_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_BUFFEROBJ) +#define DUK_HOBJECT_HAS_SPECIAL_PROXYOBJ(h) DUK_HEAPHDR_CHECK_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_PROXYOBJ) #define DUK_HOBJECT_SET_EXTENSIBLE(h) DUK_HEAPHDR_SET_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_EXTENSIBLE) #define DUK_HOBJECT_SET_CONSTRUCTABLE(h) DUK_HEAPHDR_SET_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_CONSTRUCTABLE) @@ -152,6 +154,7 @@ #define DUK_HOBJECT_SET_SPECIAL_ARGUMENTS(h) DUK_HEAPHDR_SET_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_ARGUMENTS) #define DUK_HOBJECT_SET_SPECIAL_DUKFUNC(h) DUK_HEAPHDR_SET_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_DUKFUNC) #define DUK_HOBJECT_SET_SPECIAL_BUFFEROBJ(h) DUK_HEAPHDR_SET_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_BUFFEROBJ) +#define DUK_HOBJECT_SET_SPECIAL_PROXYOBJ(h) DUK_HEAPHDR_SET_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_PROXYOBJ) #define DUK_HOBJECT_CLEAR_EXTENSIBLE(h) DUK_HEAPHDR_CLEAR_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_EXTENSIBLE) #define DUK_HOBJECT_CLEAR_CONSTRUCTABLE(h) DUK_HEAPHDR_CLEAR_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_CONSTRUCTABLE) @@ -170,6 +173,7 @@ #define DUK_HOBJECT_CLEAR_SPECIAL_ARGUMENTS(h) DUK_HEAPHDR_CLEAR_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_ARGUMENTS) #define DUK_HOBJECT_CLEAR_SPECIAL_DUKFUNC(h) DUK_HEAPHDR_CLEAR_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_DUKFUNC) #define DUK_HOBJECT_CLEAR_SPECIAL_BUFFEROBJ(h) DUK_HEAPHDR_CLEAR_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_BUFFEROBJ) +#define DUK_HOBJECT_CLEAR_SPECIAL_PROXYOBJ(h) DUK_HEAPHDR_CLEAR_FLAG_BITS(&(h)->hdr, DUK_HOBJECT_FLAG_SPECIAL_PROXYOBJ) /* flags used for property attributes in duk_propdesc and packed flags */ #define DUK_PROPDESC_FLAG_WRITABLE (1 << 0) /* E5 Section 8.6.1 */ diff --git a/src/duk_hobject_props.c b/src/duk_hobject_props.c index c9c8973d..e833cc67 100644 --- a/src/duk_hobject_props.c +++ b/src/duk_hobject_props.c @@ -50,11 +50,16 @@ #define DUK__HASH_UNUSED DUK_HOBJECT_HASHIDX_UNUSED #define DUK__HASH_DELETED DUK_HOBJECT_HASHIDX_DELETED -/* assert value that suffices for all local calls, including recursion of - * other than Duktape calls (getters etc) +/* valstack space that suffices for all local calls, including recursion + * of other than Duktape calls (getters etc) */ #define DUK__VALSTACK_SPACE 10 +/* valstack space allocated especially for proxy lookup which does a + * recursive property lookup + */ +#define DUK__VALSTACK_PROXY_LOOKUP 20 + /* * Local prototypes */ @@ -252,7 +257,83 @@ static int duk__abandon_array_slow_check_required(duk_uint32_t arr_idx, duk_uint return (arr_idx > DUK_HOBJECT_A_FAST_RESIZE_LIMIT * ((old_size + 7) >> 3)); } - + +/* + * Proxy helpers + */ + +#if defined(DUK_USE_ES6_PROXY) +static duk_small_int_t duk__proxy_check(duk_hthread *thr, duk_hobject *obj, duk_small_int_t stridx_funcname, duk_tval *tv_key, duk_hobject **out_target) { + duk_context *ctx = (duk_context *) thr; + duk_tval *tv_target; + duk_tval *tv_handler; + + DUK_ASSERT(thr != NULL); + DUK_ASSERT(obj != NULL); + DUK_ASSERT(out_target != NULL); + + tv_handler = duk_hobject_find_existing_entry_tval_ptr(obj, DUK_HTHREAD_STRING_INT_HANDLER(thr)); + if (!tv_handler) { + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy revoked"); + return 0; + } + DUK_ASSERT(DUK_TVAL_IS_OBJECT(tv_handler)); + + tv_target = duk_hobject_find_existing_entry_tval_ptr(obj, DUK_HTHREAD_STRING_INT_TARGET(thr)); + if (!tv_target) { + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy revoked"); + return 0; + } + DUK_ASSERT(DUK_TVAL_IS_OBJECT(tv_target)); + *out_target = DUK_TVAL_GET_OBJECT(tv_target); + DUK_ASSERT(*out_target != NULL); + + /* XXX: At the moment Duktape accesses internal keys like _finalizer using a + * normal property set/get which would allow a proxy handler to interfere with + * such behavior and to get access to internal key strings. This is not a problem + * as such because internal key strings can be created in other ways too (e.g. + * through buffers). The best fix is to change Duktape internal lookups to + * skip proxy behavior. Until that, internal property accesses bypass the + * proxy and are applied to the target (as if the handler did not exist). + * This has some side effects, see test-bi-proxy-internal-keys.js. + */ + + if (DUK_TVAL_IS_STRING(tv_key)) { + duk_hstring *h_key = (duk_hstring *) DUK_TVAL_GET_STRING(tv_key); + DUK_ASSERT(h_key != NULL); + if (DUK_HSTRING_HAS_INTERNAL(h_key)) { + DUK_DDDPRINT("internal key, skip proxy handler and apply to target"); + return 0; + } + } + + /* The handler is looked up with a normal property lookup; it may be an + * accessor or the handler object itself may be a proxy object. If the + * handler is a proxy, we need to extend the valstack as we make a + * recursive proxy check without a function call in between (in fact + * there is no limit to the potential recursion here). + * + * (For sanity, proxy creation rejects another proxy object as either + * the handler or the target at the moment so recursive proxy cases + * are not realized now.) + */ + + /* XXX: C recursion limit if proxies are allowed as handler/target values */ + + duk_require_stack(ctx, DUK__VALSTACK_PROXY_LOOKUP); + duk_push_tval(ctx, tv_handler); + if (duk_get_prop_stridx(ctx, -1, stridx_funcname)) { + duk_remove(ctx, -2); + duk_push_tval(ctx, tv_handler); + /* stack prepped for func call: [ ... func handler ] */ + return 1; + } else { + duk_pop_2(ctx); + return 0; + } +} +#endif /* DUK_USE_ES6_PROXY */ + /* * Reallocate property allocation, moving properties to the new allocation. * @@ -1882,6 +1963,58 @@ int duk_hobject_getprop(duk_hthread *thr, duk_tval *tv_obj, duk_tval *tv_key) { curr = DUK_TVAL_GET_OBJECT(tv_obj); DUK_ASSERT(curr != NULL); +#if defined(DUK_USE_ES6_PROXY) + if (DUK_UNLIKELY(DUK_HOBJECT_HAS_SPECIAL_PROXYOBJ(curr))) { + duk_hobject *h_target; + + if (duk__proxy_check(thr, curr, DUK_STRIDX_GET, tv_key, &h_target)) { + /* -> [ ... func handler ] */ + DUK_DDDPRINT("-> proxy object 'get' for key %!T", tv_key); + duk_push_hobject(ctx, h_target); /* target */ + duk_push_tval(ctx, tv_key); /* P */ + duk_push_tval(ctx, tv_obj); /* Receiver: Proxy object */ + duk_call_method(ctx, 3 /*nargs*/); + + /* Target object must be checked for a conflicting + * non-configurable property. + */ + arr_idx = duk__push_tval_to_hstring_arr_idx(ctx, tv_key, &key); + DUK_ASSERT(key != NULL); + + if (duk__get_own_property_desc_raw(thr, h_target, key, arr_idx, &desc, 1 /*push_value*/)) { + duk_tval *tv_hook = duk_require_tval(ctx, -3); /* value from hook */ + duk_tval *tv_targ = duk_require_tval(ctx, -1); /* value from target */ + + DUK_DPRINT("proxy 'get': target has matching property %!O, check for " + "conflicting property; tv_hook=%!T, tv_targ=%!T, desc.flags=0x%08x, " + "desc.get=%p, desc.set=%p", + key, tv_hook, tv_targ, (int) desc.flags, + (void *) desc.get, (void *) desc.set); + + int datadesc_reject = !(desc.flags & DUK_PROPDESC_FLAG_ACCESSOR) && + !(desc.flags & DUK_PROPDESC_FLAG_CONFIGURABLE) && + !(desc.flags & DUK_PROPDESC_FLAG_WRITABLE) && + !duk_js_samevalue(tv_hook, tv_targ); + int accdesc_reject = (desc.flags & DUK_PROPDESC_FLAG_ACCESSOR) && + !(desc.flags & DUK_PROPDESC_FLAG_CONFIGURABLE) && + (desc.get == NULL) && + !DUK_TVAL_IS_UNDEFINED(tv_hook); + if (datadesc_reject || accdesc_reject) { + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy get rejected"); + } + + duk_pop_2(ctx); + } else { + duk_pop(ctx); + } + return 1; /* return value */ + } + + curr = h_target; /* resume lookup from target */ + DUK_TVAL_SET_OBJECT(tv_obj, curr); + } +#endif /* DUK_USE_ES6_PROXY */ + tmp = duk__shallow_fast_path_array_check_tval(curr, tv_key); if (tmp) { duk_push_tval(ctx, tmp); @@ -2608,6 +2741,64 @@ int duk_hobject_putprop(duk_hthread *thr, duk_tval *tv_obj, duk_tval *tv_key, du /* Note: no fast paths for property put now */ orig = DUK_TVAL_GET_OBJECT(tv_obj); DUK_ASSERT(orig != NULL); + +#if defined(DUK_USE_ES6_PROXY) + if (DUK_UNLIKELY(DUK_HOBJECT_HAS_SPECIAL_PROXYOBJ(orig))) { + duk_hobject *h_target; + int tmp_bool; + + if (duk__proxy_check(thr, orig, DUK_STRIDX_SET, tv_key, &h_target)) { + /* -> [ ... func handler ] */ + DUK_DDDPRINT("-> proxy object 'set' for key %!T", tv_key); + duk_push_hobject(ctx, h_target); /* target */ + duk_push_tval(ctx, tv_key); /* P */ + duk_push_tval(ctx, tv_val); /* V */ + duk_push_tval(ctx, tv_obj); /* Receiver: Proxy object */ + duk_call_method(ctx, 4 /*nargs*/); + tmp_bool = duk_to_boolean(ctx, -1); + duk_pop(ctx); + if (!tmp_bool) { + goto fail_proxy_rejected; + } + + /* Target object must be checked for a conflicting + * non-configurable property. + */ + arr_idx = duk__push_tval_to_hstring_arr_idx(ctx, tv_key, &key); + DUK_ASSERT(key != NULL); + + if (duk__get_own_property_desc_raw(thr, h_target, key, arr_idx, &desc, 1 /*push_value*/)) { + duk_tval *tv_targ = duk_require_tval(ctx, -1); + + DUK_DPRINT("proxy 'set': target has matching property %!O, check for " + "conflicting property; tv_val=%!T, tv_targ=%!T, desc.flags=0x%08x, " + "desc.get=%p, desc.set=%p", + key, tv_val, tv_targ, (int) desc.flags, + (void *) desc.get, (void *) desc.set); + + int datadesc_reject = !(desc.flags & DUK_PROPDESC_FLAG_ACCESSOR) && + !(desc.flags & DUK_PROPDESC_FLAG_CONFIGURABLE) && + !(desc.flags & DUK_PROPDESC_FLAG_WRITABLE) && + !duk_js_samevalue(tv_val, tv_targ); + int accdesc_reject = (desc.flags & DUK_PROPDESC_FLAG_ACCESSOR) && + !(desc.flags & DUK_PROPDESC_FLAG_CONFIGURABLE) && + (desc.set == NULL); + if (datadesc_reject || accdesc_reject) { + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy set rejected"); + } + + duk_pop_2(ctx); + } else { + duk_pop(ctx); + } + return 1; /* success */ + } + + orig = h_target; /* resume write to target */ + DUK_TVAL_SET_OBJECT(tv_obj, orig); + } +#endif /* DUK_USE_ES6_PROXY */ + curr = orig; break; } @@ -3153,6 +3344,14 @@ int duk_hobject_putprop(duk_hthread *thr, duk_tval *tv_obj, duk_tval *tv_key, du duk_pop(ctx); /* remove key */ return 1; + fail_proxy_rejected: + DUK_DDDPRINT("result: error, proxy rejects"); + if (throw_flag) { + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy rejected"); + } + /* Note: no key on stack */ + return 0; + fail_base_primitive: DUK_DDDPRINT("result: error, base primitive"); if (throw_flag) { @@ -3343,6 +3542,10 @@ int duk_hobject_delprop_raw(duk_hthread *thr, duk_hobject *obj, duk_hstring *key int duk_hobject_delprop(duk_hthread *thr, duk_tval *tv_obj, duk_tval *tv_key, int throw_flag) { duk_context *ctx = (duk_context *) thr; duk_hstring *key = NULL; +#if defined(DUK_USE_ES6_PROXY) + duk_propdesc desc; +#endif + duk_int_t entry_top; duk_uint32_t arr_idx = DUK__NO_ARRAY_INDEX; int rc; @@ -3357,36 +3560,85 @@ int duk_hobject_delprop(duk_hthread *thr, duk_tval *tv_obj, duk_tval *tv_key, in DUK_ASSERT_VALSTACK_SPACE(thr, DUK__VALSTACK_SPACE); + /* Storing the entry top is cheaper here to ensure stack is correct at exit, + * as there are several paths out. + */ + entry_top = duk_get_top(ctx); + if (DUK_TVAL_IS_UNDEFINED(tv_obj) || DUK_TVAL_IS_NULL(tv_obj)) { - /* Note: unconditional throw */ DUK_DDDPRINT("base object is undefined or null -> reject"); - DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "invalid base reference for property delete"); + goto fail_invalid_base_uncond; } /* FIXME: because we need to do this, just take args through stack? */ duk_push_tval(ctx, tv_obj); duk_push_tval(ctx, tv_key); - duk_to_string(ctx, -1); - key = duk_get_hstring(ctx, -1); - DUK_ASSERT(key != NULL); - tv_obj = duk_get_tval(ctx, -2); if (DUK_TVAL_IS_OBJECT(tv_obj)) { duk_hobject *obj = DUK_TVAL_GET_OBJECT(tv_obj); - DUK_ASSERT(obj != NULL); - rc = duk_hobject_delprop_raw(thr, obj, key, throw_flag); +#if defined(DUK_USE_ES6_PROXY) + if (DUK_UNLIKELY(DUK_HOBJECT_HAS_SPECIAL_PROXYOBJ(obj))) { + duk_hobject *h_target; + int tmp_bool; + + /* Note: proxy handling must happen before key is string coerced. */ + + if (duk__proxy_check(thr, obj, DUK_STRIDX_DELETE_PROPERTY, tv_key, &h_target)) { + /* -> [ ... func handler ] */ + DUK_DDDPRINT("-> proxy object 'deleteProperty' for key %!T", tv_key); + duk_push_hobject(ctx, h_target); /* target */ + duk_push_tval(ctx, tv_key); /* P */ + duk_call_method(ctx, 2 /*nargs*/); + tmp_bool = duk_to_boolean(ctx, -1); + duk_pop(ctx); + if (!tmp_bool) { + goto fail_proxy_rejected; /* retval indicates delete failed */ + } + + /* Target object must be checked for a conflicting + * non-configurable property. + */ + arr_idx = duk__push_tval_to_hstring_arr_idx(ctx, tv_key, &key); + DUK_ASSERT(key != NULL); + + if (duk__get_own_property_desc_raw(thr, h_target, key, arr_idx, &desc, 0 /*push_value*/)) { + DUK_DPRINT("proxy 'deleteProperty': target has matching property %!O, check for " + "conflicting property; desc.flags=0x%08x, " + "desc.get=%p, desc.set=%p", + key, (int) desc.flags, (void *) desc.get, (void *) desc.set); + + int desc_reject = !(desc.flags & DUK_PROPDESC_FLAG_CONFIGURABLE); + if (desc_reject) { + /* unconditional */ + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy deleteProperty rejected"); + } + } + rc = 1; /* success */ + goto done_rc; + } - duk_pop_2(ctx); /* [obj key] -> [] */ - return rc; + obj = h_target; /* resume delete to target */ + } +#endif /* DUK_USE_ES6_PROXY */ + + duk_to_string(ctx, -1); + key = duk_get_hstring(ctx, -1); + DUK_ASSERT(key != NULL); + + rc = duk_hobject_delprop_raw(thr, obj, key, throw_flag); + goto done_rc; } else if (DUK_TVAL_IS_STRING(tv_obj)) { duk_hstring *h = DUK_TVAL_GET_STRING(tv_obj); - DUK_ASSERT(h != NULL); + duk_to_string(ctx, -1); + key = duk_get_hstring(ctx, -1); + DUK_ASSERT(key != NULL); + if (key == DUK_HTHREAD_STRING_LENGTH(thr)) { goto fail_not_configurable; } @@ -3398,16 +3650,34 @@ int duk_hobject_delprop(duk_hthread *thr, duk_tval *tv_obj, duk_tval *tv_key, in goto fail_not_configurable; } } + /* FIXME: buffer virtual properties? */ - /* string without matching properties, or any other primitive base */ + /* non-object base, no offending virtual property */ + rc = 1; + goto done_rc; - duk_pop_2(ctx); /* [obj key] -> [] */ - return 1; + done_rc: + duk_set_top(ctx, entry_top); + return rc; + + fail_invalid_base_uncond: + /* Note: unconditional throw */ + DUK_ASSERT(duk_get_top(ctx) == entry_top); + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "invalid base reference for property delete"); + return 0; + + fail_proxy_rejected: + if (throw_flag) { + DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "proxy rejected"); + } + duk_set_top(ctx, entry_top); + return 0; fail_not_configurable: if (throw_flag) { DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "property not configurable"); } + duk_set_top(ctx, entry_top); return 0; } @@ -4967,4 +5237,3 @@ int duk_hobject_object_is_sealed_frozen_helper(duk_hobject *obj, int is_frozen) #undef DUK__HASH_UNUSED #undef DUK__HASH_DELETED #undef DUK__VALSTACK_SPACE - diff --git a/src/genbuiltins.py b/src/genbuiltins.py index d1486c2e..aff4aa23 100644 --- a/src/genbuiltins.py +++ b/src/genbuiltins.py @@ -255,7 +255,10 @@ bi_global = { { 'name': 'Math', 'value': { 'type': 'builtin', 'id': 'bi_math' } }, { 'name': 'JSON', 'value': { 'type': 'builtin', 'id': 'bi_json' } }, - # DUK specific + # ES6 (draft) + { 'name': 'Proxy', 'value': { 'type': 'builtin', 'id': 'bi_proxy_constructor' } }, + + # Duktape specific { 'name': 'Duktape', 'value': { 'type': 'builtin', 'id': 'bi_duktape' } }, ], 'functions': [ @@ -1092,6 +1095,25 @@ bi_type_error_thrower = { 'functions': [], } +# ES6 (draft) +bi_proxy_constructor = { + 'internal_prototype': 'bi_function_prototype', + # no external prototype + 'class': 'Function', + 'name': 'Proxy', + + 'length': 2, + 'native': 'duk_bi_proxy_constructor', + 'callable': True, + 'constructable': True, + + 'values': [], + 'functions': [ +# { 'name': 'revocable', 'native': 'duk_bi_proxy_constructor_revocable', 'length': 2 }, + ], +} + + bi_duktape = { 'internal_prototype': 'bi_object_prototype', 'class': 'Object', @@ -1319,6 +1341,9 @@ builtins_orig = [ { 'id': 'bi_json', 'info': bi_json }, { 'id': 'bi_type_error_thrower', 'info': bi_type_error_thrower }, + # es6 + { 'id': 'bi_proxy_constructor', 'info': bi_proxy_constructor }, + # custom { 'id': 'bi_duktape', 'info': bi_duktape }, { 'id': 'bi_thread_constructor', 'info': bi_thread_constructor }, diff --git a/src/genstrings.py b/src/genstrings.py index a03de905..b8e62c74 100644 --- a/src/genstrings.py +++ b/src/genstrings.py @@ -155,7 +155,6 @@ standard_builtin_string_list = [ mkstr("length"), mkstr("prototype"), mkstr("getPrototypeOf"), - mkstr("setPrototypeOf", es6=True), mkstr("getOwnPropertyDescriptor"), mkstr("getOwnPropertyNames"), mkstr("create"), @@ -185,7 +184,6 @@ standard_builtin_string_list = [ mkstr("hasOwnProperty"), mkstr("isPrototypeOf"), mkstr("propertyIsEnumerable"), - mkstr("__proto__", es6=True), # Object instances # no special properties @@ -477,6 +475,20 @@ standard_other_string_list = [ mkstr("set"), ] +# ES6 (draft) specific strings +es6_string_list = [ + mkstr("Proxy", es6=True), + #mkstr("revocable", es6=True), + + # Proxy handler methods + mkstr("set", es6=True), + mkstr("get", es6=True), + mkstr("deleteProperty", es6=True), + + mkstr("setPrototypeOf", es6=True), + mkstr("__proto__", es6=True), +] + # Duktape specific strings duk_string_list = [ # non-standard global properties @@ -543,6 +555,10 @@ duk_string_list = [ #mkstr("metatable", internal=True, custom=True), mkstr("finalizer", internal=True, custom=True), + # internal properties for Proxy objects + mkstr("target", internal=True, custom=True), # [[ProxyTarget]] + mkstr("handler", internal=True, custom=True), # [[ProxyHandler]] + # internal properties for declarative environment records mkstr("callee", internal=True, custom=True), # to access varmap mkstr("thread", internal=True, custom=True), # to identify valstack @@ -988,6 +1004,7 @@ def gen_string_list(): str_lists = [ standard_builtin_string_list, standard_other_string_list, + es6_string_list, duk_string_list ] for lst in str_lists: diff --git a/util/make_dist.sh b/util/make_dist.sh index 87fa8161..642df175 100644 --- a/util/make_dist.sh +++ b/util/make_dist.sh @@ -88,6 +88,7 @@ for i in \ duk_bi_protos.h \ duk_bi_regexp.c \ duk_bi_string.c \ + duk_bi_proxy.c \ duk_bi_thread.c \ duk_bi_thrower.c \ duk_dblunion.h \ diff --git a/website/buildsite.py b/website/buildsite.py index fd1b7b13..cafe2a2b 100644 --- a/website/buildsite.py +++ b/website/buildsite.py @@ -776,6 +776,7 @@ def generateGuide(): navlinks.append(['#functionobjects', 'Function objects']) navlinks.append(['#finalization', 'Finalization']) navlinks.append(['#coroutines', 'Coroutines']) + navlinks.append(['#propertyvirtualization', 'Property virtualization']) navlinks.append(['#compiling', 'Compiling']) navlinks.append(['#performance', 'Performance']) navlinks.append(['#portability', 'Portability']) @@ -811,6 +812,7 @@ def generateGuide(): res += processRawDoc('guide/functionobjects.html') res += processRawDoc('guide/finalization.html') res += processRawDoc('guide/coroutines.html') + res += processRawDoc('guide/propertyvirtualization.html') res += processRawDoc('guide/compiling.html') res += processRawDoc('guide/performance.html') res += processRawDoc('guide/portability.html') diff --git a/website/guide/compiling.html b/website/guide/compiling.html index 411b1b77..46388bab 100644 --- a/website/guide/compiling.html +++ b/website/guide/compiling.html @@ -21,11 +21,12 @@ in your compiler):
Duktape feature defaults are, at a high level:
Proxy
object which
+ is enabled by default.DUK_OPT_NO_PC2LINE
.
DUK_OPT_NO_JSONX
and DUK_OPT_NO_JSONC
.DUK_OPT_NO_OBJECT_ES6_SETPROTOTYPEOF
,
+ DUK_OPT_NO_OBJECT_ES6_PROTO_PROPERTY
,
+ DUK_OPT_NO_ES6_PROXY
.DUK_OPT_NO_REGEXP_SUPPORT
.-DDUK_OPT_DEBUG_BUFSIZE=2048
to reduce
diff --git a/website/guide/duktapebuiltins.html b/website/guide/duktapebuiltins.html
index ff0cf48c..c4eef86e 100644
--- a/website/guide/duktapebuiltins.html
+++ b/website/guide/duktapebuiltins.html
@@ -13,6 +13,7 @@ values.
stdout
.stderr
.This section describes the small set of features Duktape borrows from the -current ES6 draft. -These features are not fully compliant; the intent is to minimize custom features -and to align with the coming ES6 specification.
+current ES6 draft +("Version: Rev 23, April 5, 2014 Draft"). These features are not fully compliant; +the intent is to minimize custom features and to align with the coming ES6 specification.Object.setPrototypeOf
allows user to set the internal prototype of an object which is not supported in
Ecmascript E5. The Ecmascript E6 draft also provides
-Object.prototype.__proto__
-which is an accessor property (setter/getter) which provides the same functionality
+Object.prototype.__proto__,
+an accessor property (setter/getter) which provides the same functionality
but is compatible with existing code base which has relied on a non-standard
__proto__
property for a while. Duktape does not support the
__proto__ property name in an object initializer.
These custom features can be disabled with feature options.
+These custom features can be disabled with the feature options
+DUK_OPT_NO_OBJECT_ES6_SETPROTOTYPEOF
and
+DUK_OPT_NO_OBJECT_ES6_PROTO_PROPERTY
.
FIXME.
+The Ecmascript E6 Proxy
object allows property virtualization
+and fine-grained access control for accessing an underlying plain object.
+Duktape implements a strict subset of the Proxy
object from the
+ES6 draft (Rev 23).
Limitations include:
+get
, set
, deleteProperty
+ handler methods; ES6 draft allows a lot more, e.g. to interact with
+ properties being called or constructed, enumeration, etc. This causes
+ odd behavior if you enumerate a proxy object, call
+ Object.defineProperty()
with the proxy object as an argument,
+ etc.new Proxy()
cannot
+ be proxy objects themselves. ES6 poses no such limitation, but Duktape
+ enforces it to simplify the internal implementation.This custom feature can be disabled with the feature option
+DUK_OPT_NO_ES6_PROXY
.
The upcoming Ecmascript Edition 6 standard is not yet final. Duktape borrows -a few features from the E6 draft to minimize Duktape -custom features and aligning with the upcoming standard.
+a few features from the E6 draft: +setPrototypeOf
/__proto__
and a subset of Proxy
objectsprint()
and alert()
borrowed from
- browsers, Duktape specific built-ins in the Duktape
global objectprint()
and alert()
Duktape
global objectlua_dump()
Metatables
-There is currently no equivalent of Lua metatables in Duktape (or -Ecmascript). The -ES6 proxy object -concept will most likely provide similar functionality at some point.
+There is no equivalent of Lua metatables in Ecmascript E5/E5.1, but +Ecmascript E6 Proxy objects +provide similar functionality. To allow property virtualization better than available in +E5/E5.1, Duktape implements an ES6 Proxy subset.
.This section describes the two different mechanisms Duktape provides +for interacting with property accesses programmatically.
+ +Ecmascript Edition 5 provides accessor properties (also called +"setters and getters") which allow property read/write operations to be +captured by a user function.
+ +For example, to capture writes to obj.color
so that you
+can validate the color value and trigger a redraw as a side effect:
+var obj = {}; + +Object.defineProperty(obj, 'color', { + enumerable: false, + configurable: false, + get: function () { + // current color is stored in the raw _color property here + return this._color; + }, + set: function (v) { + if (!validateColor(v)) { + // only allow valid color formats to be assigned + throw new TypeError('invalid color: ' + v); + } + this._color = v; + redraw(); + } +}); + +// Change to red and trigger a redraw. +obj.color = '#ff0000'; ++ +
Setters and getters have the advantage of being part of the E5 standard +and of being widely implemented. However, they have significant limitations:
+In addition to accessors, Duktape provides a subset implementation of the
+Ecmascript E6 (draft) Proxy
concept (see
+Proxy Objects (E6 draft)
+and Proxy (Mozilla)).
+The current subset is limited to get
, set
, and
+deleteProperty
handler methods.
For example, to simply print a line whenever any property is accessed:
++// Underlying plain object. +var target = { foo: 'bar' }; + +// Handler table, provides methods for interaction (can be modified on-the-fly). +var handler = { + get: function (targ, key, recv) { + print('get called for key=' + key); + return targ[key]; // return unmodified value + }, + + set: function (targ, key, val, recv) { + print('set called for key=' + key + ', val=' + val); + targ[key] = val; // must perform write to target manually if 'set' defined + return true; // true: indicate that property write was allowed + }, + + deleteProperty: function (targ, key) { + print('deleteProperty called for key=' + key); + delete targ[key]; // must perform delete to target manually if 'deleteProperty' defined + return true; // true: indicate that property delete was allowed + } +}; + +// Create proxy object. +var proxy = new Proxy(target, handler); + +// Proxy object is then accessed normally. +proxy.foo = 321; +print(proxy.foo); +delete proxy.foo; ++ +
A Proxy object can also be used to create a read-only version of an +underlying object (which is quite tedious otherwise):
++var proxy = new Proxy(target, { + // get is omitted: reads go through to the target object automatically + + // set returns false: rejects write + set: function () { return false; }, + + // deleteProperty returns false: rejects delete + deleteProperty: function () { return false; } +}); ++ +
You can also create a write-only version of an object (which is not +possible otherwise):
++var proxy = new Proxy(target, { + get: function() { throw new TypeError('read not allowed'); } + + // set and deleteProperty are omitted: write/delete operations + // are allowed and go through to the target automatically +}); ++ +
The following is a more convoluted example combining multiple (somewhat +artificial) behaviors:
++var target = { foo: 'bar' }; + +/* + * - 'color' behaves like in the getter/setter example, cannot be deleted + * (attempt to do so causes a TypeError) + * + * - all string values are uppercased when read + * + * - property names beginning with an underscore are read/write/delete + * protected in a few different ways + */ + +var handler = { + get: function (targ, key, recv) { + // this binding: handler table + // targ: underlying plain object (= target, above) + // key: key (can be any value, not just a string) + // recv: object being read from (= the proxy object) + + if (typeof key === 'string' && key[0] === '_') { + throw new TypeError('attempt to access a read-protected property'); + } + + // Return value: value provided as property lookup result. + var val = targ[key]; + return (typeof val === 'string' ? val.toUpperCase() : val); + }, + + set: function (targ, key, val, recv) { + // this binding: handler table + // targ: underlying plain object (= target, above) + // key: key (can be any value, not just a string) + // val: value + // recv: object being read from (= the proxy object) + + if (typeof key === 'string') { + if (key === 'color') { + if (!validateColor(val)) { + throw new TypeError('invalid color: ' + val); + } + targ.color = val; + redraw(); + + // True: indicates to caller that property write allowed. + return true; + } else if (key[0] === '_') { + // False: indicates to caller that property write rejected. + // In non-strict mode this is ignored silently, but in strict + // mode a TypeError is thrown. + return false; + } + } + + // Write to target. We could also return true without writing to the + // target to simulate a successful write without changing the target. + targ[key] = val; + return true; + }, + + deleteProperty: function (targ, key) { + // this binding: handler table + // targ: underlying plain object (= target, above) + // key: key (can be any value, not just a string) + + if (typeof key === 'string') { + if (key === 'color') { + // For 'color' a delete attempt causes an explicit error. + throw new TypeError('attempt to delete the color property'); + } else if (key[0] === '_') { + // False: indicates to caller that property delete rejected. + // In non-strict mode this is ignored silently, but in strict + // mode a TypeError is thrown. + return false; + } + } + + // Delete from target. We could also return true without deleting + // from the target to simulate a successful delete without changing + // the target. + delete targ[key]; + return true; + } +}; ++ +
The ES6 draft semantics reject some property accesses even if the handler +method would allow it. This happens if the proxy's target object has a non-configurable +conflicting property; see E6 draft Sections 9.5.8, 9.5.9, and 9.5.10 for details. +You can easily avoid any such behaviors by keeping the target object empty and, +if necessary, backing the virtual properties in an unrelated plain object.
+ +Also see:
+Proxy
implementation.Proxy
object.Proxy
implemented in Firefox, contains a lot of examples.Main features: