From 125d23cdd53c082cec0813daab9be7f698c45ccb Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Wed, 13 Mar 2024 21:05:04 +0100 Subject: [PATCH] vm: rework `in` operator semantics - Ensure that testing for array membership does strict equality tests - Ensure that `(NaN in [ NaN ]) == true` - Do not perform implicit value conversion when testing for object keys, to avoid nonsensical results such as `([] in { "[ ]": true }) == true` - Add test cases for the `in` operator Fixes: #193 Signed-off-by: Jo-Philipp Wich --- tests/custom/00_syntax/28_in_operator | 132 ++++++++++++++++++++++++++ vm.c | 49 +++++++--- 2 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 tests/custom/00_syntax/28_in_operator diff --git a/tests/custom/00_syntax/28_in_operator b/tests/custom/00_syntax/28_in_operator new file mode 100644 index 00000000..a0b6dfdf --- /dev/null +++ b/tests/custom/00_syntax/28_in_operator @@ -0,0 +1,132 @@ +The "in" operator allows testing whether a given value is an item of +a specified array or whether a given key is present in a specified +dictionary. + +When testing whether an element is part of an array, strict equality +checking is performed. + + +1. The `in` operator returns true if the given element is an item of +the specified array. Strict equality tests are performed. + +-- Expect stdout -- +[ + true, + false, + true, + false, + true, + false, + true, + false +] +-- End -- + +-- Testcase -- +{% + let o = {}; + let a = [ o, {}, "", null, false ]; + + printf("%.J\n", [ + o in a, + {} in a, + "" in a, + "test" in a, + null in a, + [] in a, + false in a, + true in a + ]); +%} +-- End -- + +2. Strict equality when testing array membership should rule out implict +type coercion. + +-- Expect stdout -- +[ + true, + false, + false, + false, + true, + false, + false +] +-- End -- + +-- Testcase -- +{% + let a = [ "", true ]; + + printf("%.J\n", [ + "" in a, + 0 in a, + false in a, + null in a, + true in a, + 1 in a, + 1.0 in a + ]); +%} +-- End -- + +3. While there is the rule that `(NaN === NaN) == false`, testing for NaN +in a given array containing NaN should yield `true`. + +-- Expect stdout -- +[ + true +] +-- End -- + +-- Testcase -- +{% + let a = [ NaN ]; + + printf("%.J\n", [ + NaN in a + ]); +%} +-- End -- + +4. When the `in` operator is applied to an object, it tests whether the given +string value is a key of the specified object. + +-- Expect stdout -- +[ + true, + true, + true, + false, + false, + false, + false, + false +] +-- End -- + +-- Testcase -- +{% + let o = { + "1": true, + "test": false, + "empty": null, + "false": 0, + "true": 1, + "[ ]": "array", + "{ }": "object" + }; + + printf("%.J\n", [ + "1" in o, + "test" in o, + "empty" in o, + 1 in o, // not implicitly converted to "1" + false in o, // not implicitly converted to "false" + true in o, // not implicitly converted to "true" + [] in o, // not implicitly converted to "[ ]" + {} in o // not implicitly converted to "{ }" + ]); +%} +-- End -- diff --git a/vm.c b/vm.c index 4642380a..bb6dc2f2 100644 --- a/vm.c +++ b/vm.c @@ -993,6 +993,37 @@ uc_vm_raise_exception(uc_vm_t *vm, uc_exception_type_t type, const char *fmt, .. vm->exception.stacktrace = uc_vm_get_error_context(vm); } +static bool +uc_vm_test_strict_equality(uc_value_t *v1, uc_value_t *v2, bool nan_equal) +{ + uc_type_t t1 = ucv_type(v1); + uc_type_t t2 = ucv_type(v2); + double d1, d2; + + if (t1 != t2) + return false; + + switch (t1) { + case UC_DOUBLE: + d1 = ((uc_double_t *)v1)->dbl; + d2 = ((uc_double_t *)v2)->dbl; + + if (isnan(d1) && isnan(d2)) + return nan_equal; + + return (d1 == d2); + + case UC_NULL: + case UC_BOOLEAN: + case UC_INTEGER: + case UC_STRING: + return ucv_is_equal(v1, v2); + + default: + return (v1 == v2); + } +} + static void uc_vm_insn_load(uc_vm_t *vm, uc_vm_insn_t insn) @@ -2066,7 +2097,6 @@ uc_vm_insn_in(uc_vm_t *vm, uc_vm_insn_t insn) uc_value_t *item; size_t arrlen, arridx; bool found = false; - char *key; switch (ucv_type(r2)) { case UC_ARRAY: @@ -2074,7 +2104,7 @@ uc_vm_insn_in(uc_vm_t *vm, uc_vm_insn_t insn) arridx < arrlen; arridx++) { item = ucv_array_get(r2, arridx); - if (ucv_compare(I_EQ, r1, item, NULL)) { + if (uc_vm_test_strict_equality(r1, item, true)) { found = true; break; } @@ -2083,14 +2113,8 @@ uc_vm_insn_in(uc_vm_t *vm, uc_vm_insn_t insn) break; case UC_OBJECT: - if (ucv_type(r1) == UC_STRING) { + if (ucv_type(r1) == UC_STRING) ucv_object_get(r2, ucv_string_get(r1), &found); - } - else { - key = ucv_to_string(vm, r1); - ucv_object_get(r2, key, &found); - free(key); - } break; @@ -2109,12 +2133,7 @@ uc_vm_insn_equality(uc_vm_t *vm, uc_vm_insn_t insn) { uc_value_t *r2 = uc_vm_stack_pop(vm); uc_value_t *r1 = uc_vm_stack_pop(vm); - bool equal; - - if (ucv_is_scalar(r1) && ucv_is_scalar(r2)) - equal = ucv_is_equal(r1, r2); - else - equal = (r1 == r2); + bool equal = uc_vm_test_strict_equality(r1, r2, false); ucv_put(r1); ucv_put(r2);