Skip to content

Commit 6bc8695

Browse files
MagicalTuxclaude
andcommitted
vm: lower $b = &$a ref-assign for simple-local LHS natively
Adds OP_STORE_LOCAL_REF, writing a ref-typed ZVal into a local slot without the COW-Dup that OP_STORE_LOCAL applies to ZtArray values (frame.go:160-166 follows ref→Nude+Dup, which detaches the ref). Emit path: $b = &$a where LHS is a simple variable - emit RHS (\&$a → OP_CREATE_REF produces ref ZVal) - OP_STORE_LOCAL_REF idx=$b localIdx Handler: - pop value - if IsRef, call RefInner() to track alias count - write directly into the slot (no Dup, no Nude) - mirror through ctx.OffsetSet for non-slot-only frames - keep-value flag for expr context ($x = ($b = &$a)) Complex LHS (object prop, array elem, static prop) still routes through AST — those write paths each carry their own ref-installation semantics that this stage-1 lowering doesn't replicate. VM output byte-identical to AST baseline across the 7-test smoke suite (scalar/array/string ref-share, function by-ref pass-through, unset-detach, ref-chain, and the expr-context $x = ($b = &$a) edge). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f2d9b9f commit 6bc8695

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

core/vm/opcode.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,25 @@ const (
615615
// Reads via objectGet, applies DoInc, writes via objectSet.
616616
OpIncDecObjDynProp
617617

618+
// OP_STORE_LOCAL_REF stores a reference-typed ZVal into a local
619+
// slot without dereferencing or COW-Dup'ing. Used for `$b = &$a`
620+
// reference assignments where the RHS is `&$expr` (a runRef
621+
// expression — produces a ref-typed ZVal via OP_CREATE_REF).
622+
//
623+
// Encoding:
624+
// A = local slot index
625+
// B bit 0 = keep value on stack (expression context, e.g.
626+
// `$x = ($b = &$a)` — the result of `$b = &$a` is
627+
// the ref ZVal which then gets dereferenced by the
628+
// outer `=`'s value semantics in OpStoreLocal)
629+
//
630+
// Differs from OP_STORE_LOCAL: that handler Dup's ZtArray values
631+
// even when the source is a ref (frame.go:160-166), which detaches
632+
// the ref. This handler writes the ref ZVal directly and calls
633+
// RefInner() to track the alias count — matches the AST's
634+
// `res.IsRef() && r.b is *runRef` branch in run-operator.go.
635+
OpStoreLocalRef
636+
618637
// Sentinel — keep last.
619638
opLast
620639
)
@@ -765,4 +784,5 @@ var opNames = [...]string{
765784
OpIncDecStaticPropDyn: "INC_DEC_STATIC_PROP_DYN",
766785
OpObjectDynCompoundAssign: "OBJECT_DYN_COMPOUND_ASSIGN",
767786
OpIncDecObjDynProp: "INC_DEC_OBJ_DYN_PROP",
787+
OpStoreLocalRef: "STORE_LOCAL_REF",
768788
}

core/vm/vm.go

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

1901+
// --- `$b = &$a` ref-assign to simple-local LHS ----------------
1902+
case OpStoreLocalRef:
1903+
v := f.pop()
1904+
idx := ins.A()
1905+
keep := ins.B()&1 != 0
1906+
if v == nil {
1907+
v = phpv.ZNULL.ZVal()
1908+
}
1909+
if v.IsRef() {
1910+
v.RefInner()
1911+
}
1912+
// Write the ref ZVal directly — no Dup, no Nude unwrap.
1913+
// This is the path that storeLocal explicitly avoids
1914+
// (frame.go:160-166 dereferences ref arrays via Nude+Dup).
1915+
f.locals[idx] = v
1916+
if !f.fn.SlotOnly {
1917+
if err := ctx.OffsetSet(ctx, f.fn.Locals[idx], v); err != nil {
1918+
return nil, false, err
1919+
}
1920+
}
1921+
if keep {
1922+
f.push(v)
1923+
}
1924+
19011925
// --- `$obj->$x OP= rhs` / `$obj->{$x} OP= rhs` dyn-name prop ---
19021926
case OpObjectDynCompoundAssign:
19031927
rhs := f.pop()

core/vm/vmcompiler/emit_expr.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,8 +879,29 @@ func (e *emitter) emitUnary(b phpv.Runnable, op tokenizer.ItemType) error {
879879
func (e *emitter) emitAssign(n operatorNode) error {
880880
// Reference assignment (`$b = &$a`): AST handles the ref-share
881881
// semantics. The VM's OpStoreLocal Dups arrays, detaching any
882-
// inbound ref — wrong for `=&`.
882+
// inbound ref — wrong for `=&`. For the simple-variable LHS,
883+
// use OP_STORE_LOCAL_REF which writes the ref ZVal directly.
884+
// Complex LHS (object prop, array elem, static prop) still
885+
// AST-delegates — those write paths each have their own ref
886+
// semantics that we don't yet replicate natively.
883887
if compiler.IsRefExpr(n.OperatorB()) {
888+
if v, ok := n.OperatorA().(variableNode); ok {
889+
stmtCtx := e.stmtCtx
890+
if err := e.withSubexpr(func() error { return e.emitExpr(n.OperatorB()) }); err != nil {
891+
return err
892+
}
893+
idx := e.localIndex(v.VariableName())
894+
var b uint16
895+
if !stmtCtx {
896+
b |= 1
897+
}
898+
e.emit(vm.OpStoreLocalRef, idx, b, 0)
899+
if stmtCtx {
900+
e.popStack(1)
901+
}
902+
// expr-context: pop+push of same value → net zero.
903+
return nil
904+
}
884905
return e.emitAssignViaAST(n)
885906
}
886907
// Plain `=` to a simple local variable.

0 commit comments

Comments
 (0)