Skip to content

Commit f2d9b9f

Browse files
MagicalTuxclaude
andcommitted
vm: lower $obj->$x OP=/++ dyn-name property compound/incdec natively
Adds OP_OBJECT_DYN_COMPOUND_ASSIGN and OP_INC_DEC_OBJ_DYN_PROP. Both pop the receiver and name from the stack rather than the const pool, reusing the existing objectGet/objectSet helpers + compoundOp/DoInc. Covers two PHP source shapes: $obj->$x OP= rhs (objectVarNode, name starts with $) $obj->{$x} OP= rhs (objectDynVarReadNode, curly braces) $obj->$x++ / ++$obj->$x $obj->{$x}++ / ++$obj->{$x} Receiver-warning semantics differ between the two shapes (mirrors AST): runObjectVar.WriteValue suppresses the undef-var warning via SetWriteContext; runObjectDynVar does not. The new helpers take a recvSilent flag to match. Smoke test covers all 28 assertions across +=, -=, *=, .=, postfix/ prefix inc/dec in both stmt and expr context, both shapes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1fd66c6 commit f2d9b9f

4 files changed

Lines changed: 238 additions & 0 deletions

File tree

core/vm/opcode.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,36 @@ const (
585585
// dyn-name form.
586586
OpIncDecStaticPropDyn
587587

588+
// OP_OBJECT_DYN_COMPOUND_ASSIGN handles `$obj->$x OP= rhs` and
589+
// `$obj->{$x} OP= rhs` dynamic-name property compound assigns.
590+
//
591+
// Encoding:
592+
// B = compound op kind (token, e.g. T_PLUS_EQUAL)
593+
// C bit 0 = keep value on stack (expression context)
594+
//
595+
// Stack effect (stmt): pops receiver, name, rhs
596+
// Stack effect (expr): pops receiver, name, rhs; pushes result
597+
//
598+
// Reads via objectGet, applies compoundOp, writes via objectSet
599+
// (both internal VM helpers that dispatch through ZObject's typed/
600+
// hook/asymmetric machinery).
601+
OpObjectDynCompoundAssign
602+
603+
// OP_INC_DEC_OBJ_DYN_PROP handles `$obj->$x++` / `++$obj->$x` and
604+
// `$obj->{$x}++` / `++$obj->{$x}` plus the dec variants for
605+
// dynamic-name properties.
606+
//
607+
// Encoding:
608+
// B bit 0 = increment (1) vs decrement (0)
609+
// B bit 1 = postfix (1) vs prefix (0)
610+
// C bit 0 = keep value on stack (expression context)
611+
//
612+
// Stack effect (stmt): pops receiver, name
613+
// Stack effect (expr): pops receiver, name; pushes pre/post value
614+
//
615+
// Reads via objectGet, applies DoInc, writes via objectSet.
616+
OpIncDecObjDynProp
617+
588618
// Sentinel — keep last.
589619
opLast
590620
)
@@ -733,4 +763,6 @@ var opNames = [...]string{
733763
OpVarVarSet: "VAR_VAR_SET",
734764
OpStaticPropDynCompoundAssign: "STATIC_PROP_DYN_COMPOUND_ASSIGN",
735765
OpIncDecStaticPropDyn: "INC_DEC_STATIC_PROP_DYN",
766+
OpObjectDynCompoundAssign: "OBJECT_DYN_COMPOUND_ASSIGN",
767+
OpIncDecObjDynProp: "INC_DEC_OBJ_DYN_PROP",
736768
}

core/vm/vm.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,6 +1898,81 @@ func (f *Frame) runUntilError(ctx phpv.Context) (retVal *phpv.ZVal, finished boo
18981898
f.push(res)
18991899
}
19001900

