Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add array_is_list(array $array) function (rebased) #6070

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Zend/zend_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,33 @@ static zend_always_inline void *zend_hash_get_current_data_ptr_ex(HashTable *ht,
ZEND_HASH_FILL_FINISH(); \
} while (0)

/* Check if an array is a list */
static zend_always_inline zend_bool zend_array_is_list(zend_array *array)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd split this into an inlined fast path (num_elements + packed without holes check) and an out of line slow path (that does the full table scan).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I guess with just two usages right now making the whole thing inline isn't terrible either.

{
zend_long expected_idx = 0;
zend_long num_idx;
zend_string* str_idx;
/* Empty arrays are lists */
if (zend_hash_num_elements(array) == 0) {
return 1;
}

/* Packed arrays are lists */
if (HT_IS_PACKED(array) && HT_IS_WITHOUT_HOLES(array)) {
nikic marked this conversation as resolved.
Show resolved Hide resolved
return 1;
}

/* Check if the list could theoretically be repacked */
ZEND_HASH_FOREACH_KEY(array, num_idx, str_idx) {
Copy link
Member

@twose twose Sep 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we rebase against the master, and try to use ZEND_HASH_REVERSE_FOREACH_KEY, just like what you said on #4886 (comment), Thanks!

if (str_idx != NULL || num_idx != expected_idx++) {
return 0;
}
} ZEND_HASH_FOREACH_END();

return 1;
}


