Skip to content

feat(objects): a property default may hold array literals, at any depth - #1069

Merged
nahime0 merged 6 commits into
mainfrom
fix/1052-nested-literal-property-default
Sep 21, 2026
Merged

nahime0 merged 6 commits into
mainfrom
fix/1052-nested-literal-property-default

Conversation

@Guikingone

@Guikingone Guikingone commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator
class C { public array $x = [[1], [2]]; }   // was: compile error

Declaring the class was enough to be refused — no read, no write, no
instantiation — while the same literal was already fine as a local, a parameter
default and a class constant.

The cause, as filed

LiteralArrayElement is the set of things a default can materialize without
evaluating code, and it was flat (Int, Bool, Float, Str, Null). A
container element had nowhere to go and fell through to
unsupported_literal_default, whose message names the property's type —
which is why it read as if the slot were the problem. It never was: a plain
array property failed exactly as ?array and mixed did.

The fix

Make the form recursive: a container element materializes into its own
container, which the enclosing one then owns. The tree is allocated during the
object's initialization and released exactly once with it.

The two enclosing paths transfer that ownership differently, and both had to
be taught it:

  • the indexed path boxes into a Mixed cell — so it needs the owned boxer,
    where the box's retain plus a release of the builder's reference nets to a
    transfer;
  • the hash path hands __rt_hash_set the pointer directly, because that
    helper does not retain what it stores; it only releases what it overwrites.

Getting either backwards fails on opposite sides: retain-without-release leaks
the whole subtree per object, release-without-retain frees a child the object
still points at.

A second gap in the same table, landed by #1053

The issue's ?array $x = ["k" => [1]]; row turned out to be two independent
gaps: a keyed literal had no boxed form at all, only the positional spelling had
been taught to box. That half is LiteralDefaultValue::BoxedAssocArray, which
#1053 landed first (the same variant, arrived at independently); this branch is
merged on top of it and only adds the nesting. The two "second gap" rows below
therefore pass on main already and are listed for completeness.

Verification

The issue's table, plus 13 further spellings, every one byte-identical to host
PHP 8.5.10:

