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:

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:

+

Features

@@ -49,8 +51,8 @@ features (some are visible to applications, while others are internal):