Constrained transactions

A constrained transaction is an ordinary C++ class derived from Randomized. Its Rand<T> fields register with the object, and its constructor records named constraints once:

enum class Opcode : uint8_t { Read, Write, Atomic, Reserved };

class Packet final : public Randomized {
  public:
    Rand<Opcode> opcode{*this, "opcode"};
    Rand<uint16_t> length{*this, "length"};
    Rand<uint16_t> address{*this, "address", 0x1000, 0x1fff};

    Packet() {
        constraint("supported opcode", opcode < Opcode::Reserved);
        constraint("packet length",
                   length >= uint16_t{64} && length <= uint16_t{1500});
        constraint("word length", length % uint16_t{4} == 0);
        constraint("aligned address", address % uint16_t{4} == 0);
        constraint("short atomic packet",
                   opcode != Opcode::Atomic || length <= uint16_t{256});
    }
};

The optional minimum and maximum arguments on Rand<T> narrow the declared field domain before constraint solving. Integral and enum fields through 64 bits are supported.

Supported expressions

Constraints form a backend-neutral expression tree rather than executing as ordinary booleans during construction.

Category

Operators

Arithmetic

+, -, *, %

Comparison

==, !=, <, <=, >, >=

Logic

&&, `

Membership

inside() with values or ranges

Use typed constants where narrowing would otherwise be ambiguous. Division, bitwise expressions, implication syntax, and arbitrary C++ function calls are not currently constraint operators. Calculate those values outside the model or express the relationship with the supported primitives.

Randomize in a test

Task<void> packet_sequence(Dut dut, TestContext& test) {
    Packet packet;

    for (uint32_t index = 0; index < 1000; ++index) {
        test.randomize(packet);

        co_await RisingEdge{dut.clk_i};
        dut.opcode_i.set(static_cast<uint8_t>(packet.opcode.get()));
        dut.length_i.set(packet.length.get());
        dut.address_i.set(packet.address.get());
    }
}

test.randomize(packet) selects the current process’s stream and configured constraint backend. If solving fails, the current test stops cleanly with the call site, backend name, and backend diagnostic. Successful randomization does not increment the test check count.

Values remain explicit. get() reads the latest solved value, while assigning to a Rand<T> only updates its stored C++ value:

packet.length = uint16_t{256};
const uint16_t length = packet.length.get();

That assignment does not constrain the next solve. Use an inline constraint when the value must participate in randomization.

Inline constraints

randomize_with() adds one hard constraint for a single call:

test.randomize_with(packet, packet.length == uint16_t{256});
test.randomize_with(packet,
                    packet.opcode == Opcode::Read &&
                    packet.address < uint16_t{0x1100});

The class constraints remain unchanged. This is useful for directed corner cases without creating transaction subclasses or mode flags.

Pre and post hooks

Override hooks only for transaction-local preparation or derived values:

class ChecksummedPacket final : public Randomized {
  public:
    Rand<uint16_t> payload{*this, "payload"};
    uint8_t checksum = 0;

    void pre_randomize() override {
        checksum = 0;
    }

    void post_randomize() override {
        checksum = static_cast<uint8_t>(payload.get() ^ (payload.get() >> 8));
    }
};

pre_randomize() runs before every solve attempt. post_randomize() runs only after a successful assignment. Hooks do not have scheduler access by default and should not hide DUT writes or timing.

Nonrepeating RandC fields

RandC<T> visits every value in its effective finite domain before beginning another cycle:

class TaggedRequest final : public Randomized {
  public:
    RandC<uint8_t> tag{*this, "tag", 0, 15};
};

Hard constraints narrow the cycle. For example, an inside(tag, {1, 3}) constraint produces both values once before either repeats. Multiple RandC fields maintain independent cycles; exhausting a short cycle does not reset a longer one.

Inspect results outside TestContext

Plain C++ code can choose how to handle a solve failure:

Random random{0x1234};
Packet packet;

const RandomizeResult result = packet.randomize(random);
if (!result) {
    std::cerr << result.message << '\n';
}

RandomizeResult::status distinguishes solved, proven unsatisfiable, search exhaustion, cycle exhaustion, and backend failure. See Solvers and diagnostics for the distinction and backend selection guidance.