static zend_always_inline zval *_zend_hash_append_ex(HashTable *ht, zend_string *key, zval *zv, bool interned)
{
uint32_t idx = ht->nNumUsed++;
Expand Down
25 changes: 3 additions & 22 deletions ext/json/json_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,10 @@ static int php_json_escape_string(

static int php_json_determine_array_type(zval *val) /* {{{ */
{
int i;
HashTable *myht = Z_ARRVAL_P(val);

i = myht ? zend_hash_num_elements(myht) : 0;
if (i > 0) {
zend_string *key;
zend_ulong index, idx;
zend_array *myht = Z_ARRVAL_P(val);

if (HT_IS_PACKED(myht) && HT_IS_WITHOUT_HOLES(myht)) {
return PHP_JSON_OUTPUT_ARRAY;
}

idx = 0;
ZEND_HASH_FOREACH_KEY(myht, index, key) {
if (key) {
return PHP_JSON_OUTPUT_OBJECT;
} else {
if (index != idx) {
return PHP_JSON_OUTPUT_OBJECT;
}
}
idx++;
} ZEND_HASH_FOREACH_END();
if (myht) {
return zend_array_is_list(myht) ? PHP_JSON_OUTPUT_ARRAY : PHP_JSON_OUTPUT_OBJECT;
}

return PHP_JSON_OUTPUT_ARRAY;
Expand Down
1 change: 1 addition & 0 deletions ext/opcache/Optimizer/sccp.c
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ static zend_bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zva
|| zend_string_equals_literal(name, "array_diff")
|| zend_string_equals_literal(name, "array_diff_assoc")
|| zend_string_equals_literal(name, "array_diff_key")
|| zend_string_equals_literal(name, "array_is_list")
|| zend_string_equals_literal(name, "array_key_exists")
|| zend_string_equals_literal(name, "array_keys")
|| zend_string_equals_literal(name, "array_merge")
Expand Down
2 changes: 2 additions & 0 deletions ext/standard/basic_functions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ function array_chunk(array $array, int $length, bool $preserve_keys = false): ar

function array_combine(array $keys, array $values): array {}

function array_is_list(array $array): bool {}

/* base64.c */

function base64_encode(string $string): string {}
Expand Down
8 changes: 7 additions & 1 deletion ext/standard/basic_functions_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 21e54280829776de72313b96e38ad2aee60bd0ee */
* Stub hash: a4779a303018370a7a222d80cd69cb37d6a52db9 */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0)
Expand Down Expand Up @@ -360,6 +360,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_combine, 0, 2, IS_ARRAY, 0
ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_is_list, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_base64_encode, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
Expand Down Expand Up @@ -2309,6 +2313,7 @@ ZEND_FUNCTION(array_map);
ZEND_FUNCTION(array_key_exists);
ZEND_FUNCTION(array_chunk);
ZEND_FUNCTION(array_combine);
ZEND_FUNCTION(array_is_list);
ZEND_FUNCTION(base64_encode);
ZEND_FUNCTION(base64_decode);
ZEND_FUNCTION(constant);
Expand Down Expand Up @@ -2933,6 +2938,7 @@ static const zend_function_entry ext_functions[] = {
ZEND_FALIAS(key_exists, array_key_exists, arginfo_key_exists)
ZEND_FE(array_chunk, arginfo_array_chunk)
ZEND_FE(array_combine, arginfo_array_combine)
ZEND_FE(array_is_list, arginfo_array_is_list)
ZEND_FE(base64_encode, arginfo_base64_encode)
ZEND_FE(base64_decode, arginfo_base64_decode)
ZEND_FE(constant, arginfo_constant)
Expand Down
98 changes: 98 additions & 0 deletions ext/standard/tests/general_functions/array_is_list.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
--TEST--
Test array_is_list() function
--FILE--
<?php

function test_is_list(string $desc, $val) : void {
try {
printf("%s: %s\n", $desc, json_encode(array_is_list($val)));
} catch (TypeError $e) {
printf("%s: threw %s\n", $desc, $e->getMessage());
}
}

test_is_list("empty", []);
test_is_list("one", [1]);
test_is_list("two", [1,2]);
test_is_list("three", [1,2,3]);
test_is_list("four", [1,2,3,4]);
test_is_list("ten", range(0, 10));

test_is_list("null", null);
test_is_list("int", 123);
test_is_list("float", 1.23);
test_is_list("string", "string");
test_is_list("object", new stdClass());
test_is_list("true", true);
test_is_list("false", false);

test_is_list("string key", ["a" => 1]);
test_is_list("mixed keys", [0 => 0, "a" => 1]);
test_is_list("ordered keys", [0 => 0, 1 => 1]);
test_is_list("shuffled keys", [1 => 0, 0 => 1]);
test_is_list("skipped keys", [0 => 0, 2 => 2]);

$arr = [1, 2, 3];
unset($arr[0]);
test_is_list("unset first", $arr);

$arr = [1, 2, 3];
unset($arr[1]);
test_is_list("unset middle", $arr);

$arr = [1, 2, 3];
unset($arr[2]);
test_is_list("unset end", $arr);

$arr = [1, "a" => "a", 2];
unset($arr["a"]);
test_is_list("unset string key", $arr);

$arr = [1 => 1, 0 => 0];
unset($arr[1]);
test_is_list("unset into order", $arr);

$arr = ["a" => 1];
unset($arr["a"]);
test_is_list("unset to empty", $arr);

$arr = [1, 2, 3];
$arr[] = 4;
test_is_list("append implicit", $arr);

$arr = [1, 2, 3];
$arr[3] = 4;
test_is_list("append explicit", $arr);

$arr = [1, 2, 3];
$arr[4] = 5;
test_is_list("append with gap", $arr);

--EXPECT--
empty: true
one: true
two: true
three: true
four: true
ten: true
null: threw array_is_list(): Argument #1 ($array) must be of type array, null given
int: threw array_is_list(): Argument #1 ($array) must be of type array, int given
float: threw array_is_list(): Argument #1 ($array) must be of type array, float given
string: threw array_is_list(): Argument #1 ($array) must be of type array, string given
object: threw array_is_list(): Argument #1 ($array) must be of type array, stdClass given
true: threw array_is_list(): Argument #1 ($array) must be of type array, bool given
false: threw array_is_list(): Argument #1 ($array) must be of type array, bool given
string key: false
mixed keys: false
ordered keys: true
shuffled keys: false
skipped keys: false
unset first: false
unset middle: false
unset end: true
unset string key: true
unset into order: true
unset to empty: true
append implicit: true
append explicit: true
append with gap: false
13 changes: 13 additions & 0 deletions ext/standard/type.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,19 @@ PHP_FUNCTION(is_array)
}
/* }}} */

/* {{{ Returns true if $array is an array whose keys are all numeric, sequential, and start at 0 */
PHP_FUNCTION(array_is_list)
{
HashTable *array;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(array)
ZEND_PARSE_PARAMETERS_END();

RETURN_BOOL(zend_array_is_list(array));
}
/* }}} */

/* {{{ Returns true if variable is an object
Warning: This function is special-cased by zend_compile.c and so is usually bypassed */
PHP_FUNCTION(is_object)
Expand Down