Skip to content

Commit db0dcd7

Browse files
MagicalTuxclaude
andcommitted
vm: lower Foo::\$bar OP= rhs to OP_STATIC_PROP_COMPOUND_ASSIGN
Mirrors the OP_OBJECT_COMPOUND_ASSIGN landed in the prior commit. Encoding: A = const-pool name index (ZString varName, w/o leading $) B = tokenizer ItemType (op token) C bit 0 = keep-value-on-stack (expr vs stmt context) Stack: pops rhs, pops class-source. Handler: cur, _ := EvalClassStaticVarRead(ctx, class-source, name, loc) cur := cur.Dup() res := compoundOp(op)(cur, rhs) AssignClassStaticProp(ctx, class-source, name, res) if C&1 { push(res) } Both helpers already exist and carry LSB-aware class resolution, read-side and asymmetric write-side visibility checks, and typed-prop coercion. The native path inherits those unchanged. Verified locally: Counter::\$n += 5 -> 15 Counter::\$n *= 2 -> 26 Counter::\$s .= ' world' -> 'hi world' Expr ctx: \$x = (Counter::\$n += 100) -> both 126 LSB: Parent1::bump() bumps Parent1::\$cnt, Child1::bump() bumps Child1::\$cnt (static::\$cnt picks up the called class) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent bb1672f commit db0dcd7

3 files changed

Lines changed: 71 additions & 0 deletions

File tree

core/vm/opcode.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,17 @@ const (
487487
// Pushes the post-op value back when keep-flag is set.
488488
OpObjectCompoundAssign
489489

490+
// `Foo::$bar OP= rhs` static-property compound assign. Encoding
491+
// mirrors OP_OBJECT_COMPOUND_ASSIGN:
492+
// A = const-pool name index (ZString varName, w/o leading $)
493+
// B = tokenizer ItemType
494+
// C bit 0 = keep-value-on-stack flag
495+
// Stack: pops rhs, pops class-source. Reads via
496+
// EvalClassStaticVarRead, applies compoundOp, writes back via
497+
// AssignClassStaticProp. The two helpers carry the LSB-aware
498+
// class resolution, visibility checks, and typed-prop coercion.
499+
OpStaticPropCompoundAssign
500+
490501
// Sentinel — keep last.
491502
opLast
492503
)
@@ -625,4 +636,5 @@ var opNames = [...]string{
625636
OpEmptyObjProp: "EMPTY_OBJ_PROP",
626637
OpUnsetObjProp: "UNSET_OBJ_PROP",
627638
OpObjectCompoundAssign: "OBJECT_COMPOUND_ASSIGN",
639+
OpStaticPropCompoundAssign: "STATIC_PROP_COMPOUND_ASSIGN",
628640
}

core/vm/vm.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,38 @@ func (f *Frame) runUntilError(ctx phpv.Context) (retVal *phpv.ZVal, finished boo
15961596
f.push(val)
15971597
}
15981598

1599+
// --- `Foo::$bar OP= rhs` static-prop compound assign --------
1600+
case OpStaticPropCompoundAssign:
1601+
rhs := f.pop()
1602+
classV := f.pop()
1603+
varName, ok := f.fn.Consts[ins.A()].(phpv.ZString)
1604+
if !ok {
1605+
return nil, false, fmt.Errorf("vm: OP_STATIC_PROP_COMPOUND_ASSIGN name const is %T not ZString", f.fn.Consts[ins.A()])
1606+
}
1607+
op := tokenizer.ItemType(ins.B())
1608+
fn := compoundOp(op)
1609+
if fn == nil {
1610+
return nil, false, fmt.Errorf("vm: OP_STATIC_PROP_COMPOUND_ASSIGN unknown op %d", ins.B())
1611+
}
1612+
cur, err := compiler.EvalClassStaticVarRead(ctx, classV, varName, f.fn.LocAt(f.pc-1))
1613+
if err != nil {
1614+
return nil, false, err
1615+
}
1616+
if cur == nil {
1617+
cur = phpv.ZNULL.ZVal()
1618+
}
1619+
cur = cur.Dup()
1620+
res, err := fn(ctx, cur, rhs)
1621+
if err != nil {
1622+
return nil, false, err
1623+
}
1624+
if err := compiler.AssignClassStaticProp(ctx, classV, varName, res); err != nil {
1625+
return nil, false, err
1626+
}
1627+
if ins.C()&1 != 0 {
1628+
f.push(res)
1629+
}
1630+
15991631
// --- typed-return non-strict coercion -----------------------
16001632
case OpCoerceReturn:
16011633
ret := f.pop()

core/vm/vmcompiler/emit_expr.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,33 @@ func (e *emitter) emitCompoundAssign(n operatorNode, op tokenizer.ItemType) erro
966966
return e.emitAssignViaAST(n)
967967
}
968968
if compiler.IsStaticPropertyTarget(n.OperatorA()) {
969+
// `Foo::$bar OP= rhs`: emit natively when the LHS is the
970+
// static-name form. The handler dispatches read +
971+
// compoundOp + write via the existing AssignClassStaticProp
972+
// + EvalClassStaticVarRead helpers.
973+
if compiler.IsClassStaticVarReadNode(n.OperatorA()) {
974+
classExpr := n.OperatorA().(interface{ ClassStaticVarReadClassExpr() phpv.Runnable }).ClassStaticVarReadClassExpr()
975+
varName := n.OperatorA().(interface{ ClassStaticVarReadName() phpv.ZString }).ClassStaticVarReadName()
976+
stmtCtx := e.stmtCtx
977+
if err := e.withSubexpr(func() error { return e.emitExpr(classExpr) }); err != nil {
978+
return err
979+
}
980+
if err := e.withSubexpr(func() error { return e.emitExpr(n.OperatorB()) }); err != nil {
981+
return err
982+
}
983+
nameIdx := e.constIndex(varName)
984+
var c int32
985+
if !stmtCtx {
986+
c = 1
987+
}
988+
e.emit(vm.OpStaticPropCompoundAssign, nameIdx, uint16(op), c)
989+
if stmtCtx {
990+
e.popStack(2)
991+
} else {
992+
e.popStack(1)
993+
}
994+
return nil
995+
}
969996
return e.emitAssignViaAST(n)
970997
}
971998
return unsupportedf("compound assign to non-variable target %T", n.OperatorA())

0 commit comments

Comments
 (0)