diff --git a/lib/fizzy/execute.cpp b/lib/fizzy/execute.cpp index d89c45fe7..67560ad85 100644 --- a/lib/fizzy/execute.cpp +++ b/lib/fizzy/execute.cpp @@ -477,10 +477,7 @@ ExecutionResult execute( const auto& code = instance.module.codesec[code_idx]; auto* const memory = instance.memory.get(); - std::vector locals(args.size() + code.local_count); - std::copy_n(args.begin(), args.size(), locals.begin()); - - OperandStack stack(static_cast(code.max_stack_height)); + OperandStack stack(args, code.local_count, static_cast(code.max_stack_height)); const Instr* pc = code.instructions.data(); const uint8_t* immediates = code.immediates.data(); @@ -614,22 +611,19 @@ ExecutionResult execute( case Instr::local_get: { const auto idx = read(immediates); - assert(idx <= locals.size()); - stack.push(locals[idx]); + stack.push(stack.local(idx)); break; } case Instr::local_set: { const auto idx = read(immediates); - assert(idx <= locals.size()); - locals[idx] = stack.pop(); + stack.local(idx) = stack.pop(); break; } case Instr::local_tee: { const auto idx = read(immediates); - assert(idx <= locals.size()); - locals[idx] = stack.top(); + stack.local(idx) = stack.top(); break; } case Instr::global_get: diff --git a/lib/fizzy/stack.hpp b/lib/fizzy/stack.hpp index f021ac5dc..2da812c94 100644 --- a/lib/fizzy/stack.hpp +++ b/lib/fizzy/stack.hpp @@ -4,6 +4,7 @@ #pragma once +#include "cxx20/span.hpp" #include "value.hpp" #include #include @@ -49,53 +50,89 @@ class Stack } }; + +/// Contains current frame's locals (including arguments) and operand stack. +/// The storage space for locals and operand stack together is allocated as a +/// continuous memory. Elements occupy the storage space in the order: +/// arguments, local variables, operand stack. Arguments and local variables can +/// be accessed under a single namespace using local() method and are separate +/// from the stack itself. class OperandStack { /// The size of the pre-allocated internal storage: 128 bytes. static constexpr auto small_storage_size = 128 / sizeof(Value); - /// The pointer to the top item, or below the stack bottom if stack is empty. + /// The pointer to the top item of the operand stack, + /// or below the stack bottom if stack is empty. /// - /// This pointer always alias m_storage, but it is kept as the first field + /// This pointer always alias m_locals, but it is kept as the first field /// because it is accessed the most. Therefore, it must be initialized - /// in the constructor after the m_storage. + /// in the constructor after the m_locals. Value* m_top; + /// The pointer to the beginning of the locals array. + /// This always points to one of the storages. + Value* m_locals; + + /// The pointer to the bottom of the operand stack. + Value* m_bottom; + /// The pre-allocated internal storage. Value m_small_storage[small_storage_size]; /// The unbounded storage for items. std::unique_ptr m_large_storage; - Value* bottom() noexcept { return m_large_storage ? m_large_storage.get() : m_small_storage; } - - const Value* bottom() const noexcept - { - return m_large_storage ? m_large_storage.get() : m_small_storage; - } - public: /// Default constructor. /// - /// Based on @p max_stack_height decides if to use small pre-allocated storage or allocate - /// large storage. - /// Sets the top item pointer to below the stack bottom. - explicit OperandStack(size_t max_stack_height) + /// Based on required storage space decides to use small pre-allocated + /// storage or allocate large storage. + /// Sets the top stack operand pointer to below the operand stack bottom. + /// @param args Function arguments. Values are copied at the beginning of the + /// storage space. + /// @param num_local_variables The number of the function local variables (excluding + /// arguments). This number of values is zeroed in the storage space + /// after the arguments. + /// @param max_stack_height The maximum operand stack height in the function. This excludes + /// args and num_local_variables. + OperandStack(span args, size_t num_local_variables, size_t max_stack_height) { - if (max_stack_height > small_storage_size) - m_large_storage = std::make_unique(max_stack_height); - m_top = bottom() - 1; + const auto num_args = args.size(); + const auto storage_size_required = num_args + num_local_variables + max_stack_height; + + if (storage_size_required <= small_storage_size) + { + m_locals = &m_small_storage[0]; + } + else + { + m_large_storage = std::make_unique(storage_size_required); + m_locals = &m_large_storage[0]; + } + + m_bottom = m_locals + num_args + num_local_variables; + m_top = m_bottom - 1; + + std::copy(std::begin(args), std::end(args), m_locals); + std::fill_n(m_locals + num_args, num_local_variables, 0); } OperandStack(const OperandStack&) = delete; OperandStack& operator=(const OperandStack&) = delete; + Value& local(size_t index) noexcept + { + assert(m_locals + index < m_bottom); + return m_locals[index]; + } + /// The current number of items on the stack (aka stack height). - size_t size() const noexcept { return static_cast(m_top + 1 - bottom()); } + size_t size() const noexcept { return static_cast(m_top + 1 - m_bottom); } /// Returns the reference to the top item. /// Requires non-empty stack. - auto& top() noexcept + Value& top() noexcept { assert(size() != 0); return *m_top; @@ -103,7 +140,7 @@ class OperandStack /// Returns the reference to the stack item on given position from the stack top. /// Requires index < size(). - auto& operator[](size_t index) noexcept + Value& operator[](size_t index) noexcept { assert(index < size()); return *(m_top - index); @@ -115,7 +152,7 @@ class OperandStack /// Returns an item popped from the top of the stack. /// Requires non-empty stack. - auto pop() noexcept + Value pop() noexcept { assert(size() != 0); return *m_top--; @@ -127,19 +164,8 @@ class OperandStack m_top -= num; } - /// Shrinks the stack to the given new size by dropping items from the top. - /// - /// Requires new_size <= size(). - /// shrink(0) clears entire stack and moves the top pointer below the stack base. - void shrink(size_t new_size) noexcept - { - assert(new_size <= size()); - // For new_size == 0, the m_top will point below the storage. - m_top = bottom() + new_size - 1; - } - /// Returns iterator to the bottom of the stack. - const Value* rbegin() const noexcept { return bottom(); } + const Value* rbegin() const noexcept { return m_bottom; } /// Returns end iterator counting from the bottom of the stack. const Value* rend() const noexcept { return m_top + 1; } diff --git a/test/unittests/span_test.cpp b/test/unittests/span_test.cpp index b954705ef..512a45e0e 100644 --- a/test/unittests/span_test.cpp +++ b/test/unittests/span_test.cpp @@ -50,7 +50,7 @@ TEST(span, array) TEST(span, stack) { - OperandStack stack(4); + OperandStack stack({}, 0, 4); stack.push(10); stack.push(11); stack.push(12); diff --git a/test/unittests/stack_test.cpp b/test/unittests/stack_test.cpp index 5ac226bf5..853ab9cdb 100644 --- a/test/unittests/stack_test.cpp +++ b/test/unittests/stack_test.cpp @@ -8,6 +8,14 @@ using namespace fizzy; using namespace testing; +namespace +{ +intptr_t address_diff(const void* a, const void* b) noexcept +{ + return std::abs(reinterpret_cast(a) - reinterpret_cast(b)); +} +} // namespace + TEST(stack, push_and_pop) { Stack stack; @@ -115,15 +123,13 @@ TEST(stack, struct_item) TEST(operand_stack, construct) { - OperandStack stack(0); - EXPECT_EQ(stack.size(), 0); - stack.shrink(0); + OperandStack stack({}, 0, 0); EXPECT_EQ(stack.size(), 0); } TEST(operand_stack, top) { - OperandStack stack(1); + OperandStack stack({}, 0, 1); EXPECT_EQ(stack.size(), 0); stack.push(1); @@ -136,7 +142,12 @@ TEST(operand_stack, top) EXPECT_EQ(stack.top().i64, 101); EXPECT_EQ(stack[0].i64, 101); - stack.shrink(0); + stack.drop(0); + EXPECT_EQ(stack.size(), 1); + EXPECT_EQ(stack.top().i64, 101); + EXPECT_EQ(stack[0].i64, 101); + + stack.drop(1); EXPECT_EQ(stack.size(), 0); stack.push(2); @@ -147,7 +158,9 @@ TEST(operand_stack, top) TEST(operand_stack, small) { - OperandStack stack(3); + OperandStack stack({}, 0, 3); + ASSERT_LT(address_diff(&stack, stack.rbegin()), 100) << "not allocated on the system stack"; + EXPECT_EQ(stack.size(), 0); stack.push(1); @@ -173,10 +186,51 @@ TEST(operand_stack, small) EXPECT_EQ(stack.top().i64, 12); } +TEST(operand_stack, small_with_locals) +{ + const fizzy::Value args[] = {0xa1, 0xa2}; + OperandStack stack(args, 3, 1); + ASSERT_LT(address_diff(&stack, stack.rbegin()), 100) << "not allocated on the system stack"; + + EXPECT_EQ(stack.size(), 0); + + stack.push(0xff); + EXPECT_EQ(stack.size(), 1); + EXPECT_EQ(stack.top().i64, 0xff); + EXPECT_EQ(stack[0].i64, 0xff); + + EXPECT_EQ(stack.local(0).i64, 0xa1); + EXPECT_EQ(stack.local(1).i64, 0xa2); + EXPECT_EQ(stack.local(2).i64, 0); + EXPECT_EQ(stack.local(3).i64, 0); + EXPECT_EQ(stack.local(4).i64, 0); + + stack.local(0) = 0xc0; + stack.local(1) = 0xc1; + stack.local(2) = 0xc2; + stack.local(3) = 0xc3; + stack.local(4) = 0xc4; + + EXPECT_EQ(stack.local(0).i64, 0xc0); + EXPECT_EQ(stack.local(1).i64, 0xc1); + EXPECT_EQ(stack.local(2).i64, 0xc2); + EXPECT_EQ(stack.local(3).i64, 0xc3); + EXPECT_EQ(stack.local(4).i64, 0xc4); + + EXPECT_EQ(stack.pop().i64, 0xff); + EXPECT_EQ(stack.size(), 0); + EXPECT_EQ(stack.local(0).i64, 0xc0); + EXPECT_EQ(stack.local(1).i64, 0xc1); + EXPECT_EQ(stack.local(2).i64, 0xc2); + EXPECT_EQ(stack.local(3).i64, 0xc3); + EXPECT_EQ(stack.local(4).i64, 0xc4); +} + TEST(operand_stack, large) { constexpr auto max_height = 33; - OperandStack stack(max_height); + OperandStack stack({}, 0, max_height); + ASSERT_GT(address_diff(&stack, stack.rbegin()), 100) << "not allocated on the heap"; for (unsigned i = 0; i < max_height; ++i) stack.push(i); @@ -187,26 +241,45 @@ TEST(operand_stack, large) EXPECT_EQ(stack.size(), 0); } -TEST(operand_stack, shrink) +TEST(operand_stack, large_with_locals) { - constexpr auto max_height = 60; - OperandStack stack(max_height); + const fizzy::Value args[] = {0xa1, 0xa2}; + constexpr auto max_height = 33; + constexpr auto num_locals = 5; + constexpr auto num_args = std::size(args); + OperandStack stack(args, num_locals, max_height); + ASSERT_GT(address_diff(&stack, stack.rbegin()), 100) << "not allocated on the heap"; for (unsigned i = 0; i < max_height; ++i) stack.push(i); EXPECT_EQ(stack.size(), max_height); - constexpr auto new_height = max_height / 3; - stack.shrink(new_height); - EXPECT_EQ(stack.size(), new_height); - EXPECT_EQ(stack.top().i64, new_height - 1); - EXPECT_EQ(stack[0].i64, new_height - 1); - EXPECT_EQ(stack[new_height - 1].i64, 0); + for (unsigned i = 0; i < max_height; ++i) + EXPECT_EQ(stack[i].i64, max_height - i - 1); + + EXPECT_EQ(stack.local(0).i64, 0xa1); + EXPECT_EQ(stack.local(1).i64, 0xa2); + + for (unsigned i = num_args; i < num_args + num_locals; ++i) + EXPECT_EQ(stack.local(i).i64, 0); + + for (unsigned i = 0; i < num_args + num_locals; ++i) + stack.local(i) = fizzy::Value{i}; + for (unsigned i = 0; i < num_args + num_locals; ++i) + EXPECT_EQ(stack.local(i).i64, i); + + for (int expected = max_height - 1; expected >= 0; --expected) + EXPECT_EQ(stack.pop().i64, expected); + EXPECT_EQ(stack.size(), 0); + + for (unsigned i = 0; i < num_args + num_locals; ++i) + EXPECT_EQ(stack.local(i).i64, i); } + TEST(operand_stack, rbegin_rend) { - OperandStack stack(3); + OperandStack stack({}, 0, 3); EXPECT_EQ(stack.rbegin(), stack.rend()); stack.push(1); @@ -217,9 +290,30 @@ TEST(operand_stack, rbegin_rend) EXPECT_EQ((stack.rend() - 1)->i64, 3); } +TEST(operand_stack, rbegin_rend_locals) +{ + const fizzy::Value args[] = {0xa1}; + OperandStack stack(args, 4, 2); + EXPECT_EQ(stack.rbegin(), stack.rend()); + + stack.push(1); + EXPECT_LT(stack.rbegin(), stack.rend()); + EXPECT_EQ(stack.rend() - stack.rbegin(), 1); + EXPECT_EQ(stack.rbegin()->i64, 1); + EXPECT_EQ((stack.rend() - 1)->i64, 1); + + stack.push(2); + EXPECT_LT(stack.rbegin(), stack.rend()); + EXPECT_EQ(stack.rend() - stack.rbegin(), 2); + EXPECT_EQ(stack.rbegin()->i64, 1); + EXPECT_EQ((stack.rbegin() + 1)->i64, 2); + EXPECT_EQ((stack.rend() - 1)->i64, 2); + EXPECT_EQ((stack.rend() - 2)->i64, 1); +} + TEST(operand_stack, to_vector) { - OperandStack stack(3); + OperandStack stack({}, 0, 3); EXPECT_THAT(std::vector(stack.rbegin(), stack.rend()), IsEmpty()); stack.push(1);