diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd4640..920599b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 relied on the previous unconditional by-name behavior must pass the flag. Closures declared in constant expressions are unaffected. +- On PHP 8.6, `deepclone_to_array()` references anonymous closures declared in + attribute arguments and parameter default values as `[class, id, line]`, where + `id` is the engine's canonical const-expr closure id (see + `Closure::fromConstExpr()`). This replaces the per-call declaration-site scan + with the engine's non-evaluating walk. `deepclone_from_array()` accepts both + this and the site-based form: site-based payloads written on PHP 8.5 keep + resolving on 8.6, and engine-id payloads fail with an explicit message on older + PHP. +- On PHP 8.6, first-class callables declared in a constant expression of another + class (`#[When(Validators::check(...))]`) or over a global function + (`#[When(strlen(...))]`) get their declaring class from the engine and + serialize as a (site-based) declaration-site reference with no + `allow_named_closures` opt-in -- the same payload the extension produces on + 8.5 through ReflectionAttribute provenance, and that the polyfill produces. + (First-class callables keep the site-based form rather than an engine id: an + engine id resolves to an fcc site whose source line userland cannot reproduce, + which would break interchange with the polyfill.) + - On PHP 8.4+, `deepclone_from_array()` now creates object nodes whose payload slots or replayed `__unserialize` state carry a named-closure or const-expr-closure marker as diff --git a/deepclone.c b/deepclone.c index d53ca97..8b6447e 100644 --- a/deepclone.c +++ b/deepclone.c @@ -1566,6 +1566,26 @@ static zend_class_entry *dc_provenance_lookup(zend_class_entry *target_ce, zend_ return zend_lookup_class_ex(decl, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); } +/* The class whose constant expression declares this first-class callable. On + * PHP 8.6 the engine records it (zend_constexpr_closure_ref), so it is exact + * and needs no capture; on 8.5 it comes from the ReflectionAttribute-captured + * index. Either way it feeds the same site-based (5-element) reference, which + * is interchangeable with the polyfill — unlike the engine-id form, whose fcc + * line userland cannot reproduce. */ +static zend_class_entry *dc_declaring_class(zval *src, const zend_function *func) +{ +#if PHP_VERSION_ID >= 80600 + zend_class_entry *ce; + uint32_t id, line; + if (zend_constexpr_closure_ref(Z_OBJ_P(src), &ce, &id, &line) == SUCCESS) { + return ce; + } +#endif + return func->common.function_name + ? dc_provenance_lookup(func->common.scope, func->common.function_name) + : NULL; +} + /* Walk a value (a getArguments() argument, or a newInstance() attribute object * and its properties), recording every cross-class FCC against `scope`. The * `seen` set guards cycles: getArguments() values are acyclic constant @@ -1656,6 +1676,66 @@ static void ZEND_FASTCALL dc_attr_new_instance_wrapper(INTERNAL_FUNCTION_PARAMET } #endif /* PHP_VERSION_ID >= 80500 */ +/* deepclone_from_array() counterpart for engine-id references [class, id, + * line], emitted on PHP >= 8.6: the id is the engine's canonical per-class + * const-expr closure id (see Closure::fromConstExpr()). */ +static void dc_cexpr_resolve_id(HashTable *ht, HashTable *allowed_set, zval *retval) +{ + zval *zclass = zend_hash_index_find(ht, 0); + zval *zid = zend_hash_index_find(ht, 1); + zval *zline = zend_hash_index_find(ht, 2); + if (!zclass || !zid || !zline || zend_hash_num_elements(ht) != 3) { + zend_value_error("deepclone_from_array(): malformed payload, const-expr-closure value must have 3 elements"); + return; + } + ZVAL_DEREF(zclass); + ZVAL_DEREF(zid); + ZVAL_DEREF(zline); + if (Z_TYPE_P(zclass) != IS_STRING) { + zend_value_error("deepclone_from_array(): malformed payload, const-expr-closure class name must be of type string, %s given", zend_zval_value_name(zclass)); + return; + } + if (Z_TYPE_P(zline) != IS_LONG) { + zend_value_error("deepclone_from_array(): malformed payload, const-expr-closure line must be of type int, %s given", zend_zval_value_name(zline)); + return; + } + + /* Gate before zend_lookup_class(): the payload must not be able to + * autoload, let alone evaluate, classes outside the allow-list. */ + if (!dc_class_allowed(allowed_set, Z_STR_P(zclass))) { + zend_value_error("deepclone_from_array(): class \"%s\" is not allowed", Z_STRVAL_P(zclass)); + return; + } + +#if PHP_VERSION_ID >= 80600 + zend_class_entry *ce = zend_lookup_class(Z_STR_P(zclass)); + if (!ce) { + zend_value_error("deepclone_from_array(): malformed payload, const-expr-closure references unknown class \"%s\"", Z_STRVAL_P(zclass)); + return; + } + + zend_ast *site = zend_constexpr_closure_site_by_id(ce, Z_LVAL_P(zid)); + if (!site) { + zend_value_error("deepclone_from_array(): malformed payload, const-expr-closure references unknown closure id " ZEND_LONG_FMT " in class \"%s\"", Z_LVAL_P(zid), ZSTR_VAL(ce->name)); + return; + } + if (site->kind != ZEND_AST_OP_ARRAY) { + zend_value_error("deepclone_from_array(): malformed payload, const-expr-closure references a first-class callable site"); + return; + } + + zend_op_array *op = zend_ast_get_op_array(site)->op_array; + if (Z_LVAL_P(zline) != (zend_long) op->line_start) { + zend_value_error("deepclone_from_array(): stale payload, const-expr-closure moved from line " ZEND_LONG_FMT " to line %u", Z_LVAL_P(zline), op->line_start); + return; + } + + zend_create_closure(retval, (zend_function *) op, ce, ce, NULL); +#else + zend_value_error("deepclone_from_array(): const-expr-closure payload was created on PHP 8.6 or later and cannot be resolved on PHP %s", PHP_VERSION); +#endif +} + /* deepclone_from_array() counterpart: resolve a declaration-site reference * back to a live Closure. */ static void dc_cexpr_resolve(zval *value, HashTable *allowed_set, zval *retval) @@ -1665,6 +1745,18 @@ static void dc_cexpr_resolve(zval *value, HashTable *allowed_set, zval *retval) return; } HashTable *ht = Z_ARRVAL_P(value); + + zval *zid = zend_hash_index_find(ht, 1); + if (zid) { + ZVAL_DEREF(zid); + } + if (zid && Z_TYPE_P(zid) == IS_LONG) { + /* The type of element 1 (int id vs string site) discriminates + * engine-id references from site-based ones. */ + dc_cexpr_resolve_id(ht, allowed_set, retval); + return; + } + zval *zclass = zend_hash_index_find(ht, 0); zval *zsite = zend_hash_index_find(ht, 1); zval *zattr = zend_hash_index_find(ht, 2); @@ -2031,6 +2123,31 @@ static void dc_copy_value(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst) zend_value_error("deepclone_to_array(): class \"Closure\" is not allowed"); return; } +#if PHP_VERSION_ID >= 80600 + /* The engine assigns a canonical per-class id to anonymous closures + * declared in attribute arguments and parameter default values; prefer + * it to the site-based reference below. First-class callables are + * excluded: their engine id resolves to an fcc site the decode side + * cannot recreate, so they keep the site-based and by-name paths. + * Closures in class constant values and property defaults have no id + * and also fall through. */ + if (!(func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) { + zend_class_entry *site_ce; + uint32_t cexpr_id, cexpr_line; + if (zend_constexpr_closure_ref(Z_OBJ_P(src), &site_ce, &cexpr_id, &cexpr_line) == SUCCESS) { + zval tmp; + array_init_size(dst, 3); + ZVAL_STR_COPY(&tmp, site_ce->name); + zend_hash_index_add_new(Z_ARRVAL_P(dst), 0, &tmp); + ZVAL_LONG(&tmp, (zend_long) cexpr_id); + zend_hash_index_add_new(Z_ARRVAL_P(dst), 1, &tmp); + ZVAL_LONG(&tmp, (zend_long) cexpr_line); + zend_hash_index_add_new(Z_ARRVAL_P(dst), 2, &tmp); + DC_MASK_CONSTEXPR_CLOSURE(mask_dst); + goto handle_value; + } + } +#endif zval payload; ZVAL_UNDEF(&payload); if (dc_cexpr_locate(func, &payload)) { @@ -2046,8 +2163,8 @@ static void dc_copy_value(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst) * scope) misses. On 8.5 there is no engine provenance; fall back * to a declaring class captured from ReflectionAttribute, if * any, and locate the site there. */ - if (DC_G(capture_attribute_closures) && func->common.scope && func->common.function_name) { - zend_class_entry *decl = dc_provenance_lookup(func->common.scope, func->common.function_name); + if (func->common.function_name) { + zend_class_entry *decl = dc_declaring_class(src, func); if (decl && decl != func->common.scope && dc_cexpr_locate_ce(func, decl, &payload)) { ZVAL_COPY_VALUE(dst, &payload); DC_MASK_CONSTEXPR_CLOSURE(mask_dst); @@ -2061,15 +2178,15 @@ static void dc_copy_value(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst) } /* Global-function first-class callable (no scope, internal or user): - * the declaring class can only come from captured provenance. Same + * the declaring class comes from the engine (8.6) or captured + * provenance (8.5). Same * declaration-site reference and Closure gating as above; unresolved * ones fall through to the by-name path. */ if (func && (func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) - && !func->common.scope && func->common.function_name - && DC_G(capture_attribute_closures)) { + && !func->common.scope && func->common.function_name) { zval *this_ptr = zend_get_closure_this_ptr(src); if (!this_ptr || Z_TYPE_P(this_ptr) != IS_OBJECT) { - zend_class_entry *decl = dc_provenance_lookup(NULL, func->common.function_name); + zend_class_entry *decl = dc_declaring_class(src, func); if (decl) { if (!dc_class_allowed(ctx->allowed_ht, zend_ce_closure->name)) { zend_value_error("deepclone_to_array(): class \"Closure\" is not allowed"); diff --git a/tests/deepclone_constexpr_closures.phpt b/tests/deepclone_constexpr_closures.phpt index a2ce192..0ed9386 100644 --- a/tests/deepclone_constexpr_closures.phpt +++ b/tests/deepclone_constexpr_closures.phpt @@ -4,6 +4,7 @@ deepclone references closures declared in constant expressions (PHP 8.5) deepclone --SKIPIF-- += 80600) die('skip PHP 8.6 emits engine-id references, covered by deepclone_constexpr_closures_native.phpt'); ?> --FILE-- = 80600) die('skip PHP < 8.6 only'); ?> +--FILE-- + '', 'objectMeta' => 0, 'prepared' => [Fix::class, 0, 1], 'mask' => 1]); +} catch (\ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +deepclone_from_array(): const-expr-closure payload was created on PHP 8.6 or later and cannot be resolved on PHP %s diff --git a/tests/deepclone_constexpr_closures_native.phpt b/tests/deepclone_constexpr_closures_native.phpt new file mode 100644 index 0000000..c70e96d --- /dev/null +++ b/tests/deepclone_constexpr_closures_native.phpt @@ -0,0 +1,228 @@ +--TEST-- +deepclone references const-expr closures through engine ids (PHP 8.6) +--EXTENSIONS-- +deepclone +--SKIPIF-- + +--FILE-- +args = $args; } } + +#[CA(static function (): string { return self::SECRET; })] +class Fix { + private const SECRET = 'class-secret'; + public const CALLBACKS = ['first' => static function (): string { return 'const-value'; }]; + #[CA(cb: [1, ['x' => static function (int $i): int { return $i * 2; }]])] + public string $tagged = 'v'; + public ?Closure $factory = static function (): string { return 'prop-default'; }; + #[CA('not-a-closure')] + #[CA(static function (): string { return 'repeated'; })] + public function tagged( + #[CA(static function (): string { return 'param-attr'; })] + ?Closure $cb = static function (): string { return 'param-default'; }, + ): void {} +} + +$rc = new ReflectionClass(Fix::class); + +// ── Wire format: engine-id reference [class, id, line] ── +$c = $rc->getAttributes()[0]->getArguments()[0]; +$line = (new ReflectionFunction($c))->getStartLine(); +$d = deepclone_to_array($c); +var_dump($d['prepared'] === [Fix::class, 0, $line]); +var_dump($d['mask'] === 1); +$r = deepclone_from_array($d); +var_dump($r instanceof Closure, $r !== $c, $r() === 'class-secret'); + +// ── The emitted reference matches the engine's ── +$rf = new ReflectionFunction($c); +var_dump($d['prepared'] === [$rf->getConstExprClass(), $rf->getConstExprId(), $line]); + +// ── Attribute sites: nested argument, repeated attribute, parameter attribute, parameter default ── +foreach ([ + [$rc->getProperty('tagged')->getAttributes()[0]->getArguments()['cb'][1]['x'], [3], 6], + [$rc->getMethod('tagged')->getAttributes()[1]->getArguments()[0], [], 'repeated'], + [$rc->getMethod('tagged')->getParameters()[0]->getAttributes()[0]->getArguments()[0], [], 'param-attr'], + [$rc->getMethod('tagged')->getParameters()[0]->getDefaultValue(), [], 'param-default'], +] as [$c, $args, $expected]) { + $d = deepclone_to_array($c); + $rf = new ReflectionFunction($c); + var_dump($d['prepared'] === [Fix::class, $rf->getConstExprId(), $rf->getStartLine()], deepclone_from_array($d)(...$args) === $expected); +} + +// ── Constant values and property defaults have no engine id: site-based form ── +$d = deepclone_to_array(Fix::CALLBACKS['first']); +var_dump(count($d['prepared']) === 5, $d['prepared'][1] === 'CALLBACKS', deepclone_from_array($d)() === 'const-value'); +$d = deepclone_to_array($rc->getProperty('factory')->getDefaultValue()); +var_dump($d['prepared'][1] === '$factory', deepclone_from_array($d)() === 'prop-default'); + +// ── Site-based references written on PHP 8.5 still resolve ── +var_dump(deepclone_from_array(['classes' => '', 'objectMeta' => 0, 'prepared' => [Fix::class, '', 0, 0, $line], 'mask' => 1])() === 'class-secret'); + +// ── Same-line closures get distinct ids ── +#[CA(static function (): string { return 'first'; }, static function (): string { return 'second'; })] +class FixAmbiguous {} +$args = (new ReflectionClass(FixAmbiguous::class))->getAttributes()[0]->getArguments(); +$d0 = deepclone_to_array($args[0]); +$d1 = deepclone_to_array($args[1]); +var_dump([$d0['prepared'][1], $d1['prepared'][1]] === [0, 1]); +var_dump(deepclone_from_array($d0)() === 'first', deepclone_from_array($d1)() === 'second'); + +// ── Enum case attribute gets an id, enum constant value stays site-based ── +enum FixEnum: string { + #[CA(static function (): string { return 'enum-case-attr'; })] + case Active = 'A'; + public const FILTER = static function (): string { return 'enum-const'; }; +} +$d = deepclone_to_array((new ReflectionClassConstant(FixEnum::class, 'Active'))->getAttributes()[0]->getArguments()[0]); +var_dump(is_int($d['prepared'][1]), deepclone_from_array($d)() === 'enum-case-attr'); +$d = deepclone_to_array(FixEnum::FILTER); +var_dump($d['prepared'][1] === 'FILTER', deepclone_from_array($d)() === 'enum-const'); + +// ── Property hooks ── +class FixHooked { + public string $virtual { + #[CA(static function (): string { return 'get-hook-attr'; })] + get => 'vx'; + } +} +$c = (new ReflectionProperty(FixHooked::class, 'virtual'))->getHook(PropertyHookType::Get)->getAttributes()[0]->getArguments()[0]; +$d = deepclone_to_array($c); +var_dump(is_int($d['prepared'][1]), deepclone_from_array($d)() === 'get-hook-attr'); + +// ── Trait method attribute: the using class declares the closure ── +trait FixTrait { + #[CA(static function (): string { return 'trait-attr'; })] + public function traitTagged(): void {} +} +class FixTraitUser { use FixTrait; } +$d = deepclone_to_array((new ReflectionClass(FixTraitUser::class))->getMethod('traitTagged')->getAttributes()[0]->getArguments()[0]); +var_dump(is_int($d['prepared'][1]), deepclone_from_array($d)() === 'trait-attr'); + +// ── Inherited declaration keeps the declaring class ── +class FixParent { + #[CA(static function (): string { return 'parent-attr'; })] + public function pm(): void {} +} +class FixChild extends FixParent {} +$c = (new ReflectionMethod(FixChild::class, 'pm'))->getAttributes()[0]->getArguments()[0]; +$d = deepclone_to_array($c); +var_dump($d['prepared'][0] === FixParent::class, deepclone_from_array($d)() === 'parent-attr'); + +// ── First-class callables use the site-based reference, not an engine id: +// the engine id of an fcc resolves to a site the decode path cannot recreate, +// so they keep the declaration-site (5-element) form ── +class FixFcc { + #[CA(self::helper(...))] + public static function helper(): bool { return true; } +} +$d = deepclone_to_array((new ReflectionMethod(FixFcc::class, 'helper'))->getAttributes()[0]->getArguments()[0]); +var_dump($d['mask'] === 1, deepclone_from_array($d)() === true); + +// ── ... but a crafted payload addressing the FCC site is rejected ── +try { + deepclone_from_array(['classes' => '', 'objectMeta' => 0, 'prepared' => [FixFcc::class, 0, 1], 'mask' => 1]); +} catch (\ValueError $e) { + var_dump($e->getMessage()); +} + +// ── Runtime closures still refuse, through the engine's own __serialize() ── +try { + deepclone_to_array(static function () { return 'runtime'; }); +} catch (\Exception $e) { + var_dump($e->getMessage()); +} + +// ── Object graph survives a JSON round trip ── +$graph = (object) ['cb' => $rc->getAttributes()[0]->getArguments()[0]]; +$d = json_decode(json_encode(deepclone_to_array($graph)), true); +var_dump((deepclone_from_array($d)->cb)() === 'class-secret'); + +// ── allowed_classes gating, both directions ── +try { + deepclone_to_array($rc->getAttributes()[0]->getArguments()[0], []); +} catch (\ValueError $e) { + var_dump($e->getMessage()); +} +$d = deepclone_to_array($rc->getAttributes()[0]->getArguments()[0], ['Closure']); +try { + deepclone_from_array($d, []); +} catch (\ValueError $e) { + var_dump($e->getMessage()); +} +try { + deepclone_from_array($d, ['Closure']); +} catch (\ValueError $e) { + var_dump($e->getMessage()); +} +var_dump(deepclone_from_array($d, ['Closure', 'Fix'])() === 'class-secret'); + +// ── Stale payload ── +$d = deepclone_to_array($rc->getAttributes()[0]->getArguments()[0]); +$d['prepared'][2]++; +try { + deepclone_from_array($d); +} catch (\ValueError $e) { + var_dump(str_contains($e->getMessage(), 'stale payload, const-expr-closure moved from line')); +} + +// ── Unknown id, unknown class ── +try { + deepclone_from_array(['classes' => '', 'objectMeta' => 0, 'prepared' => [Fix::class, 999, $line], 'mask' => 1]); +} catch (\ValueError $e) { + var_dump($e->getMessage()); +} +try { + deepclone_from_array(['classes' => '', 'objectMeta' => 0, 'prepared' => ['No\Such\ClassAtAll', 0, 1], 'mask' => 1]); +} catch (\ValueError $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(100) "deepclone_from_array(): malformed payload, const-expr-closure references a first-class callable site" +string(41) "Serialization of 'Closure' is not allowed" +bool(true) +string(52) "deepclone_to_array(): class "Closure" is not allowed" +string(54) "deepclone_from_array(): class "Closure" is not allowed" +string(50) "deepclone_from_array(): class "Fix" is not allowed" +bool(true) +bool(true) +string(110) "deepclone_from_array(): malformed payload, const-expr-closure references unknown closure id 999 in class "Fix"" +string(107) "deepclone_from_array(): malformed payload, const-expr-closure references unknown class "No\Such\ClassAtAll"" diff --git a/tests/deepclone_constexpr_closures_native_fcc.phpt b/tests/deepclone_constexpr_closures_native_fcc.phpt new file mode 100644 index 0000000..168db98 --- /dev/null +++ b/tests/deepclone_constexpr_closures_native_fcc.phpt @@ -0,0 +1,52 @@ +--TEST-- +deepclone resolves cross-class and global first-class-callable declaring classes from the engine (PHP 8.6) +--EXTENSIONS-- +deepclone +--SKIPIF-- + +--FILE-- +cb = $a[0]; } } + +class Target { public static function check(): bool { return true; } } + +function dc_native_global(): int { return 41; } + +class Decl { + #[CA(Target::check(...))] public int $x = 0; // cross-class first-class callable + #[CA(strlen(...))] public int $g = 0; // global internal function + #[CA(dc_native_global(...))] public int $u = 0; // global user function +} + +// The engine yields the declaring class (no ReflectionAttribute capture, no +// allow_named_closures opt-in), and the closure serializes as the same +// site-based reference rooted at the declaring class -- not the target's scope. +$rp = new ReflectionClass(Decl::class); + +$cross = deepclone_to_array($rp->getProperty('x')->getAttributes()[0]->getArguments()[0]); +var_dump($cross['mask'] === 1, $cross['prepared'][0] === Decl::class, deepclone_from_array($cross)() === true); + +$gi = deepclone_to_array($rp->getProperty('g')->getAttributes()[0]->getArguments()[0]); +var_dump($gi['mask'] === 1, $gi['prepared'][0] === Decl::class, deepclone_from_array($gi)('hello') === 5); + +$gu = deepclone_to_array($rp->getProperty('u')->getAttributes()[0]->getArguments()[0]); +var_dump($gu['mask'] === 1, $gu['prepared'][0] === Decl::class, deepclone_from_array($gu)() === 41); + +echo "Done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +Done diff --git a/tests/deepclone_constexpr_closures_validation.phpt b/tests/deepclone_constexpr_closures_validation.phpt index bbe0694..4a4abee 100644 --- a/tests/deepclone_constexpr_closures_validation.phpt +++ b/tests/deepclone_constexpr_closures_validation.phpt @@ -41,6 +41,11 @@ $cases = [ [Fix::class, 'tagged()', null, 0, $line], [Fix::class, '$tagged::get()', 0, 0, $line], [Fix::class, '$tagged::bad()', 0, 0, $line], + // an int element 1 makes the payload an engine-id reference [class, id, line] + [Fix::class, 0], + [Fix::class, 0, $line, 'x'], + [42, 0, $line], + [Fix::class, 0, 'x'], ]; foreach ($cases as $prepared) { @@ -57,7 +62,7 @@ deepclone_from_array(): malformed payload, const-expr-closure value must be of t deepclone_from_array(): malformed payload, const-expr-closure value must have 5 elements deepclone_from_array(): malformed payload, const-expr-closure class name must be of type string, int given deepclone_from_array(): malformed payload, const-expr-closure references unknown class "No\Such\ClassAtAll" -deepclone_from_array(): malformed payload, const-expr-closure site must be of type string, int given +deepclone_from_array(): malformed payload, const-expr-closure value must have 3 elements deepclone_from_array(): malformed payload, const-expr-closure attribute index must be of type int or null, string given deepclone_from_array(): malformed payload, const-expr-closure closure index must be of type int, string given deepclone_from_array(): malformed payload, const-expr-closure line must be of type int, string given @@ -74,3 +79,7 @@ deepclone_from_array(): malformed payload, const-expr-closure attribute index is deepclone_from_array(): malformed payload, const-expr-closure attribute index is required for site "tagged()" deepclone_from_array(): malformed payload, const-expr-closure references unknown hook "$tagged::get()" deepclone_from_array(): malformed payload, const-expr-closure references unknown hook "$tagged::bad()" +deepclone_from_array(): malformed payload, const-expr-closure value must have 3 elements +deepclone_from_array(): malformed payload, const-expr-closure value must have 3 elements +deepclone_from_array(): malformed payload, const-expr-closure class name must be of type string, int given +deepclone_from_array(): malformed payload, const-expr-closure line must be of type int, string given