1901+
// --- `$obj->$x OP= rhs` / `$obj->{$x} OP= rhs` dyn-name prop ---
1902+
case OpObjectDynCompoundAssign:
1903+
rhs := f.pop()
1904+
nameV := f.pop()
1905+
receiver := f.pop()
1906+
name := phpv.ZString(nameV.AsString(ctx))
1907+
op := tokenizer.ItemType(ins.B())
1908+
fn := compoundOp(op)
1909+
if fn == nil {
1910+
return nil, false, fmt.Errorf("vm: OP_OBJECT_DYN_COMPOUND_ASSIGN unknown op %d", ins.B())
1911+
}
1912+
cur, err := objectGet(ctx, receiver, name)
1913+
if err != nil {
1914+
return nil, false, err
1915+
}
1916+
if cur == nil {
1917+
cur = phpv.ZNULL.ZVal()
1918+
}
1919+
cur = cur.Dup()
1920+
res, err := fn(ctx, cur, rhs)
1921+
if err != nil {
1922+
return nil, false, err
1923+
}
1924+
if err := objectSet(ctx, receiver, name, res); err != nil {
1925+
return nil, false, err
1926+
}
1927+
if ins.C()&1 != 0 {
1928+
f.push(res)
1929+
}
1930+
1931+
// --- `$obj->$x++` / `++$obj->{$x}` dyn-name prop inc/dec -----
1932+
case OpIncDecObjDynProp:
1933+
nameV := f.pop()
1934+
receiver := f.pop()
1935+
name := phpv.ZString(nameV.AsString(ctx))
1936+
inc := ins.B()&1 != 0
1937+
post := ins.B()&2 != 0
1938+
keep := ins.C()&1 != 0
1939+
// PHP 8: ++/-- on a property of a non-object throws
1940+
// "Attempt to increment/decrement property" — mirrors the
1941+
// static-name OP_INC_DEC_OBJ_PROP receiver check above.
1942+
if receiver == nil || receiver.Value() == nil {
1943+
return nil, false, phpobj.ThrowError(ctx, phpobj.Error,
1944+
fmt.Sprintf("Attempt to increment/decrement property \"%s\" on null", name))
1945+
}
1946+
if _, isObj := receiver.Value().(phpv.ZObjectAccess); !isObj {
1947+
return nil, false, phpobj.ThrowError(ctx, phpobj.Error,
1948+
fmt.Sprintf("Attempt to increment/decrement property \"%s\" on %s", name, compiler.PhpValueTypeName(receiver)))
1949+
}
1950+
cur, err := objectGet(ctx, receiver, name)
1951+
if err != nil {
1952+
return nil, false, err
1953+
}
1954+
if cur == nil {
1955+
cur = phpv.ZNULL.ZVal()
1956+
}
1957+
cur = cur.Dup()
1958+
var pre *phpv.ZVal
1959+
if post {
1960+
pre = cur.Dup()
1961+
}
1962+
if err := compiler.DoInc(ctx, cur, inc); err != nil {
1963+
return nil, false, err
1964+
}
1965+
if err := objectSet(ctx, receiver, name, cur); err != nil {
1966+
return nil, false, err
1967+
}
1968+
if keep {
1969+
if post {
1970+
f.push(pre)
1971+
} else {
1972+
f.push(cur)
1973+
}
1974+
}
1975+
19011976
// --- runConstant — user / namespaced / built-in constant ----
19021977
case OpLoadConstantByName:
19031978
name, ok := f.fn.Consts[ins.A()].(phpv.ZString)

