Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions nautilus/include/nautilus/val_std.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
* | Parameterised construction | Yes | `val<T>(val<A>, val<B>, …)` — args unwrapped via `unwrap_val_t` |
* | Copy construction | Yes | `memcpy` for trivially-copyable; otherwise `invoke`|
* | Copy assignment | Yes | same as copy construction |
* | Move construction/assignment | No | Not provided; use copy |
* | Move construction | Yes | Transfers the underlying alloca; no new allocation |
* | Move assignment | Yes | Destroys the LHS storage, then transfers from RHS |
* | Destruction | Yes | Non-trivial dtor forwarded through `invoke()` |
* | Field read (`get`) | Yes | Returns `val<F&>` for arithmetic/pointer fields |
* | Field write (`set`) | Yes | Accepts `val<F>` or plain `F` |
Expand Down Expand Up @@ -111,8 +112,10 @@
#include "nautilus/val_base.hpp"
#include "nautilus/val_concepts.hpp"
#include "nautilus/val_memcpy.hpp"
#include <concepts>
#include <cstring>
#include <type_traits>
#include <utility>

namespace nautilus {

Expand Down Expand Up @@ -174,6 +177,11 @@ class val<ValueType> : public val_base {
// alloca itself appear as first-class operations in the IR.
val<ValueType*> value_ptr;

// True after a move transferred ownership of value_ptr to another object.
// Suppresses the destructor's traced destruct() call and the heap free so
// that the resource is released exactly once.
bool moved_ = false;

static void construct(ValueType* ptr) {
new (ptr) ValueType();
}
Expand All @@ -198,6 +206,25 @@ class val<ValueType> : public val_base {
new (ptr) ValueType(args...);
}

// Releases the resources owned by value_ptr (traced destruct + heap free in
// interpreter mode) unless this object has been moved-from. Used by both
// the destructor and move assignment.
void release_storage() {
if (moved_) {
return;
}
if constexpr (!std::is_trivially_destructible_v<ValueType>) {
invoke(destruct, value_ptr);
}
#ifdef ENABLE_TRACING
if (!tracing::inTracer()) {
::operator delete(value_ptr.value);
}
#else
::operator delete(value_ptr.value);
#endif
}

public:
// Default-constructs the object on the traced stack.
// For trivially-default-constructible types the ctor call is elided.
Expand All @@ -217,13 +244,30 @@ class val<ValueType> : public val_base {
}
}

// Move-constructs from another val<ValueType>.
// Transfers the underlying alloca/heap pointer; no new allocation, no copy.
// In tracing mode this constructs the new value_ptr directly from the source's
// SSA ref instead of going through val<T*>'s copy ctor, which would emit an
// extra traceCopy/ASSIGN op into the IR for what is logically just an alias.
// The source is left in a moved-from state and its destructor becomes a no-op.
#ifdef ENABLE_TRACING
val(val<ValueType>&& other) noexcept : value_ptr(other.value_ptr.value, other.value_ptr.getState()) {
other.moved_ = true;
}
#else
val(val<ValueType>&& other) noexcept : value_ptr(other.value_ptr.value) {
other.moved_ = true;
}
#endif

// Constructs the object from one or more traced (val<T>) or plain arguments.
// Each argument's raw type is deduced via unwrap_val_t, which produces the concrete
// construct_with<RawArgs...> instantiation passed to invoke().
// The non-template copy constructor above always wins for same-type copies, so
// this template never conflicts with it.
// The same-type guard ensures the dedicated copy/move constructors above are
// always selected for val<ValueType> arguments instead of this template.
template <typename... ValArgs>
requires(sizeof...(ValArgs) > 0)
requires(sizeof...(ValArgs) > 0 &&
!(sizeof...(ValArgs) == 1 && (std::same_as<std::remove_cvref_t<ValArgs>, val<ValueType>> || ...)))
val(ValArgs&&... args) : value_ptr(details::nautilus_alloca<ValueType>()) {
invoke(construct_with<details::unwrap_val_t<ValArgs>...>, value_ptr, std::forward<ValArgs>(args)...);
}
Expand All @@ -239,6 +283,19 @@ class val<ValueType> : public val_base {
return *this;
}

// Move-assigns from another val<ValueType>.
// Releases this object's storage, then transfers ownership of other's storage.
val<ValueType>& operator=(val<ValueType>&& other) noexcept {
if (std::addressof(other) == this) {
return *this;
}
release_storage();
value_ptr = other.value_ptr;
moved_ = false;
other.moved_ = true;
return *this;
}

/**
* Read a direct data member.
*
Expand Down Expand Up @@ -303,18 +360,10 @@ class val<ValueType> : public val_base {
}

// Destroys the object. For trivially-destructible types the dtor call is elided.
// A moved-from val<T> skips both the traced destruct and the heap free so the
// resource is released exactly once.
~val() {
if constexpr (!std::is_trivially_destructible_v<ValueType>) {
invoke(destruct, value_ptr);
}
// in interpreter mode the value is allocated on heap, so we remove the allocation here
#ifdef ENABLE_TRACING
if (!tracing::inTracer()) {
::operator delete(value_ptr.value);
}
#else
::operator delete(value_ptr.value);
#endif
release_storage();
}
};
} // namespace nautilus
124 changes: 124 additions & 0 deletions nautilus/test/common/ValueTypeFunctions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <cstdint>
#include <nautilus/Engine.hpp>
#include <nautilus/nautilus_function.hpp>
#include <nautilus/static.hpp>
#include <nautilus/val.hpp>
#include <nautilus/val_memcpy.hpp>
#include <nautilus/val_ptr.hpp>
Expand Down Expand Up @@ -45,6 +46,129 @@ struct TestWithDtor {
}
};

// Struct that counts constructions and destructions globally so tests can
// assert that move semantics do not leak or double-destruct objects.
struct CountedDtor {
static inline int ctor_count = 0;
static inline int dtor_count = 0;
int32_t v;
CountedDtor() : v(0) {
++ctor_count;
}
CountedDtor(const CountedDtor& other) : v(other.v) {
++ctor_count;
}
CountedDtor& operator=(const CountedDtor& other) {
v = other.v;
return *this;
}
~CountedDtor() {
++dtor_count;
}
};

// Move-construct from a trivially-copyable struct. The source's data must
// be observable through the moved-to object.
val<int32_t> moveConstructTrivial() {
val<Test> original;
original.set(&Test::a, 42);
original.set(&Test::b, 10);
val<Test> moved(std::move(original));
return moved.get(&Test::a) + moved.get(&Test::b);
}

// Move-construct from a non-trivially-destructible struct.
val<int32_t> moveConstructNonTrivial() {
val<TestWithDtor> original;
original.set(&TestWithDtor::value, 99);
val<TestWithDtor> moved(std::move(original));
return moved.get(&TestWithDtor::value);
}

// Move-assign overwrites the LHS storage with the RHS storage.
val<int32_t> moveAssign() {
val<Test> a;
a.set(&Test::a, 1);
val<Test> b;
b.set(&Test::a, 99);
b = std::move(a);
return b.get(&Test::a);
}

// Self-move-assignment must not destroy or corrupt the value. The ref alias
// hides the self-move from -Wself-move; the operator= must still detect it
// at runtime and short-circuit.
val<int32_t> moveAssignSelf() {
val<Test> a;
a.set(&Test::a, 5);
val<Test>& ref = a;
ref = std::move(a);
return a.get(&Test::a);
}

// Move + destruct of a CountedDtor object.
// At runtime ctor_count and dtor_count must end balanced and >= 1.
val<int32_t> moveDtorBalance() {
val<CountedDtor> a;
a.set(&CountedDtor::v, 7);
val<CountedDtor> b(std::move(a));
return b.get(&CountedDtor::v);
}

// Returning a named local: NRVO is allowed, the move ctor is the fallback.
val<Test> makeTest(val<int32_t> x) {
val<Test> t;
t.set(&Test::a, x);
t.set(&Test::b, 0);
return t;
}

val<int32_t> returnByValue(val<int32_t> x) {
val<Test> r = makeTest(x);
return r.get(&Test::a);
}

// Conditional return of two named locals: NRVO cannot apply, so the implicit
// move on return is what avoids a deep copy.
val<Test> makeTestCond(val<int32_t> x) {
val<Test> a;
a.set(&Test::a, 1);
val<Test> b;
b.set(&Test::a, 2);
if (x > 0) {
return a;
}
return b;
}

val<int32_t> returnByValueCond(val<int32_t> x) {
val<Test> r = makeTestCond(x);
return r.get(&Test::a);
}

// Static (fully-unrolled) loop body that constructs a fresh val<Test> each
// iteration and copy-assigns it into a val<Test> declared outside the loop.
// With N=3 unrolled iterations the IR has 4 allocas: 1 for `outer` plus
// 1 per unrolled iteration for `inner`. The copy-assignment writes through
// `outer.value_ptr` rather than reallocating, so `outer` keeps its single slot.
val<int32_t> staticLoopAssignStructToOuter() {
val<Test> outer;
outer.set(&Test::a, 0);
outer.set(&Test::b, 0);
for (static_val<int> i = 0; i < 3; i = i + 1) {
val<Test> inner;
inner.set(&Test::a, i + 1);
inner.set(&Test::b, (i + 1) * 10);
outer = inner;
}
return outer.get(&Test::a) + outer.get(&Test::b);
}

static_assert(std::is_move_constructible_v<val<Test>>);
static_assert(std::is_move_assignable_v<val<Test>>);
static_assert(std::is_nothrow_move_constructible_v<val<Test>>);
static_assert(std::is_nothrow_move_assignable_v<val<Test>>);

// Structs with mixed-alignment members to exercise field_offset padding.
// Layout: [i8][3 pad][i32][i64] = 16 bytes, alignof = 8
struct MixedAlign {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
nautilus {
execute() :i32 {
Block_0():
$1 = alloca 8b :ptr
$3 = 0 :ui64
$5 = 1 :ui64
$6 = 0 :ui64
$7 = $1 + $6 :ptr
$10 = 42 :i32
store($10, $7) :void
$15 = 4 :ui64
$17 = 1 :ui64
$18 = 4 :ui64
$19 = $1 + $18 :ptr
$22 = 10 :i32
store($22, $19) :void
$27 = 0 :ui64
$29 = 1 :ui64
$30 = 0 :ui64
$31 = $1 + $30 :ptr
$35 = 4 :ui64
$37 = 1 :ui64
$38 = 4 :ui64
$39 = $1 + $38 :ptr
$42 = load($31) :i32
$43 = load($39) :i32
$44 = $42 + $43 :i32
return ($44) :i32
}
} //nautilus
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
nautilus {
execute($1:i32) :i32 {
Block_0($1:i32):
$3 = alloca 8b :ptr
$7 = 0 :ui64
$9 = 1 :ui64
$10 = 0 :ui64
$11 = $3 + $10 :ptr
store($1, $11) :void
$18 = 4 :ui64
$20 = 1 :ui64
$21 = 4 :ui64
$22 = $3 + $21 :ptr
$25 = 0 :i32
store($25, $22) :void
$30 = 0 :ui64
$32 = 1 :ui64
$33 = 0 :ui64
$34 = $3 + $33 :ptr
$37 = load($34) :i32
return ($37) :i32
}
} //nautilus
Loading
Loading