Declaration before after
public array $x = [[1], [2]]; compile error 2 ✓
public ?array $x = [[1], [2]]; compile error ✓
public mixed $x = [[1], [2]]; compile error ✓
public array $x = ["k" => [1]]; compile error 1 ✓
public ?array $x = ["k" => [1]]; compile error ✓
public mixed $x = ["k" => [1]]; compile error ✓
public ?array $x = ["k" => 1]; compile error ✓ (the second gap, via #1053)
public mixed $x = ["k" => 1]; compile error ✓ (the second gap, via #1053)
public array $x = [[1], 2]; compile error ✓ container beside a scalar
public array $x = [[[1]]]; compile error ✓ three levels
public array $x = [[]]; compile error ✓
public array $x = [[1, "s", 2.5, null, true]]; compile error ✓ every leaf kind
public array $x = [0 => [1], 5 => [2], "k" => [3]]; compile error ✓
public static array $s = [[1], [2]]; compile error ✓ separate emitter
public array $x = [1, 2]; / ["k" => 1] ok unchanged ✓

Ownership measured, not assumed. 600 objects carrying three different nested
shapes: allocs=7401 frees=7401, leak summary: clean, peak 2832 bytes flat
across the run. Instances do not share storage ($a->x[0][] = 99 leaves a second
instance at its default), and a copy taken out of a default outlives the object
it came from — the over-release side of the same contract.

Tests: 6 behaviour in objects/nested_array_property_defaults.rs, 4 heap-debug
in runtime_gc/nested_property_defaults.rs. Suites green: objects 260,
runtime_gc 302, oop 619, -p elephc --lib 1680 including
lowers_examples_corpus.

docs/php/classes.md replaces the stated limitation, and examples/classes
gains a Grid showing both spellings and the per-instance storage.

Out of scope

public iterable $x = ["k" => 1]; fails earlier and differently
(prop_set assigning PHP type AssocArray), on main as much as here. Not in
the issue's table; left alone rather than folded in.

Two pre-existing defects on the WRITE side become easier to reach now that a
nested default can be declared, and are deliberately not touched here since
neither involves the default path (both reproduce with the literal assigned in
the constructor instead):

The heap-debug fixtures here cover the defaults' own allocation and release; the
behaviour test that writes through a default ($a->x[0][] = 99) checks
per-instance storage, not the heap.

Fixes #1052.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr

@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. size:m Medium-sized pull request. type:feature Introduces new user-visible behavior or capabilities. labels Sep 17, 2026
@greptile-apps

greptile-apps Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness, ownership, security, or repository-rule issue remains.

Fix All in Claude CodeFindings

  1. P2 Static Boxed Path Untested ▶
  2. P2 Assembly Group Comments Missing ▶
Fix with agent prompt
### Issue 1
src/codegen/block_emit.rs:1189-1202
The new static `BoxedAssocArray` emitter is not exercised by the added tests. The only static fixture uses a positional, non-boxed `public static array` default, so regressions in this separate keyed boxing and ownership path would not be caught. Please add a static keyed default on a nullable, mixed, or union property.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 2
src/codegen/literal_defaults.rs:987-988
The new nested-container instruction group lacks the required `// -- description --` block comment. The same omission occurs in the corresponding x86_64 group and both Mixed-storage groups at lines 1033–1034, 1075–1076, and 1117–1118. The repository’s assembly comment policy requires these block comments before related `emitter.instruction(...)` calls, so this requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR enables property defaults to contain recursively nested indexed and associative array literals while preserving per-instance storage and balanced ownership.

  • Extends literal-default analysis and emission to recursively materialize nested containers.
  • Handles the distinct ownership-transfer rules for indexed Mixed cells and associative hash entries on both supported architectures.
  • Adds behavioral, static-property, copy-lifetime, and heap-debug regression coverage.
  • Updates the classes documentation and example program to demonstrate nested defaults.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Property array-literal default] --> B{Element kind}
    B -->|Scalar or null| C[Materialize payload]
    B -->|Nested indexed literal| D[Allocate and recursively populate array]
    B -->|Nested keyed literal| E[Allocate and recursively populate hash]
    C --> F{Enclosing storage}
    D --> F
    E --> F
    F -->|Indexed Mixed element| G[Box owned value and transfer reference]
    F -->|Associative hash entry| H[Move owned pointer into hash]
    G --> I[Root container owns subtree]
    H --> I
    I --> J[Object or static property owns root]
Loading

Reviews (7) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

Comment thread src/codegen/block_emit.rs
Comment on lines +1168 to +1181
LiteralDefaultValue::BoxedAssocArray {
value_type,
entries,
} => {
emit_assoc_array_literal_default_to_result(ctx, value_type, entries)?;
// The OWNED boxer, for the same reason as the positional arm above.
crate::codegen::emit_box_current_owned_value_as_mixed(
ctx.emitter,
&PhpType::AssocArray {
key: Box::new(PhpType::Mixed),
value: Box::new(value_type.clone()),
},
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Static Boxed Path Untested

The new static BoxedAssocArray emitter is not exercised by the added tests. The only static fixture uses a positional, non-boxed public static array default, so regressions in this separate keyed boxing and ownership path would not be caught. Please add a static keyed default on a nullable, mixed, or union property.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/codegen/block_emit.rs
Line: 1168-1181

Comment:
**Static Boxed Path Untested**

The new static `BoxedAssocArray` emitter is not exercised by the added tests. The only static fixture uses a positional, non-boxed `public static array` default, so regressions in this separate keyed boxing and ownership path would not be caught. Please add a static keyed default on a nullable, mixed, or union property.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex Fix in Cursor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Addressed in 7b4328e: test_static_keyed_defaults_on_boxed_slots in tests/codegen/objects/nested_array_property_defaults.rs covers a static ?array keyed default (flat), a static mixed keyed default with a nested element, and a static ?array keyed default two levels deep, all through the static BoxedAssocArray emitter in block_emit.rs.

    class C { public array $x = [[1], [2]]; }   // was: compile error

Declaring the class was enough to be refused -- no read, no write, no
instantiation -- while the same literal was already accepted as a local, a
parameter default and a class constant.

`LiteralArrayElement` is the set of things a default can materialize without
evaluating code, and it was flat: Int, Bool, Float, Str, Null. A container
element had nowhere to go, so it fell through to the unsupported-default error,
which names the PROPERTY's type and so read as if the slot were at fault. It was
never the slot: a plain `array` property failed exactly as `?array` and `mixed`
did.

Make the form recursive. A container element materializes into its own
container, which the enclosing one then owns, so the tree is allocated with the
object and released exactly once with it. The two enclosing paths transfer that
ownership differently and both had to be taught it: the indexed path boxes into
a Mixed cell with the OWNED boxer, so the box's retain plus the builder's
release nets to a transfer; the hash path hands `__rt_hash_set` the pointer
directly, because it does not retain what it stores -- it only releases what it
overwrites.

Also adds `BoxedAssocArray`, the keyed counterpart of `BoxedArray`. The issue's
`?array $x = ["k" => [1]];` row was two independent gaps: a keyed literal had no
boxed form at ALL, so `public ?array $x = ["k" => 1];` was refused on its own
while `public ?array $x = [1, 2];` beside it compiled.

Verified over the issue's table plus 13 further spellings, every one
byte-identical to host PHP 8.5.10. Ownership measured rather than assumed: 600
objects carrying three different nested shapes closed at allocs=7401 frees=7401,
`leak summary: clean`, with a flat 2832-byte peak. Instances do not share
storage, and a copy taken out of a default outlives the object it came from.

Out of scope, left as it was: `public iterable $x = ["k" => 1];` fails earlier
and differently (`prop_set assigning PHP type AssocArray`), on main as much as
here.

Fixes #1052.

Claude-Session: https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr
@Guikingone
Guikingone force-pushed the fix/1052-nested-literal-property-default branch from 500c2c6 to c0447be Compare September 18, 2026 12:23
@Guikingone

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main; this branch is up to date and mergeable again.

Heads-up on an overlap found while rebasing. This PR and #1069 both add the same
LiteralDefaultValue::BoxedAssocArray { value_type, entries } variant to
src/codegen/literal_defaults.rs, arrived at independently:

They are the same addition, so whichever merges first, the other will conflict on that variant
and on its two match arms (block_emit.rs, objects/property_defaults.rs). The resolution is
to keep one copy of the variant, not both — I will rebase the second one onto the first once
the order is decided.

Guikingone and others added 2 commits September 18, 2026 17:05
Raised in review. The static fixture used a positional default on a plain
`array` slot, which never reaches the keyed boxing emitter: `block_emit`'s
static path is separate from `property_defaults`' instance one, and the
`BoxedAssocArray` arm is separate again from the `BoxedArray` arm beside it. A
regression in the static keyed boxing or its ownership would not have been
caught.

Covers a nullable flat keyed default, a `mixed` slot holding a nested indexed
literal, and a two-level keyed one. The flat spelling is included because it was
refused on its own before this change, independently of any nesting.

Claude-Session: https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr
…al-property-default

# Conflicts:
#	examples/classes/main.php
#	src/codegen/literal_defaults.rs
#	src/codegen/lower_inst/objects/property_defaults.rs

Co-authored-by: Vincenzo Petrucci <nahime0@users.noreply.github.com>
Comment on lines +987 to +988
ctx.emitter.instruction("mov x3, x0"); // pass the nested container pointer as the hash value low word
ctx.emitter.instruction("mov x4, xzr"); // container hash values do not use the high payload word

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Assembly Group Comments Missing

The new nested-container instruction group lacks the required // -- description -- block comment. The same omission occurs in the corresponding x86_64 group and both Mixed-storage groups at lines 1033–1034, 1075–1076, and 1117–1118. The repository’s assembly comment policy requires these block comments before related emitter.instruction(...) calls, so this requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/codegen/literal_defaults.rs
Line: 987-988

Comment:
**Assembly Group Comments Missing**

The new nested-container instruction group lacks the required `// -- description --` block comment. The same omission occurs in the corresponding x86_64 group and both Mixed-storage groups at lines 1033–1034, 1075–1076, and 1117–1118. The repository’s assembly comment policy requires these block comments before related `emitter.instruction(...)` calls, so this requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/illegalstudio/elephc/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex Fix in Cursor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not applied, on purpose. In literal_defaults.rs no arm of these four materializers uses a // -- description -- group line: each arm is two instructions with the mandatory inline comment at column 81, introduced by an explanatory comment on the arm itself. The new Array/AssocArray arms follow exactly that layout. Adding group lines to the four new arms alone would leave the file inconsistent with its own siblings; if the file should switch to group lines, that is a file-wide change for a separate PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're right. The surrounding materializers consistently use an arm-level explanatory comment plus column-81 inline comments; they do not use // -- description -- group headers. The four new arms follow that established file-local convention, so my comment does not apply and no change is needed.

cursoragent and others added 3 commits September 21, 2026 10:27
Co-authored-by: Vincenzo Petrucci <nahime0@users.noreply.github.com>
…al-property-default

# Conflicts:
#	tests/codegen/runtime_gc.rs

Co-authored-by: Vincenzo Petrucci <nahime0@users.noreply.github.com>
…al-property-default

# Conflicts:
#	tests/codegen/runtime_gc.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen Touches target-aware assembly or backend lowering. size:m Medium-sized pull request. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A property default whose elements are array literals is refused

3 participants