core/vm/vmcompiler/emit_expr.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,8 +1088,31 @@ func (e *emitter) emitCompoundAssign(n operatorNode, op tokenizer.ItemType) erro
10881088
if !ov.ObjectVarIsNullSafe() && !(len(name) > 0 && name[0] == '$') {
10891089
return e.emitObjectVarCompoundAssign(ov, n.OperatorB(), op, e.stmtCtx)
10901090
}
1091+
// `$obj->$x OP= rhs`: dyn-name compound assign. Name is the
1092+
// local variable referenced after the `$` prefix. Receiver
1093+
// write-context suppresses the undef-var warning — pass
1094+
// recvSilent=true to mirror runObjectVar.WriteValue.
1095+
if !ov.ObjectVarIsNullSafe() && len(name) > 0 && name[0] == '$' {
1096+
localIdx := e.localIndex(name[1:])
1097+
pushName := func() error {
1098+
e.emit(vm.OpLoadLocal, localIdx, 0, 0)
1099+
e.pushStack(1)
1100+
return nil
1101+
}
1102+
return e.emitObjectDynVarCompoundAssign(ov.ObjectVarReceiver(), pushName, n.OperatorB(), op, true, e.stmtCtx)
1103+
}
10911104
return e.emitAssignViaAST(n)
10921105
}
1106+
// `$obj->{$x} OP= rhs` (curly-brace dyn-name). runObjectDynVar
1107+
// does NOT suppress the receiver warning — recvSilent=false.
1108+
if compiler.IsObjectDynVarReadNode(n.OperatorA()) {
1109+
if dv, ok := n.OperatorA().(objectDynVarReadNode); ok {
1110+
nameExpr := dv.ObjectDynVarNameExpr()
1111+
pushName := func() error { return e.emitExpr(nameExpr) }
1112+
return e.emitObjectDynVarCompoundAssign(dv.ObjectDynVarReceiver(), pushName, n.OperatorB(), op, false, e.stmtCtx)
1113+
}
1114+
return unsupportedf("object-dyn-var compound LHS shape: %T", n.OperatorA())
1115+
}
10931116
// Array compound assigns still need a dedicated path.
10941117
if _, ok := n.OperatorA().(arrayAccessNode); ok {
10951118
return e.emitAssignViaAST(n)
@@ -1191,8 +1214,29 @@ func (e *emitter) emitIncDec(n operatorNode, inc bool) error {
11911214
if !ov.ObjectVarIsNullSafe() && !(len(name) > 0 && name[0] == '$') {
11921215
return e.emitObjectVarIncDec(ov, inc, post, e.stmtCtx)
11931216
}
1217+
// `$obj->$x++` dyn-name inc/dec. Mirrors compound-assign
1218+
// dollar-prefix branch: silent receiver, name comes from
1219+
// local var after the `$` prefix.
1220+
if !ov.ObjectVarIsNullSafe() && len(name) > 0 && name[0] == '$' {
1221+
localIdx := e.localIndex(name[1:])
1222+
pushName := func() error {
1223+
e.emit(vm.OpLoadLocal, localIdx, 0, 0)
1224+
e.pushStack(1)
1225+
return nil
1226+
}
1227+
return e.emitObjectDynVarIncDec(ov.ObjectVarReceiver(), pushName, inc, post, true, e.stmtCtx)
1228+
}
11941229
return e.emitAssignViaAST(n)
11951230
}
1231+
// `$obj->{$x}++` curly-brace dyn-name inc/dec.
1232+
if compiler.IsObjectDynVarReadNode(target) {
1233+
if dv, ok := target.(objectDynVarReadNode); ok {
1234+
nameExpr := dv.ObjectDynVarNameExpr()
1235+
pushName := func() error { return e.emitExpr(nameExpr) }
1236+
return e.emitObjectDynVarIncDec(dv.ObjectDynVarReceiver(), pushName, inc, post, false, e.stmtCtx)
1237+
}
1238+
return unsupportedf("object-dyn-var inc/dec shape: %T", target)
1239+
}
11961240
// `Foo::$bar++` / `++Foo::$bar` / `--`: emit natively for the
11971241
// static-name form (*runClassStaticVarRef).
11981242
if compiler.IsStaticPropertyTarget(target) && compiler.IsClassStaticVarReadNode(target) {

core/vm/vmcompiler/emit_object.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,93 @@ func (e *emitter) emitObjectVarIncDec(lhs objectVarNode, inc bool, post bool, st
245245
return nil
246246
}
247247

248+
// emitObjectDynVarCompoundAssign emits `$obj->$x OP= rhs` and
249+
// `$obj->{$x} OP= rhs` for dynamic-name property access. Receiver and
250+
// name are evaluated and pushed; OP_OBJECT_DYN_COMPOUND_ASSIGN reads
251+
// the current value via objectGet, applies the compound op, and writes
252+
// back via objectSet (typed props, hooks, asymmetric visibility).
253+
//
254+
// `recvSilent` mirrors the dollar-prefix shape semantics: an undefined
255+
// simple-variable receiver does not warn (write-context). The curly-
256+
// brace form (`*runObjectDynVar`) does NOT suppress the warning, so it
257+
// uses the default emitExpr path.
258+
func (e *emitter) emitObjectDynVarCompoundAssign(recv phpv.Runnable, pushName func() error, rhs phpv.Runnable, op tokenizer.ItemType, recvSilent bool, stmtCtx bool) error {
259+
if recvSilent {
260+
if v, ok := recv.(variableNode); ok {
261+
idx := e.localIndex(v.VariableName())
262+
e.emit(vm.OpLoadLocal, idx, 0, 0)
263+
e.pushStack(1)
264+
} else {
265+
if err := e.withSubexpr(func() error { return e.emitExpr(recv) }); err != nil {
266+
return err
267+
}
268+
}
269+
} else {
270+
if err := e.withSubexpr(func() error { return e.emitExpr(recv) }); err != nil {
271+
return err
272+
}
273+
}
274+
if err := e.withSubexpr(pushName); err != nil {
275+
return err
276+
}
277+
if err := e.withSubexpr(func() error { return e.emitExpr(rhs) }); err != nil {
278+
return err
279+
}
280+
var c int32
281+
if !stmtCtx {
282+
c = 1
283+
}
284+
e.emit(vm.OpObjectDynCompoundAssign, 0, uint16(op), c)
285+
if stmtCtx {
286+
e.popStack(3) // pop receiver+name+rhs
287+
} else {
288+
e.popStack(2) // pop receiver+name+rhs, push result → net -2
289+
}
290+
return nil
291+
}
292+
293+
// emitObjectDynVarIncDec emits `$obj->$x++` / `++$obj->{$x}` and dec
294+
// variants. Receiver and name are evaluated and pushed; OP_INC_DEC_OBJ_DYN_PROP
295+
// performs read-mutate-write through objectGet/objectSet.
296+
func (e *emitter) emitObjectDynVarIncDec(recv phpv.Runnable, pushName func() error, inc bool, post bool, recvSilent bool, stmtCtx bool) error {
297+
if recvSilent {
298+
if v, ok := recv.(variableNode); ok {
299+
idx := e.localIndex(v.VariableName())
300+
e.emit(vm.OpLoadLocal, idx, 0, 0)
301+
e.pushStack(1)
302+
} else {
303+
if err := e.withSubexpr(func() error { return e.emitExpr(recv) }); err != nil {
304+
return err
305+
}
306+
}
307+
} else {
308+
if err := e.withSubexpr(func() error { return e.emitExpr(recv) }); err != nil {
309+
return err
310+
}
311+
}
312+
if err := e.withSubexpr(pushName); err != nil {
313+
return err
314+
}
315+
var b uint16
316+
if inc {
317+
b |= 1
318+
}
319+
if post {
320+
b |= 2
321+
}
322+
var c int32
323+
if !stmtCtx {
324+
c = 1
325+
}
326+
e.emit(vm.OpIncDecObjDynProp, 0, b, c)
327+
if stmtCtx {
328+
e.popStack(2) // receiver+name consumed, nothing pushed
329+
} else {
330+
e.popStack(1) // receiver+name consumed, pre/post pushed → net -1
331+
}
332+
return nil
333+
}
334+
248335
func (e *emitter) emitObjectVarRead(n objectVarNode) error {
249336
name := n.ObjectVarName()
250337
// `$obj->$x` (no curly braces) parses to runObjectVar with the

0 commit comments

Comments
 (0)