Skip to content

Assignments and expressions

Every value update to a hardware resource in Kathryn goes through a Cycle-Considered Operation (CCO) — the assignment operator that drives the resource. Only hardware resources count: assignments to plain C++ primitive variables, or to the intermediate expression& results that operators build, are not CCOs. Because CCOs are user-defined operators and Hybrid Design Blocks are composed from them, designers describe cycle-accurate control flow at the user level. There are two:

  • Edge Assignment — written <<=, resolved at the next clock edge.
  • Level Assignment — written =, resolved combinationally.

An Edge Assignment schedules its right-hand value to land on the target at the next clock edge. It is the operator that makes time visible: each <<= in a sequential block is one cycle of latency.

a <<= a + 1;
c <<= c + 1;

Edge Assignment is sensitive to the next pos/neg edge; its supported hardware resources are Register and MemBlk. In the code, Reg::doBlockAsm and MemBlockEleHolder::doBlockAsm implement it, while Wire, expression, and Val reject <<= (their doBlockAsm asserts).

A Level Assignment drives its target combinationally — sensitive to state and all of its sources. It carries no clock latency of its own.

par{
a = 0;
b = 0;
c = 0;
d = 0;
}

Level Assignment’s supported resources are Register and Wire. In the code the level path is doNonBlockAsm, reached through each type’s operator=; Reg, Wire, expression, and MemBlockEleHolder define it, while Val and PmVal reject it (they are read-only constants).

On a Register, = is equivalent to having issued the same Edge Assignment (<<=) one clock cycle earlier: the register already carries the right-hand value in the current cycle.

The two CCOs and where they apply:

flowchart TB
    CCO["Cycle-Considered Operation"]
    CCO --> E["Edge Assignment - written as A <<= B"]
    CCO --> L["Level Assignment - written as A = B"]
    E --> ET["resolved at next clock edge<br/>(one cycle of latency)"]
    ET --> ER["targets: Register, MemBlk"]
    L --> LT["resolved combinationally<br/>(sensitive to state and all sources)"]
    LT --> LR["targets: Register, Wire"]

Reading a signal and combining it with an operator produces an expression — a combinational result you can nest, slice, or assign. The overloaded operators live on Operable (src/model/hwComponent/abstract/) and each returns an expression&. Kathryn provides the usual bitwise, shift, comparison, signed-comparison, arithmetic, and bit-extension operators, applicable to all resource types. The right-hand side may be another Kathryn signal or a plain C++ integer:

a <<= a + 1; // arithmetic
d <<= c + d;
return (freenum + commitReqSize) >= (req2.uext(2) + 1); // comparison + extend

Bit-extension helpers appear on the results too — sext(width) for signed and uext(width) for unsigned extension:

result = g(instr(25, 32), instr(21, 25), instr(20)).sext(DATA_LEN);

Here g(...) concatenates instruction slices into a Nest and sext widens the concatenation. Slices such as instr(25, 32) are themselves readable operands, so expressions compose freely.

The g(...) and gr(...) accessors build a Nest — a composite that concatenates several signals (or slices) and inherits their update constraints. The difference is direction:

  • g(...) — read and write (makeNest). The concatenation can appear on the left of an assignment.
  • gr(...) — read-only (makeNestReadOnly).

From src/example/o3/core/immGen.h, a nest concatenates instruction slices and sign-extends the result:

zcase(IMM_I) {result = g(instr(25, 32), instr(21, 25), instr(20)).sext(DATA_LEN);}
  • Supported operators — every operator, one by one: semantics, result widths, integer operands, and the LUE/LSE rules.
  • Hardware resources — the resource types these assignments target.
  • Decentralized Update — how multiple blocks may assign the same resource, resolved by priority.