Skip to content

Commit 9d941b6

Browse files
authored
fix(avm): cpp addressing (#13652)
fix(avm): cpp addressing testing for addressing
1 parent 53c070d commit 9d941b6

File tree

8 files changed

+386
-25
lines changed

8 files changed

+386
-25
lines changed

barretenberg/cpp/src/barretenberg/vm2/common/tagged_value.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ std::string TaggedValue::to_string() const
250250
[](const uint1_t& val) -> std::string { return val.value() == 0 ? "0" : "1"; },
251251
[](auto&& val) -> std::string { return std::to_string(val); } },
252252
value);
253-
return "TaggedValue(" + v + ", " + std::to_string(get_tag()) + ")";
253+
return std::to_string(get_tag()) + "(" + v + ")";
254254
}
255255

256256
} // namespace bb::avm2

barretenberg/cpp/src/barretenberg/vm2/common/tagged_value.hpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ enum class ValueTag {
2222
MAX = U128,
2323
};
2424

25+
template <typename T> ValueTag tag_for_type()
26+
{
27+
if constexpr (std::is_same_v<T, FF>) {
28+
return ValueTag::FF;
29+
} else if constexpr (std::is_same_v<T, uint1_t>) {
30+
return ValueTag::U1;
31+
} else if constexpr (std::is_same_v<T, uint8_t>) {
32+
return ValueTag::U8;
33+
} else if constexpr (std::is_same_v<T, uint16_t>) {
34+
return ValueTag::U16;
35+
} else if constexpr (std::is_same_v<T, uint32_t>) {
36+
return ValueTag::U32;
37+
} else if constexpr (std::is_same_v<T, uint64_t>) {
38+
return ValueTag::U64;
39+
} else if constexpr (std::is_same_v<T, uint128_t>) {
40+
return ValueTag::U128;
41+
}
42+
}
43+
2544
class TaggedValue {
2645
public:
2746
// We are using variant to avoid heap allocations at the cost of a bigger memory footprint.
@@ -70,7 +89,8 @@ class TaggedValue {
7089
if (std::holds_alternative<T>(value)) {
7190
return std::get<T>(value);
7291
}
73-
throw std::runtime_error("TaggedValue::as(): type mismatch");
92+
throw std::runtime_error("TaggedValue::as(): type mismatch. Wanted type " +
93+
std::to_string(static_cast<uint32_t>(tag_for_type<T>())) + " but got " + to_string());
7494
}
7595

7696
std::size_t hash() const noexcept { return std::hash<FF>{}(as_ff()); }

barretenberg/cpp/src/barretenberg/vm2/simulation/addressing.cpp

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,49 +30,72 @@ std::vector<Operand> Addressing::resolve(const Instruction& instruction, MemoryI
3030
assert(spec.num_addresses <= instruction.operands.size());
3131

3232
// We retrieve, cache first because this is probably what we'll do in the circuit.
33-
// However, we can't check the value and tag yet! This should be done only if it's used.
33+
// However, we can't check that the base address is valid* yet! This should be done only if it's used.
3434
// This is because the first few instructions might not YET have a valid stack pointer.
35-
auto base_address = memory.get(0);
35+
// *valid = valid_address and valid_tag.
36+
MemoryValue base_address = memory.get(0);
3637
event.base_address = base_address;
3738

38-
// First process relative addressing for all the addresses.
39+
// First, we make sure that the operands themselves are valid addresses.
40+
for (size_t i = 0; i < spec.num_addresses; ++i) {
41+
// We are a bit flexible here, we don't check the tag, because we might get a U8 or U16 from the
42+
// wire format but would want to interpret it as a MemoryAddress. We only need to check that "it fits".
43+
// That's why we use as_ff() here.
44+
if (!memory.is_valid_address(instruction.operands[i].as_ff())) {
45+
throw AddressingException(AddressingEventError::OPERAND_INVALID_ADDRESS, i);
46+
}
47+
}
48+
49+
// Guarantees by this point:
50+
// - all operands are valid addresses IF interpreted as a MemoryAddress.
51+
52+
// Then, we process relative addressing for all the addresses.
53+
// That is, if relative addressing is used, after_relative[i] = base_address + operands[i].
54+
// We fist store the operands as is, and then we'll update them if they are relative.
3955
event.after_relative = instruction.operands;
4056
for (size_t i = 0; i < spec.num_addresses; ++i) {
41-
if ((instruction.indirect >> i) & 1) {
57+
if ((instruction.indirect >> (i + spec.num_addresses)) & 1) {
4258
if (!memory.is_valid_address(base_address)) {
4359
throw AddressingException(AddressingEventError::BASE_ADDRESS_INVALID_ADDRESS, i);
4460
}
4561

62+
// We extend the address to FF to avoid overflows.
4663
FF offset = event.after_relative[i];
47-
offset = offset + base_address;
48-
event.after_relative[i] = Operand::from(offset);
64+
offset += base_address;
65+
// We store the offset as an FF operand. If the circuit needs to prove overflow, it will
66+
// need the full value.
67+
event.after_relative[i] = Operand::from<FF>(offset);
4968
if (!memory.is_valid_address(offset)) {
69+
// If this happens, it means that the relative computation overflowed. However both the base and
70+
// operand addresses by themselves were valid.
5071
throw AddressingException(AddressingEventError::RELATIVE_COMPUTATION_OOB, i);
5172
}
5273
}
74+
// Now that we are sure that the offset is valid, we can update the value to be of the right type.
75+
event.after_relative[i] = Operand::from(static_cast<MemoryAddress>(event.after_relative[i].as_ff()));
5376
}
5477

78+
// Guarantees by this point:
79+
// - all operands are valid addresses IF interpreted as MemoryAddress.
80+
// - all after_relative values are valid addresses.
81+
5582
// Then indirection.
83+
// That is, if indirection is used, resolved_operands[i] = memory[after_relative[i]].
84+
// We first store the after_relative values as is, and then we'll update them if they are indirect.
5685
event.resolved_operands = event.after_relative;
5786
for (size_t i = 0; i < spec.num_addresses; ++i) {
58-
if ((instruction.indirect >> (i + spec.num_addresses)) & 1) {
59-
FF offset = event.resolved_operands[i];
60-
if (!memory.is_valid_address(offset)) {
87+
if ((instruction.indirect >> i) & 1) {
88+
event.resolved_operands[i] = memory.get(event.after_relative[i].as<MemoryAddress>());
89+
if (!memory.is_valid_address(event.resolved_operands[i])) {
6190
throw AddressingException(AddressingEventError::INDIRECT_INVALID_ADDRESS, i);
6291
}
63-
FF new_address = memory.get(static_cast<MemoryAddress>(offset));
64-
event.resolved_operands[i] = Operand::from(new_address);
6592
}
6693
}
6794

68-
// We guarantee that the resolved operands are valid addresses.
69-
for (size_t i = 0; i < spec.num_addresses; ++i) {
70-
if (!memory.is_valid_address(MemoryValue(event.resolved_operands[i]))) {
71-
throw AddressingException(AddressingEventError::FINAL_ADDRESS_INVALID, i);
72-
}
73-
event.resolved_operands[i] =
74-
Operand::from<uint32_t>(static_cast<MemoryAddress>(event.resolved_operands[i].as_ff()));
75-
}
95+
// Guarantees by this point:
96+
// - all operands are valid addresses.
97+
// - all after_relative values are valid addresses.
98+
// - all resolved_operands are valid addresses.
7699
} catch (const AddressingException& e) {
77100
event.error = e;
78101
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#include "barretenberg/vm2/simulation/addressing.hpp"
2+
3+
#include <gmock/gmock.h>
4+
#include <gtest/gtest.h>
5+
6+
#include "barretenberg/vm2/common/memory_types.hpp"
7+
#include "barretenberg/vm2/common/opcodes.hpp"
8+
#include "barretenberg/vm2/simulation/events/event_emitter.hpp"
9+
#include "barretenberg/vm2/simulation/lib/instruction_info.hpp"
10+
#include "barretenberg/vm2/simulation/lib/serialization.hpp"
11+
#include "barretenberg/vm2/simulation/memory.hpp"
12+
#include "barretenberg/vm2/simulation/testing/mock_memory.hpp"
13+
#include "barretenberg/vm2/testing/instruction_builder.hpp"
14+
15+
namespace bb::avm2::simulation {
16+
namespace {
17+
18+
using ::bb::avm2::testing::InstructionBuilder;
19+
using ::bb::avm2::testing::OperandBuilder;
20+
using enum ::bb::avm2::WireOpCode;
21+
using ::testing::ElementsAre;
22+
using ::testing::ReturnRef;
23+
using ::testing::StrictMock;
24+
25+
template <typename T> auto from = ::bb::avm2::simulation::Operand::from<T>;
26+
27+
TEST(AvmSimulationAddressingTest, AllDirectAndNonRelative)
28+
{
29+
InstructionInfoDB instruction_info_db;
30+
NoopEventEmitter<AddressingEvent> event_emitter;
31+
Addressing addressing(instruction_info_db, event_emitter);
32+
MemoryValue zero_u32 = MemoryValue::from<uint32_t>(0);
33+
34+
{
35+
const auto instr = InstructionBuilder(SET_8)
36+
// dstOffset
37+
.operand<uint8_t>(1)
38+
// tag
39+
.operand(MemoryTag::FF)
40+
// value
41+
.operand<uint8_t>(1)
42+
.build();
43+
44+
StrictMock<MockMemory> memory;
45+
EXPECT_CALL(memory, get(0)).WillOnce(ReturnRef(zero_u32)); // call to get the base address.
46+
47+
const auto operands = addressing.resolve(instr, memory);
48+
EXPECT_THAT(operands,
49+
ElementsAre(
50+
// dstOffset has been resolved and is now a MemoryAddress.
51+
from<MemoryAddress>(1),
52+
// tag is unchanged.
53+
instr.operands.at(1),
54+
// value is unchanged.
55+
instr.operands.at(2)));
56+
}
57+
{
58+
const auto instr = InstructionBuilder(ADD_16)
59+
// aOffset
60+
.operand<uint16_t>(1)
61+
// bOffset
62+
.operand<uint16_t>(2)
63+
// dstOffset
64+
.operand<uint16_t>(3)
65+
.build();
66+
67+
StrictMock<MockMemory> memory;
68+
EXPECT_CALL(memory, get(0)).WillOnce(ReturnRef(zero_u32)); // call to get the base address.
69+
70+
const auto operands = addressing.resolve(instr, memory);
71+
EXPECT_THAT(operands, ElementsAre(from<MemoryAddress>(1), from<MemoryAddress>(2), from<MemoryAddress>(3)));
72+
}
73+
}
74+
75+
TEST(AvmSimulationAddressingTest, RelativeAddressing)
76+
{
77+
InstructionInfoDB instruction_info_db;
78+
NoopEventEmitter<AddressingEvent> event_emitter;
79+
Addressing addressing(instruction_info_db, event_emitter);
80+
81+
// For relative addressing, we need a base address other than 0
82+
// Base pointer at address 100
83+
MemoryValue base_addr = MemoryValue::from<uint32_t>(100);
84+
85+
// Set up the ADD_8 instruction with relative addressing
86+
const auto instr = InstructionBuilder(ADD_8)
87+
// aOffset
88+
.operand<uint8_t>(10)
89+
.relative()
90+
// bOffset
91+
.operand<uint8_t>(20)
92+
// dstOffset
93+
.operand<uint8_t>(30)
94+
.relative()
95+
.build();
96+
97+
StrictMock<MockMemory> memory;
98+
EXPECT_CALL(memory, get(0)).WillOnce(ReturnRef(base_addr));
99+
100+
const auto operands = addressing.resolve(instr, memory);
101+
102+
EXPECT_THAT(operands,
103+
ElementsAre(
104+
// aOffset resolved as base + 10 = 110
105+
from<MemoryAddress>(110),
106+
// bOffset stays the same
107+
from<MemoryAddress>(20),
108+
// dstOffset resolved as base + 30 = 130
109+
from<MemoryAddress>(130)));
110+
}
111+
112+
TEST(AvmSimulationAddressingTest, IndirectAddressing)
113+
{
114+
InstructionInfoDB instruction_info_db;
115+
NoopEventEmitter<AddressingEvent> event_emitter;
116+
Addressing addressing(instruction_info_db, event_emitter);
117+
118+
// For indirect addressing, memory locations contain the actual addresses
119+
MemoryValue zero_u32 = MemoryValue::from<uint32_t>(0);
120+
121+
// Set up the ADD_8 instruction with indirect addressing
122+
const auto instr = InstructionBuilder(ADD_8)
123+
// aOffset - address 5 contains the actual address
124+
.operand<uint8_t>(5)
125+
.indirect()
126+
// bOffset
127+
.operand<uint8_t>(10)
128+
// dstOffset - address 15 contains the actual address
129+
.operand<uint8_t>(15)
130+
.indirect()
131+
.build();
132+
133+
StrictMock<MockMemory> memory;
134+
EXPECT_CALL(memory, get(0)).WillOnce(ReturnRef(zero_u32)); // Base address
135+
MemoryValue addr_5_value = MemoryValue::from<uint32_t>(50);
136+
EXPECT_CALL(memory, get(5)).WillOnce(ReturnRef(addr_5_value));
137+
MemoryValue addr_15_value = MemoryValue::from<uint32_t>(60);
138+
EXPECT_CALL(memory, get(15)).WillOnce(ReturnRef(addr_15_value));
139+
140+
const auto operands = addressing.resolve(instr, memory);
141+
142+
// Expect indirect offsets to be resolved by looking up their values in memory
143+
EXPECT_THAT(operands,
144+
ElementsAre(
145+
// aOffset resolved indirectly: memory[5] = 50
146+
from<MemoryAddress>(50),
147+
// bOffset stays the same
148+
from<MemoryAddress>(10),
149+
// dstOffset resolved indirectly: memory[15] = 60
150+
from<MemoryAddress>(60)));
151+
}
152+
153+
TEST(AvmSimulationAddressingTest, IndirectAndRelativeAddressing)
154+
{
155+
InstructionInfoDB instruction_info_db;
156+
NoopEventEmitter<AddressingEvent> event_emitter;
157+
Addressing addressing(instruction_info_db, event_emitter);
158+
159+
// Base address is 100
160+
MemoryValue base_addr = MemoryValue::from<uint32_t>(100);
161+
162+
// Set up the ADD_8 instruction with both indirect and relative addressing
163+
const auto instr = InstructionBuilder(ADD_8)
164+
// aOffset (indirect and relative): address base+5 contains value
165+
.operand<uint8_t>(5)
166+
.indirect()
167+
.relative()
168+
// bOffset (indirect only): address 10 contains value
169+
.operand<uint8_t>(10)
170+
.indirect()
171+
// dstOffset (relative only)
172+
.operand<uint8_t>(15)
173+
.relative()
174+
.build();
175+
176+
StrictMock<MockMemory> memory;
177+
EXPECT_CALL(memory, get(0)).WillOnce(ReturnRef(base_addr)); // Base address (100)
178+
// Address 105 (base+5) contains value 200
179+
MemoryValue addr_105_value = MemoryValue::from<uint32_t>(200);
180+
EXPECT_CALL(memory, get(105)).WillOnce(ReturnRef(addr_105_value));
181+
// Address 10 contains value 60
182+
MemoryValue addr_10_value = MemoryValue::from<uint32_t>(60);
183+
EXPECT_CALL(memory, get(10)).WillOnce(ReturnRef(addr_10_value));
184+
185+
const auto operands = addressing.resolve(instr, memory);
186+
187+
// Expect combined indirect and relative addressing
188+
EXPECT_THAT(operands,
189+
ElementsAre(
190+
// aOffset: relative (base+5=105) and indirect (memory[105]=200)
191+
from<MemoryAddress>(200),
192+
// bOffset: indirect only (memory[10]=60)
193+
from<MemoryAddress>(60),
194+
// dstOffset: relative only (base+15=115)
195+
from<MemoryAddress>(115)));
196+
}
197+
198+
} // namespace
199+
} // namespace bb::avm2::simulation

barretenberg/cpp/src/barretenberg/vm2/simulation/events/addressing_event.hpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <cstdint>
44
#include <optional>
55
#include <stdexcept>
6+
#include <string>
67
#include <vector>
78

89
#include "barretenberg/vm2/common/instruction_spec.hpp"
@@ -12,16 +13,36 @@
1213
namespace bb::avm2::simulation {
1314

1415
enum class AddressingEventError {
16+
// The operand is not a valid address.
17+
OPERAND_INVALID_ADDRESS,
18+
// The base address is not a valid address.
1519
BASE_ADDRESS_INVALID_ADDRESS,
20+
// The relative computation overflowed.
1621
RELATIVE_COMPUTATION_OOB,
22+
// The address obtained after applying indirection is not a valid address.
1723
INDIRECT_INVALID_ADDRESS,
18-
FINAL_ADDRESS_INVALID,
1924
};
2025

26+
inline std::string to_string(AddressingEventError e)
27+
{
28+
switch (e) {
29+
case AddressingEventError::OPERAND_INVALID_ADDRESS:
30+
return "OPERAND_INVALID_ADDRESS";
31+
case AddressingEventError::BASE_ADDRESS_INVALID_ADDRESS:
32+
return "BASE_ADDRESS_INVALID_ADDRESS";
33+
case AddressingEventError::RELATIVE_COMPUTATION_OOB:
34+
return "RELATIVE_COMPUTATION_OOB";
35+
case AddressingEventError::INDIRECT_INVALID_ADDRESS:
36+
return "INDIRECT_INVALID_ADDRESS";
37+
}
38+
39+
// Only to please the compiler.
40+
return "UNKNOWN_ADDRESSING_ERROR";
41+
}
42+
2143
struct AddressingException : public std::runtime_error {
2244
explicit AddressingException(AddressingEventError e, size_t operand_idx = 0)
23-
: std::runtime_error("Addressing error: " + std::to_string(static_cast<int>(e)) + " at operand " +
24-
std::to_string(operand_idx))
45+
: std::runtime_error("Addressing error: " + to_string(e) + " at operand " + std::to_string(operand_idx))
2546
, error(e)
2647
, operand_idx(operand_idx)
2748
{}

barretenberg/cpp/src/barretenberg/vm2/simulation/execution.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ inline void Execution::call_with_operands(void (Execution::*f)(ContextInterface&
196196
assert(resolved_operands.size() == sizeof...(Ts));
197197
auto operand_indices = std::make_index_sequence<sizeof...(Ts)>{};
198198
[f, this, &context, &resolved_operands]<std::size_t... Is>(std::index_sequence<Is...>) {
199-
(this->*f)(context, resolved_operands.at(Is).template as<Ts>()...);
199+
// FIXME(fcarreiro): we go through FF here.
200+
(this->*f)(context, static_cast<Ts>(resolved_operands.at(Is).as_ff())...);
200201
}(operand_indices);
201202
}
202203

0 commit comments

Comments
 (0)