From d9511a54ea5bbb348a755fcba746c06c46a865a0 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 2 Nov 2026 14:45:44 +0800 Subject: [PATCH] fix(gen): keep `new T()` construction in the storage it allocated For a plain struct with no field defaults and no constructor arguments, the `A()` call path returned `zeroinitializer` directly instead of constructing into the storage provided by `new`. Consequences for `p: A* = new A()`: - the malloc block was leaked, - the struct value was materialized into an entry alloca by coercion, so `p` pointed at the frame, - the slife scope-exit free then freed a stack address (heap corruption / 0xC0000374). Reuse the constructor path whenever a `new` storage descriptor is pending, so `_translate_struct_ctor` zero-initializes the heap block and returns its pointer. Also guard the compiler-owned `new ... slife` binding: the target slot must be a pointer variable, otherwise a value-typed `T` target would make the scope-exit free load the struct value and bitcast it to a pointer. Probe: `new heap A()`, `new A()`, `new stack A()` and `new heap hlife A()` on a field-only class all print the expected values and exit 0; the generated IR frees the malloc pointer instead of a stack slot. Self-host 110/110; ListTest, GenericTest3, PtrOpProbe and HashLibTest pass; NewTest, NewLifetimeTest, StructTest, EscapeOkTest and OopInheritTest pass. NewAstTest and GenericOopTest fail the same way on the previous compiler (pre-existing). --- App/lib/core/Gen/AssignGenMixin.vp | 6 ++++++ App/lib/core/Gen/Call/CallGenMixin.vp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 4a3f451..2090b2d 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1711,6 +1711,12 @@ novirtual class AssignGenMixin: # 生命期已是 FRAME(propagate_life 同一谓词),这里补上**出口释放**这一半。 owned = self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, target_name) if owned != 0: + # `new` yields T*: a compiler-owned slife binding must be a pointer + # variable. A value-typed `T` target would make the scope-exit free + # load the struct value and bitcast it to a pointer (invalid IR). + if target_slot.Ty is None or target_slot.Ty.Pointee == 0 or self.Gen.ExprGenBase.is_ptr_type(target_slot.Ty.Pointee) == 0: + self.Gen.TypeGenErr.fatal_error(rhs_node, "`new ...` returns T* and must be stored in a pointer variable (T*); a value-typed T target cannot own the heap object", "TypeError") + return 0 sh: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot) if sh is None: return 0 self._SlifeDropOwned(pool, builder, mod, trans, target_slot) diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index d261727..f7a2827 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -601,6 +601,12 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): if gen_has_defaults == 1: # Has defaults: use the constructor path to apply them. return self.Gen.CallGenCtor._translate_struct_ctor(pool, builder, mod, struct_ty, func_name, cargs, sizet(0), trans, cl.IsVolatile) + # `new ... T()` already allocated storage: construct there and return its + # pointer. Returning ConstZero would discard that storage and make the + # caller materialize a stack temporary instead (leaking the heap block and + # freeing a frame address at scope exit). + if self.Gen.CallGenCtor.CurCtorStorage() is not None: + return self.Gen.CallGenCtor._translate_struct_ctor(pool, builder, mod, struct_ty, func_name, cargs, sizet(0), trans, cl.IsVolatile) # No defaults: return zeroinitializer. ret: mlirlite.Value* = mlirlite.ConstZero(pool, struct_ty) return ret