From b6a033aeb973a284bd093f60cf86767c9759441f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 07:01:03 +0000 Subject: [PATCH 1/3] Add move semantics to val for class types Previously val for class types had only a copy constructor, which forced an alloca + memcpy/copy_construct on every rvalue. NRVO failures (e.g. multi-return functions) and explicit std::move(x) silently fell back to deep copies. Add a real move constructor and move assignment that transfer the underlying alloca/heap pointer and a moved_ flag that suppresses the source's traced destruct() call and heap free, so the resource is released exactly once. Constrain the variadic-args constructor to exclude same-type so the dedicated copy/move ctors always win in overload resolution. Tests cover move construction (trivial and non-trivial), move assignment, self-move, ctor/dtor balance via a CountedDtor struct, and return-by-value with both NRVO-friendly and conditional-return paths. Static asserts verify nothrow move semantics. --- nautilus/include/nautilus/val_std.hpp | 70 +++++++++--- nautilus/test/common/ValueTypeFunctions.hpp | 105 ++++++++++++++++++ .../test/execution-tests/ExecutionTest.cpp | 36 ++++++ 3 files changed, 196 insertions(+), 15 deletions(-) diff --git a/nautilus/include/nautilus/val_std.hpp b/nautilus/include/nautilus/val_std.hpp index 767037a0d..fc6957e22 100644 --- a/nautilus/include/nautilus/val_std.hpp +++ b/nautilus/include/nautilus/val_std.hpp @@ -28,7 +28,8 @@ * | Parameterised construction | Yes | `val(val, val, …)` — 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` for arithmetic/pointer fields | * | Field write (`set`) | Yes | Accepts `val` or plain `F` | @@ -111,8 +112,10 @@ #include "nautilus/val_base.hpp" #include "nautilus/val_concepts.hpp" #include "nautilus/val_memcpy.hpp" +#include #include #include +#include namespace nautilus { @@ -174,6 +177,11 @@ class val : public val_base { // alloca itself appear as first-class operations in the IR. val 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(); } @@ -198,6 +206,25 @@ class val : 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) { + 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. @@ -217,13 +244,21 @@ class val : public val_base { } } + // Move-constructs from another val. + // Transfers the underlying alloca/heap pointer; no new allocation, no copy. + // The source is left in a moved-from state and its destructor becomes a no-op. + val(val&& other) noexcept : value_ptr(other.value_ptr) { + other.moved_ = true; + } + // Constructs the object from one or more traced (val) or plain arguments. // Each argument's raw type is deduced via unwrap_val_t, which produces the concrete // construct_with 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 arguments instead of this template. template - requires(sizeof...(ValArgs) > 0) + requires(sizeof...(ValArgs) > 0 && + !(sizeof...(ValArgs) == 1 && (std::same_as, val> || ...))) val(ValArgs&&... args) : value_ptr(details::nautilus_alloca()) { invoke(construct_with...>, value_ptr, std::forward(args)...); } @@ -239,6 +274,19 @@ class val : public val_base { return *this; } + // Move-assigns from another val. + // Releases this object's storage, then transfers ownership of other's storage. + val& operator=(val&& 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. * @@ -303,18 +351,10 @@ class val : public val_base { } // Destroys the object. For trivially-destructible types the dtor call is elided. + // A moved-from val skips both the traced destruct and the heap free so the + // resource is released exactly once. ~val() { - if constexpr (!std::is_trivially_destructible_v) { - 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 diff --git a/nautilus/test/common/ValueTypeFunctions.hpp b/nautilus/test/common/ValueTypeFunctions.hpp index a13145faa..329530f02 100644 --- a/nautilus/test/common/ValueTypeFunctions.hpp +++ b/nautilus/test/common/ValueTypeFunctions.hpp @@ -45,6 +45,111 @@ 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 moveConstructTrivial() { + val original; + original.set(&Test::a, 42); + original.set(&Test::b, 10); + val moved(std::move(original)); + return moved.get(&Test::a) + moved.get(&Test::b); +} + +// Move-construct from a non-trivially-destructible struct. +val moveConstructNonTrivial() { + val original; + original.set(&TestWithDtor::value, 99); + val moved(std::move(original)); + return moved.get(&TestWithDtor::value); +} + +// Move-assign overwrites the LHS storage with the RHS storage. +val moveAssign() { + val a; + a.set(&Test::a, 1); + val 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 moveAssignSelf() { + val a; + a.set(&Test::a, 5); + val& 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 moveDtorBalance() { + val a; + a.set(&CountedDtor::v, 7); + val b(std::move(a)); + return b.get(&CountedDtor::v); +} + +// Returning a named local: NRVO is allowed, the move ctor is the fallback. +val makeTest(val x) { + val t; + t.set(&Test::a, x); + t.set(&Test::b, 0); + return t; +} + +val returnByValue(val x) { + val 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 makeTestCond(val x) { + val a; + a.set(&Test::a, 1); + val b; + b.set(&Test::a, 2); + if (x > 0) { + return a; + } + return b; +} + +val returnByValueCond(val x) { + val r = makeTestCond(x); + return r.get(&Test::a); +} + +static_assert(std::is_move_constructible_v>); +static_assert(std::is_move_assignable_v>); +static_assert(std::is_nothrow_move_constructible_v>); +static_assert(std::is_nothrow_move_assignable_v>); + // Structs with mixed-alignment members to exercise field_offset padding. // Layout: [i8][3 pad][i32][i64] = 16 bytes, alignof = 8 struct MixedAlign { diff --git a/nautilus/test/execution-tests/ExecutionTest.cpp b/nautilus/test/execution-tests/ExecutionTest.cpp index c603fc50a..f894561db 100644 --- a/nautilus/test/execution-tests/ExecutionTest.cpp +++ b/nautilus/test/execution-tests/ExecutionTest.cpp @@ -975,6 +975,42 @@ void valueExecutionTest(engine::NautilusEngine& engine) { auto f = engine.registerFunction(copyConstructNonTrivial); REQUIRE(f() == 99); } + SECTION("moveConstructTrivial") { + auto f = engine.registerFunction(moveConstructTrivial); + REQUIRE(f() == 52); + } + SECTION("moveConstructNonTrivial") { + auto f = engine.registerFunction(moveConstructNonTrivial); + REQUIRE(f() == 99); + } + SECTION("moveAssign") { + auto f = engine.registerFunction(moveAssign); + REQUIRE(f() == 1); + } + SECTION("moveAssignSelf") { + auto f = engine.registerFunction(moveAssignSelf); + REQUIRE(f() == 5); + } + SECTION("moveDtorBalance") { + CountedDtor::ctor_count = 0; + CountedDtor::dtor_count = 0; + auto f = engine.registerFunction(moveDtorBalance); + REQUIRE(f() == 7); + REQUIRE(CountedDtor::ctor_count >= 1); + REQUIRE(CountedDtor::ctor_count == CountedDtor::dtor_count); + } + SECTION("returnByValue") { + auto f = engine.registerFunction(returnByValue); + REQUIRE(f((int32_t) 0) == 0); + REQUIRE(f((int32_t) 42) == 42); + REQUIRE(f((int32_t) -7) == -7); + } + SECTION("returnByValueCond") { + auto f = engine.registerFunction(returnByValueCond); + REQUIRE(f((int32_t) 1) == 1); + REQUIRE(f((int32_t) 0) == 2); + REQUIRE(f((int32_t) -3) == 2); + } SECTION("nonTrivialDestructor") { auto f = engine.registerFunction(nonTrivialDestructor); REQUIRE(f() == 42); From f85453b1883b2fe74e34350807fbaa7b8d99778e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 08:06:06 +0000 Subject: [PATCH 2/3] Avoid traceCopy IR op when move-constructing val for class types The move ctor previously initialized value_ptr via val's copy constructor, which in tracing mode emits an extra traceCopy/ASSIGN SSA op for what is logically just a pointer alias. Construct value_ptr directly from the source's (raw pointer, TypedValueRef) so no IR op is emitted for the move itself; only the source's refcount entry remains in place via TypedValueRefHolder, which is purely host-side bookkeeping. The new traces moveConstructTrivial and returnByValue lock in the optimal IR (one ALLOCA, no traceCopy) so any future regression that re-introduces the copy in the move path will fail the snapshot test. --- nautilus/include/nautilus/val_std.hpp | 11 ++++- .../moveConstructTrivial.nautilus | 30 ++++++++++++ .../returnByValue.nautilus | 23 +++++++++ .../moveConstructTrivial.nautilus | 30 ++++++++++++ .../returnByValue.nautilus | 23 +++++++++ .../after_ssa/moveConstructTrivial.trace | 28 +++++++++++ .../after_ssa/returnByValue.trace | 21 ++++++++ .../ir/moveConstructTrivial.nautilus | 30 ++++++++++++ .../ir/returnByValue.nautilus | 23 +++++++++ .../tracing/moveConstructTrivial.trace | 48 +++++++++++++++++++ .../tracing/returnByValue.trace | 40 ++++++++++++++++ nautilus/test/execution-tests/TracingTest.cpp | 3 ++ 12 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 nautilus/test/data/value-tracing-tests/after_constant_folding/moveConstructTrivial.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/after_constant_folding/returnByValue.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/after_empty_block_elim/moveConstructTrivial.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/after_empty_block_elim/returnByValue.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/after_ssa/moveConstructTrivial.trace create mode 100644 nautilus/test/data/value-tracing-tests/after_ssa/returnByValue.trace create mode 100644 nautilus/test/data/value-tracing-tests/ir/moveConstructTrivial.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/ir/returnByValue.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/tracing/moveConstructTrivial.trace create mode 100644 nautilus/test/data/value-tracing-tests/tracing/returnByValue.trace diff --git a/nautilus/include/nautilus/val_std.hpp b/nautilus/include/nautilus/val_std.hpp index fc6957e22..5bb4596e3 100644 --- a/nautilus/include/nautilus/val_std.hpp +++ b/nautilus/include/nautilus/val_std.hpp @@ -246,10 +246,19 @@ class val : public val_base { // Move-constructs from another val. // 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'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. - val(val&& other) noexcept : value_ptr(other.value_ptr) { +#ifdef ENABLE_TRACING + val(val&& other) noexcept : value_ptr(other.value_ptr.value, other.value_ptr.getState()) { other.moved_ = true; } +#else + val(val&& other) noexcept : value_ptr(other.value_ptr.value) { + other.moved_ = true; + } +#endif // Constructs the object from one or more traced (val) or plain arguments. // Each argument's raw type is deduced via unwrap_val_t, which produces the concrete diff --git a/nautilus/test/data/value-tracing-tests/after_constant_folding/moveConstructTrivial.nautilus b/nautilus/test/data/value-tracing-tests/after_constant_folding/moveConstructTrivial.nautilus new file mode 100644 index 000000000..6a960586d --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_constant_folding/moveConstructTrivial.nautilus @@ -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 diff --git a/nautilus/test/data/value-tracing-tests/after_constant_folding/returnByValue.nautilus b/nautilus/test/data/value-tracing-tests/after_constant_folding/returnByValue.nautilus new file mode 100644 index 000000000..65d4cf963 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_constant_folding/returnByValue.nautilus @@ -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 diff --git a/nautilus/test/data/value-tracing-tests/after_empty_block_elim/moveConstructTrivial.nautilus b/nautilus/test/data/value-tracing-tests/after_empty_block_elim/moveConstructTrivial.nautilus new file mode 100644 index 000000000..3b6a97c56 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_empty_block_elim/moveConstructTrivial.nautilus @@ -0,0 +1,30 @@ +nautilus { +execute() :i32 { +Block_0(): + $1 = alloca 8b :ptr + $3 = 0 :ui64 + $5 = 1 :ui64 + $6 = $3 * $5 :ui64 + $7 = $1 + $6 :ptr + $10 = 42 :i32 + store($10, $7) :void + $15 = 4 :ui64 + $17 = 1 :ui64 + $18 = $15 * $17 :ui64 + $19 = $1 + $18 :ptr + $22 = 10 :i32 + store($22, $19) :void + $27 = 0 :ui64 + $29 = 1 :ui64 + $30 = $27 * $29 :ui64 + $31 = $1 + $30 :ptr + $35 = 4 :ui64 + $37 = 1 :ui64 + $38 = $35 * $37 :ui64 + $39 = $1 + $38 :ptr + $42 = load($31) :i32 + $43 = load($39) :i32 + $44 = $42 + $43 :i32 + return ($44) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/after_empty_block_elim/returnByValue.nautilus b/nautilus/test/data/value-tracing-tests/after_empty_block_elim/returnByValue.nautilus new file mode 100644 index 000000000..b3222fd69 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_empty_block_elim/returnByValue.nautilus @@ -0,0 +1,23 @@ +nautilus { +execute($1:i32) :i32 { +Block_0($1:i32): + $3 = alloca 8b :ptr + $7 = 0 :ui64 + $9 = 1 :ui64 + $10 = $7 * $9 :ui64 + $11 = $3 + $10 :ptr + store($1, $11) :void + $18 = 4 :ui64 + $20 = 1 :ui64 + $21 = $18 * $20 :ui64 + $22 = $3 + $21 :ptr + $25 = 0 :i32 + store($25, $22) :void + $30 = 0 :ui64 + $32 = 1 :ui64 + $33 = $30 * $32 :ui64 + $34 = $3 + $33 :ptr + $37 = load($34) :i32 + return ($37) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/after_ssa/moveConstructTrivial.trace b/nautilus/test/data/value-tracing-tests/after_ssa/moveConstructTrivial.trace new file mode 100644 index 000000000..046e1531e --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_ssa/moveConstructTrivial.trace @@ -0,0 +1,28 @@ +EXECUTE: +B0() + ALLOCA $1 :ptr + CONST $3 0 :ui64 + CONST $5 1 :ui64 + MUL $6 $3 $5 :ui64 + ADD $7 $1 $6 :ptr + CONST $10 42 :i32 + STORE $13 $7 $10 :void + CONST $15 4 :ui64 + CONST $17 1 :ui64 + MUL $18 $15 $17 :ui64 + ADD $19 $1 $18 :ptr + CONST $22 10 :i32 + STORE $25 $19 $22 :void + CONST $27 0 :ui64 + CONST $29 1 :ui64 + MUL $30 $27 $29 :ui64 + ADD $31 $1 $30 :ptr + CONST $35 4 :ui64 + CONST $37 1 :ui64 + MUL $38 $35 $37 :ui64 + ADD $39 $1 $38 :ptr + LOAD $42 $31 :i32 + LOAD $43 $39 :i32 + ADD $44 $42 $43 :i32 + RETURN $0 $44 :i32 + diff --git a/nautilus/test/data/value-tracing-tests/after_ssa/returnByValue.trace b/nautilus/test/data/value-tracing-tests/after_ssa/returnByValue.trace new file mode 100644 index 000000000..50dbc7ef8 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_ssa/returnByValue.trace @@ -0,0 +1,21 @@ +EXECUTE: +B0($1:i32) + ALLOCA $3 :ptr + CONST $7 0 :ui64 + CONST $9 1 :ui64 + MUL $10 $7 $9 :ui64 + ADD $11 $3 $10 :ptr + STORE $16 $11 $1 :void + CONST $18 4 :ui64 + CONST $20 1 :ui64 + MUL $21 $18 $20 :ui64 + ADD $22 $3 $21 :ptr + CONST $25 0 :i32 + STORE $28 $22 $25 :void + CONST $30 0 :ui64 + CONST $32 1 :ui64 + MUL $33 $30 $32 :ui64 + ADD $34 $3 $33 :ptr + LOAD $37 $34 :i32 + RETURN $0 $37 :i32 + diff --git a/nautilus/test/data/value-tracing-tests/ir/moveConstructTrivial.nautilus b/nautilus/test/data/value-tracing-tests/ir/moveConstructTrivial.nautilus new file mode 100644 index 000000000..3b6a97c56 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/ir/moveConstructTrivial.nautilus @@ -0,0 +1,30 @@ +nautilus { +execute() :i32 { +Block_0(): + $1 = alloca 8b :ptr + $3 = 0 :ui64 + $5 = 1 :ui64 + $6 = $3 * $5 :ui64 + $7 = $1 + $6 :ptr + $10 = 42 :i32 + store($10, $7) :void + $15 = 4 :ui64 + $17 = 1 :ui64 + $18 = $15 * $17 :ui64 + $19 = $1 + $18 :ptr + $22 = 10 :i32 + store($22, $19) :void + $27 = 0 :ui64 + $29 = 1 :ui64 + $30 = $27 * $29 :ui64 + $31 = $1 + $30 :ptr + $35 = 4 :ui64 + $37 = 1 :ui64 + $38 = $35 * $37 :ui64 + $39 = $1 + $38 :ptr + $42 = load($31) :i32 + $43 = load($39) :i32 + $44 = $42 + $43 :i32 + return ($44) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/ir/returnByValue.nautilus b/nautilus/test/data/value-tracing-tests/ir/returnByValue.nautilus new file mode 100644 index 000000000..b3222fd69 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/ir/returnByValue.nautilus @@ -0,0 +1,23 @@ +nautilus { +execute($1:i32) :i32 { +Block_0($1:i32): + $3 = alloca 8b :ptr + $7 = 0 :ui64 + $9 = 1 :ui64 + $10 = $7 * $9 :ui64 + $11 = $3 + $10 :ptr + store($1, $11) :void + $18 = 4 :ui64 + $20 = 1 :ui64 + $21 = $18 * $20 :ui64 + $22 = $3 + $21 :ptr + $25 = 0 :i32 + store($25, $22) :void + $30 = 0 :ui64 + $32 = 1 :ui64 + $33 = $30 * $32 :ui64 + $34 = $3 + $33 :ptr + $37 = load($34) :i32 + return ($37) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/tracing/moveConstructTrivial.trace b/nautilus/test/data/value-tracing-tests/tracing/moveConstructTrivial.trace new file mode 100644 index 000000000..ce7328643 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/tracing/moveConstructTrivial.trace @@ -0,0 +1,48 @@ +EXECUTE: +B0() + ALLOCA $1 :ptr + ASSIGN $2 $1 :ptr + CONST $3 0 :ui64 + ASSIGN $4 $3 :ui64 + CONST $5 1 :ui64 + MUL $6 $4 $5 :ui64 + ADD $7 $2 $6 :ptr + ASSIGN $8 $7 :ptr + ASSIGN $9 $8 :ptr + CONST $10 42 :i32 + ASSIGN $11 $10 :i32 + ASSIGN $12 $11 :i32 + STORE $13 $9 $12 :void + ASSIGN $14 $1 :ptr + CONST $15 4 :ui64 + ASSIGN $16 $15 :ui64 + CONST $17 1 :ui64 + MUL $18 $16 $17 :ui64 + ADD $19 $14 $18 :ptr + ASSIGN $20 $19 :ptr + ASSIGN $21 $20 :ptr + CONST $22 10 :i32 + ASSIGN $23 $22 :i32 + ASSIGN $24 $23 :i32 + STORE $25 $21 $24 :void + ASSIGN $26 $1 :ptr + CONST $27 0 :ui64 + ASSIGN $28 $27 :ui64 + CONST $29 1 :ui64 + MUL $30 $28 $29 :ui64 + ADD $31 $26 $30 :ptr + ASSIGN $32 $31 :ptr + ASSIGN $33 $32 :ptr + ASSIGN $34 $1 :ptr + CONST $35 4 :ui64 + ASSIGN $36 $35 :ui64 + CONST $37 1 :ui64 + MUL $38 $36 $37 :ui64 + ADD $39 $34 $38 :ptr + ASSIGN $40 $39 :ptr + ASSIGN $41 $40 :ptr + LOAD $42 $33 :i32 + LOAD $43 $41 :i32 + ADD $44 $42 $43 :i32 + RETURN $0 $44 :i32 + diff --git a/nautilus/test/data/value-tracing-tests/tracing/returnByValue.trace b/nautilus/test/data/value-tracing-tests/tracing/returnByValue.trace new file mode 100644 index 000000000..38be1cc34 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/tracing/returnByValue.trace @@ -0,0 +1,40 @@ +EXECUTE: +B0($1:i32) + ASSIGN $2 $1 :i32 + ALLOCA $3 :ptr + ASSIGN $4 $2 :i32 + ASSIGN $5 $4 :i32 + ASSIGN $6 $3 :ptr + CONST $7 0 :ui64 + ASSIGN $8 $7 :ui64 + CONST $9 1 :ui64 + MUL $10 $8 $9 :ui64 + ADD $11 $6 $10 :ptr + ASSIGN $12 $11 :ptr + ASSIGN $13 $12 :ptr + ASSIGN $14 $5 :i32 + ASSIGN $15 $14 :i32 + STORE $16 $13 $15 :void + ASSIGN $17 $3 :ptr + CONST $18 4 :ui64 + ASSIGN $19 $18 :ui64 + CONST $20 1 :ui64 + MUL $21 $19 $20 :ui64 + ADD $22 $17 $21 :ptr + ASSIGN $23 $22 :ptr + ASSIGN $24 $23 :ptr + CONST $25 0 :i32 + ASSIGN $26 $25 :i32 + ASSIGN $27 $26 :i32 + STORE $28 $24 $27 :void + ASSIGN $29 $3 :ptr + CONST $30 0 :ui64 + ASSIGN $31 $30 :ui64 + CONST $32 1 :ui64 + MUL $33 $31 $32 :ui64 + ADD $34 $29 $33 :ptr + ASSIGN $35 $34 :ptr + ASSIGN $36 $35 :ptr + LOAD $37 $36 :i32 + RETURN $0 $37 :i32 + diff --git a/nautilus/test/execution-tests/TracingTest.cpp b/nautilus/test/execution-tests/TracingTest.cpp index 13708f165..758bd49d9 100644 --- a/nautilus/test/execution-tests/TracingTest.cpp +++ b/nautilus/test/execution-tests/TracingTest.cpp @@ -343,6 +343,9 @@ TEST_CASE("Value Trace Test") { {"copyConstruct", details::createFunctionWrapper(copyConstruct)}, {"copyAssign", details::createFunctionWrapper(copyAssign)}, {"copyConstructNonTrivial", details::createFunctionWrapper(copyConstructNonTrivial)}, + // move constructor (no traceCopy/ASSIGN op should appear for the move itself) + {"moveConstructTrivial", details::createFunctionWrapper(moveConstructTrivial)}, + {"returnByValue", details::createFunctionWrapper(returnByValue)}, // destructor {"nonTrivialDestructor", details::createFunctionWrapper(nonTrivialDestructor)}, // loops From c348f19bb711107f66ec1416362685e0c4bad27d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 12:32:57 +0000 Subject: [PATCH 3/3] Add static-loop test that assigns a per-iteration struct into an outer val Verifies the alloca count for a fully-unrolled static loop where each iteration constructs a fresh val and copy-assigns it into a val declared outside the loop. With three unrolled iterations the IR has four allocas (one for outer plus one per iteration for inner) and three memcpy calls for the copy-assignments; outer keeps its single slot because copy-assignment writes through the existing pointer rather than reallocating. --- nautilus/test/common/ValueTypeFunctions.hpp | 19 +++ .../staticLoopAssignStructToOuter.nautilus | 75 ++++++++++ .../staticLoopAssignStructToOuter.nautilus | 75 ++++++++++ .../staticLoopAssignStructToOuter.trace | 73 ++++++++++ .../ir/staticLoopAssignStructToOuter.nautilus | 75 ++++++++++ .../staticLoopAssignStructToOuter.trace | 129 ++++++++++++++++++ .../test/execution-tests/ExecutionTest.cpp | 5 + nautilus/test/execution-tests/TracingTest.cpp | 2 + 8 files changed, 453 insertions(+) create mode 100644 nautilus/test/data/value-tracing-tests/after_constant_folding/staticLoopAssignStructToOuter.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/after_empty_block_elim/staticLoopAssignStructToOuter.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/after_ssa/staticLoopAssignStructToOuter.trace create mode 100644 nautilus/test/data/value-tracing-tests/ir/staticLoopAssignStructToOuter.nautilus create mode 100644 nautilus/test/data/value-tracing-tests/tracing/staticLoopAssignStructToOuter.trace diff --git a/nautilus/test/common/ValueTypeFunctions.hpp b/nautilus/test/common/ValueTypeFunctions.hpp index 329530f02..1edd2bdd0 100644 --- a/nautilus/test/common/ValueTypeFunctions.hpp +++ b/nautilus/test/common/ValueTypeFunctions.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -145,6 +146,24 @@ val returnByValueCond(val x) { return r.get(&Test::a); } +// Static (fully-unrolled) loop body that constructs a fresh val each +// iteration and copy-assigns it into a val 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 staticLoopAssignStructToOuter() { + val outer; + outer.set(&Test::a, 0); + outer.set(&Test::b, 0); + for (static_val i = 0; i < 3; i = i + 1) { + val 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>); static_assert(std::is_move_assignable_v>); static_assert(std::is_nothrow_move_constructible_v>); diff --git a/nautilus/test/data/value-tracing-tests/after_constant_folding/staticLoopAssignStructToOuter.nautilus b/nautilus/test/data/value-tracing-tests/after_constant_folding/staticLoopAssignStructToOuter.nautilus new file mode 100644 index 000000000..9b64d5d5b --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_constant_folding/staticLoopAssignStructToOuter.nautilus @@ -0,0 +1,75 @@ +nautilus { +execute() :i32 { +Block_0(): + $1 = alloca 8b :ptr + $26 = alloca 8b :ptr + $53 = alloca 8b :ptr + $80 = alloca 8b :ptr + $3 = 0 :ui64 + $5 = 1 :ui64 + $6 = 0 :ui64 + $7 = $1 + $6 :ptr + $10 = 0 :i32 + store($10, $7) :void + $15 = 4 :ui64 + $17 = 1 :ui64 + $18 = 4 :ui64 + $19 = $1 + $18 :ptr + $22 = 0 :i32 + store($22, $19) :void + $28 = 0 :ui64 + $30 = 1 :ui64 + $31 = 0 :ui64 + $32 = $26 + $31 :ptr + $35 = 1 :i32 + store($35, $32) :void + $40 = 4 :ui64 + $42 = 1 :ui64 + $43 = 4 :ui64 + $44 = $26 + $43 :ptr + $47 = 10 :i32 + store($47, $44) :void + $51 = 8 :ui64 + $52 = func_*($1,$26,$51) :ptr + $55 = 0 :ui64 + $57 = 1 :ui64 + $58 = 0 :ui64 + $59 = $53 + $58 :ptr + $62 = 2 :i32 + store($62, $59) :void + $67 = 4 :ui64 + $69 = 1 :ui64 + $70 = 4 :ui64 + $71 = $53 + $70 :ptr + $74 = 20 :i32 + store($74, $71) :void + $78 = 8 :ui64 + $79 = func_*($1,$53,$78) :ptr + $82 = 0 :ui64 + $84 = 1 :ui64 + $85 = 0 :ui64 + $86 = $80 + $85 :ptr + $89 = 3 :i32 + store($89, $86) :void + $94 = 4 :ui64 + $96 = 1 :ui64 + $97 = 4 :ui64 + $98 = $80 + $97 :ptr + $101 = 30 :i32 + store($101, $98) :void + $105 = 8 :ui64 + $106 = func_*($1,$80,$105) :ptr + $108 = 0 :ui64 + $110 = 1 :ui64 + $111 = 0 :ui64 + $112 = $1 + $111 :ptr + $116 = 4 :ui64 + $118 = 1 :ui64 + $119 = 4 :ui64 + $120 = $1 + $119 :ptr + $123 = load($112) :i32 + $124 = load($120) :i32 + $125 = $123 + $124 :i32 + return ($125) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/after_empty_block_elim/staticLoopAssignStructToOuter.nautilus b/nautilus/test/data/value-tracing-tests/after_empty_block_elim/staticLoopAssignStructToOuter.nautilus new file mode 100644 index 000000000..2a323f6bb --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_empty_block_elim/staticLoopAssignStructToOuter.nautilus @@ -0,0 +1,75 @@ +nautilus { +execute() :i32 { +Block_0(): + $1 = alloca 8b :ptr + $26 = alloca 8b :ptr + $53 = alloca 8b :ptr + $80 = alloca 8b :ptr + $3 = 0 :ui64 + $5 = 1 :ui64 + $6 = $3 * $5 :ui64 + $7 = $1 + $6 :ptr + $10 = 0 :i32 + store($10, $7) :void + $15 = 4 :ui64 + $17 = 1 :ui64 + $18 = $15 * $17 :ui64 + $19 = $1 + $18 :ptr + $22 = 0 :i32 + store($22, $19) :void + $28 = 0 :ui64 + $30 = 1 :ui64 + $31 = $28 * $30 :ui64 + $32 = $26 + $31 :ptr + $35 = 1 :i32 + store($35, $32) :void + $40 = 4 :ui64 + $42 = 1 :ui64 + $43 = $40 * $42 :ui64 + $44 = $26 + $43 :ptr + $47 = 10 :i32 + store($47, $44) :void + $51 = 8 :ui64 + $52 = func_*($1,$26,$51) :ptr + $55 = 0 :ui64 + $57 = 1 :ui64 + $58 = $55 * $57 :ui64 + $59 = $53 + $58 :ptr + $62 = 2 :i32 + store($62, $59) :void + $67 = 4 :ui64 + $69 = 1 :ui64 + $70 = $67 * $69 :ui64 + $71 = $53 + $70 :ptr + $74 = 20 :i32 + store($74, $71) :void + $78 = 8 :ui64 + $79 = func_*($1,$53,$78) :ptr + $82 = 0 :ui64 + $84 = 1 :ui64 + $85 = $82 * $84 :ui64 + $86 = $80 + $85 :ptr + $89 = 3 :i32 + store($89, $86) :void + $94 = 4 :ui64 + $96 = 1 :ui64 + $97 = $94 * $96 :ui64 + $98 = $80 + $97 :ptr + $101 = 30 :i32 + store($101, $98) :void + $105 = 8 :ui64 + $106 = func_*($1,$80,$105) :ptr + $108 = 0 :ui64 + $110 = 1 :ui64 + $111 = $108 * $110 :ui64 + $112 = $1 + $111 :ptr + $116 = 4 :ui64 + $118 = 1 :ui64 + $119 = $116 * $118 :ui64 + $120 = $1 + $119 :ptr + $123 = load($112) :i32 + $124 = load($120) :i32 + $125 = $123 + $124 :i32 + return ($125) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/after_ssa/staticLoopAssignStructToOuter.trace b/nautilus/test/data/value-tracing-tests/after_ssa/staticLoopAssignStructToOuter.trace new file mode 100644 index 000000000..67be07f29 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/after_ssa/staticLoopAssignStructToOuter.trace @@ -0,0 +1,73 @@ +EXECUTE: +B0() + ALLOCA $1 :ptr + ALLOCA $26 :ptr + ALLOCA $53 :ptr + ALLOCA $80 :ptr + CONST $3 0 :ui64 + CONST $5 1 :ui64 + MUL $6 $3 $5 :ui64 + ADD $7 $1 $6 :ptr + CONST $10 0 :i32 + STORE $13 $7 $10 :void + CONST $15 4 :ui64 + CONST $17 1 :ui64 + MUL $18 $15 $17 :ui64 + ADD $19 $1 $18 :ptr + CONST $22 0 :i32 + STORE $25 $19 $22 :void + CONST $28 0 :ui64 + CONST $30 1 :ui64 + MUL $31 $28 $30 :ui64 + ADD $32 $26 $31 :ptr + CONST $35 1 :i32 + STORE $38 $32 $35 :void + CONST $40 4 :ui64 + CONST $42 1 :ui64 + MUL $43 $40 $42 :ui64 + ADD $44 $26 $43 :ptr + CONST $47 10 :i32 + STORE $50 $44 $47 :void + CONST $51 8 :ui64 + CALL $52 func_*($1,$26,$51) :ptr + CONST $55 0 :ui64 + CONST $57 1 :ui64 + MUL $58 $55 $57 :ui64 + ADD $59 $53 $58 :ptr + CONST $62 2 :i32 + STORE $65 $59 $62 :void + CONST $67 4 :ui64 + CONST $69 1 :ui64 + MUL $70 $67 $69 :ui64 + ADD $71 $53 $70 :ptr + CONST $74 20 :i32 + STORE $77 $71 $74 :void + CONST $78 8 :ui64 + CALL $79 func_*($1,$53,$78) :ptr + CONST $82 0 :ui64 + CONST $84 1 :ui64 + MUL $85 $82 $84 :ui64 + ADD $86 $80 $85 :ptr + CONST $89 3 :i32 + STORE $92 $86 $89 :void + CONST $94 4 :ui64 + CONST $96 1 :ui64 + MUL $97 $94 $96 :ui64 + ADD $98 $80 $97 :ptr + CONST $101 30 :i32 + STORE $104 $98 $101 :void + CONST $105 8 :ui64 + CALL $106 func_*($1,$80,$105) :ptr + CONST $108 0 :ui64 + CONST $110 1 :ui64 + MUL $111 $108 $110 :ui64 + ADD $112 $1 $111 :ptr + CONST $116 4 :ui64 + CONST $118 1 :ui64 + MUL $119 $116 $118 :ui64 + ADD $120 $1 $119 :ptr + LOAD $123 $112 :i32 + LOAD $124 $120 :i32 + ADD $125 $123 $124 :i32 + RETURN $0 $125 :i32 + diff --git a/nautilus/test/data/value-tracing-tests/ir/staticLoopAssignStructToOuter.nautilus b/nautilus/test/data/value-tracing-tests/ir/staticLoopAssignStructToOuter.nautilus new file mode 100644 index 000000000..2a323f6bb --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/ir/staticLoopAssignStructToOuter.nautilus @@ -0,0 +1,75 @@ +nautilus { +execute() :i32 { +Block_0(): + $1 = alloca 8b :ptr + $26 = alloca 8b :ptr + $53 = alloca 8b :ptr + $80 = alloca 8b :ptr + $3 = 0 :ui64 + $5 = 1 :ui64 + $6 = $3 * $5 :ui64 + $7 = $1 + $6 :ptr + $10 = 0 :i32 + store($10, $7) :void + $15 = 4 :ui64 + $17 = 1 :ui64 + $18 = $15 * $17 :ui64 + $19 = $1 + $18 :ptr + $22 = 0 :i32 + store($22, $19) :void + $28 = 0 :ui64 + $30 = 1 :ui64 + $31 = $28 * $30 :ui64 + $32 = $26 + $31 :ptr + $35 = 1 :i32 + store($35, $32) :void + $40 = 4 :ui64 + $42 = 1 :ui64 + $43 = $40 * $42 :ui64 + $44 = $26 + $43 :ptr + $47 = 10 :i32 + store($47, $44) :void + $51 = 8 :ui64 + $52 = func_*($1,$26,$51) :ptr + $55 = 0 :ui64 + $57 = 1 :ui64 + $58 = $55 * $57 :ui64 + $59 = $53 + $58 :ptr + $62 = 2 :i32 + store($62, $59) :void + $67 = 4 :ui64 + $69 = 1 :ui64 + $70 = $67 * $69 :ui64 + $71 = $53 + $70 :ptr + $74 = 20 :i32 + store($74, $71) :void + $78 = 8 :ui64 + $79 = func_*($1,$53,$78) :ptr + $82 = 0 :ui64 + $84 = 1 :ui64 + $85 = $82 * $84 :ui64 + $86 = $80 + $85 :ptr + $89 = 3 :i32 + store($89, $86) :void + $94 = 4 :ui64 + $96 = 1 :ui64 + $97 = $94 * $96 :ui64 + $98 = $80 + $97 :ptr + $101 = 30 :i32 + store($101, $98) :void + $105 = 8 :ui64 + $106 = func_*($1,$80,$105) :ptr + $108 = 0 :ui64 + $110 = 1 :ui64 + $111 = $108 * $110 :ui64 + $112 = $1 + $111 :ptr + $116 = 4 :ui64 + $118 = 1 :ui64 + $119 = $116 * $118 :ui64 + $120 = $1 + $119 :ptr + $123 = load($112) :i32 + $124 = load($120) :i32 + $125 = $123 + $124 :i32 + return ($125) :i32 +} +} //nautilus diff --git a/nautilus/test/data/value-tracing-tests/tracing/staticLoopAssignStructToOuter.trace b/nautilus/test/data/value-tracing-tests/tracing/staticLoopAssignStructToOuter.trace new file mode 100644 index 000000000..d0e6aa1e2 --- /dev/null +++ b/nautilus/test/data/value-tracing-tests/tracing/staticLoopAssignStructToOuter.trace @@ -0,0 +1,129 @@ +EXECUTE: +B0() + ALLOCA $1 :ptr + ASSIGN $2 $1 :ptr + CONST $3 0 :ui64 + ASSIGN $4 $3 :ui64 + CONST $5 1 :ui64 + MUL $6 $4 $5 :ui64 + ADD $7 $2 $6 :ptr + ASSIGN $8 $7 :ptr + ASSIGN $9 $8 :ptr + CONST $10 0 :i32 + ASSIGN $11 $10 :i32 + ASSIGN $12 $11 :i32 + STORE $13 $9 $12 :void + ASSIGN $14 $1 :ptr + CONST $15 4 :ui64 + ASSIGN $16 $15 :ui64 + CONST $17 1 :ui64 + MUL $18 $16 $17 :ui64 + ADD $19 $14 $18 :ptr + ASSIGN $20 $19 :ptr + ASSIGN $21 $20 :ptr + CONST $22 0 :i32 + ASSIGN $23 $22 :i32 + ASSIGN $24 $23 :i32 + STORE $25 $21 $24 :void + ALLOCA $26 :ptr + ASSIGN $27 $26 :ptr + CONST $28 0 :ui64 + ASSIGN $29 $28 :ui64 + CONST $30 1 :ui64 + MUL $31 $29 $30 :ui64 + ADD $32 $27 $31 :ptr + ASSIGN $33 $32 :ptr + ASSIGN $34 $33 :ptr + CONST $35 1 :i32 + ASSIGN $36 $35 :i32 + ASSIGN $37 $36 :i32 + STORE $38 $34 $37 :void + ASSIGN $39 $26 :ptr + CONST $40 4 :ui64 + ASSIGN $41 $40 :ui64 + CONST $42 1 :ui64 + MUL $43 $41 $42 :ui64 + ADD $44 $39 $43 :ptr + ASSIGN $45 $44 :ptr + ASSIGN $46 $45 :ptr + CONST $47 10 :i32 + ASSIGN $48 $47 :i32 + ASSIGN $49 $48 :i32 + STORE $50 $46 $49 :void + CONST $51 8 :ui64 + CALL $52 func_*($1,$26,$51) :ptr + ALLOCA $53 :ptr + ASSIGN $54 $53 :ptr + CONST $55 0 :ui64 + ASSIGN $56 $55 :ui64 + CONST $57 1 :ui64 + MUL $58 $56 $57 :ui64 + ADD $59 $54 $58 :ptr + ASSIGN $60 $59 :ptr + ASSIGN $61 $60 :ptr + CONST $62 2 :i32 + ASSIGN $63 $62 :i32 + ASSIGN $64 $63 :i32 + STORE $65 $61 $64 :void + ASSIGN $66 $53 :ptr + CONST $67 4 :ui64 + ASSIGN $68 $67 :ui64 + CONST $69 1 :ui64 + MUL $70 $68 $69 :ui64 + ADD $71 $66 $70 :ptr + ASSIGN $72 $71 :ptr + ASSIGN $73 $72 :ptr + CONST $74 20 :i32 + ASSIGN $75 $74 :i32 + ASSIGN $76 $75 :i32 + STORE $77 $73 $76 :void + CONST $78 8 :ui64 + CALL $79 func_*($1,$53,$78) :ptr + ALLOCA $80 :ptr + ASSIGN $81 $80 :ptr + CONST $82 0 :ui64 + ASSIGN $83 $82 :ui64 + CONST $84 1 :ui64 + MUL $85 $83 $84 :ui64 + ADD $86 $81 $85 :ptr + ASSIGN $87 $86 :ptr + ASSIGN $88 $87 :ptr + CONST $89 3 :i32 + ASSIGN $90 $89 :i32 + ASSIGN $91 $90 :i32 + STORE $92 $88 $91 :void + ASSIGN $93 $80 :ptr + CONST $94 4 :ui64 + ASSIGN $95 $94 :ui64 + CONST $96 1 :ui64 + MUL $97 $95 $96 :ui64 + ADD $98 $93 $97 :ptr + ASSIGN $99 $98 :ptr + ASSIGN $100 $99 :ptr + CONST $101 30 :i32 + ASSIGN $102 $101 :i32 + ASSIGN $103 $102 :i32 + STORE $104 $100 $103 :void + CONST $105 8 :ui64 + CALL $106 func_*($1,$80,$105) :ptr + ASSIGN $107 $1 :ptr + CONST $108 0 :ui64 + ASSIGN $109 $108 :ui64 + CONST $110 1 :ui64 + MUL $111 $109 $110 :ui64 + ADD $112 $107 $111 :ptr + ASSIGN $113 $112 :ptr + ASSIGN $114 $113 :ptr + ASSIGN $115 $1 :ptr + CONST $116 4 :ui64 + ASSIGN $117 $116 :ui64 + CONST $118 1 :ui64 + MUL $119 $117 $118 :ui64 + ADD $120 $115 $119 :ptr + ASSIGN $121 $120 :ptr + ASSIGN $122 $121 :ptr + LOAD $123 $114 :i32 + LOAD $124 $122 :i32 + ADD $125 $123 $124 :i32 + RETURN $0 $125 :i32 + diff --git a/nautilus/test/execution-tests/ExecutionTest.cpp b/nautilus/test/execution-tests/ExecutionTest.cpp index f894561db..1a8ecf11b 100644 --- a/nautilus/test/execution-tests/ExecutionTest.cpp +++ b/nautilus/test/execution-tests/ExecutionTest.cpp @@ -1011,6 +1011,11 @@ void valueExecutionTest(engine::NautilusEngine& engine) { REQUIRE(f((int32_t) 0) == 2); REQUIRE(f((int32_t) -3) == 2); } + SECTION("staticLoopAssignStructToOuter") { + auto f = engine.registerFunction(staticLoopAssignStructToOuter); + // Final iteration writes inner.a=3, inner.b=30 into outer; sum is 33. + REQUIRE(f() == 33); + } SECTION("nonTrivialDestructor") { auto f = engine.registerFunction(nonTrivialDestructor); REQUIRE(f() == 42); diff --git a/nautilus/test/execution-tests/TracingTest.cpp b/nautilus/test/execution-tests/TracingTest.cpp index 758bd49d9..fb25f8484 100644 --- a/nautilus/test/execution-tests/TracingTest.cpp +++ b/nautilus/test/execution-tests/TracingTest.cpp @@ -346,6 +346,8 @@ TEST_CASE("Value Trace Test") { // move constructor (no traceCopy/ASSIGN op should appear for the move itself) {"moveConstructTrivial", details::createFunctionWrapper(moveConstructTrivial)}, {"returnByValue", details::createFunctionWrapper(returnByValue)}, + // static (fully-unrolled) loop with per-iteration alloca assigned into an outer struct + {"staticLoopAssignStructToOuter", details::createFunctionWrapper(staticLoopAssignStructToOuter)}, // destructor {"nonTrivialDestructor", details::createFunctionWrapper(nonTrivialDestructor)}, // loops