Quick Intro to Pyrope¶
Pyrope is a hardware description language: every construct elaborates to wires, muxes, flops, and memories. This page is the working subset needed to read and write correct Pyrope, with pointers to the chapters that explain each topic in depth. If you read nothing else, read this and the pitfalls at the end.
Declarations and storage kinds¶
Every declaration starts with a kind keyword — data: const / mut / reg;
lambda: comb / pipe / mod / fluid — and every data declaration needs
= value:
comptime const SIZE = 16 // compile-time constant (explicit `comptime`)
const my_constant = 42 // immutable after assignment (NOT compile-time)
mut my_wire = 0 // combinational: no persistence across cycles
reg my_state = 0 // register: persists across cycles; '= 0' is the RESET value
constmeans immutable, not compile-time;comptimeis an explicit prefix modifier (comptime mutis valid too). Identifier casing carries no meaning.nilmeans "no value yet" — reading it is a compile error;reg x = nildeclares a register with no reset. Unknown bits (Verilogx) are written0sb?/0ub10??01. There is no bare?and no_default.- No variable shadowing, anywhere.
;is the same as a newline.
Details: Variables and types.
One integer type¶
Integers are unlimited-precision and signed; everything else is a range
constraint on that one type. u8 is the same as int(min=0, max=255),
i4 is int(min=-8, max=7), and unsigned is int(min=0):
mut a:u8 = 100
mut b:int(min=0, max=300) = 0
wrap a = a + 200 // narrowing must be annotated: wrap drops bits, sat saturates
- Booleans and integers never mix:
if x != 0 {}, castsint(true),boolean(v#[3]).and/or/not/impliesare boolean-only;& | ^ ~are bitwise integer ops. - Precedence is shallow — parenthesize:
3 & (4*4), never3 & 4*4. - An unannotated narrowing assignment is a compile error; prefix it with
wraporsat.
Details: Basics, Variables and types, Attributes.
Tuples are the core data structure¶
mut p = (mut x:u8 = 0, mut y:u8 = 0) // named fields use a kind keyword
mut t = (1, 2, 3) // positional entries are bare values
mut arr = [1, 2, 3] // [] = array: all entries same type
cassert(p.x == 0) // named access (also p['x'])
cassert(t[0] == 1) // integer indices select positional entries ONLY
Named fields are unordered and name-access only — t[0] never aliases a
named field. (...a, ...b) concatenates (splice). A selector [...] takes one expression
(integer, string, range, or conditional).
Details: Tuples, Type system.
Lambdas (the only functions)¶
| kind | contract |
|---|---|
comb |
Pure combinational, zero cycles. No reg. |
pipe[N] |
Fixed latency N > 0; never a combinational input→output path. |
mod |
No constraints; every output declares its landing cycle: -> (x:u8@[2]). |
fluid |
Transactional valid/retry handshakes (TBD: not yet implemented). |
comb add(a:u8, b:u8) -> (r:u9) { r = a + b }
pipe mul(a:u16, b:u16) -> (c:u32) { c = a * b }
pipe acc(a:u32, b:u32) -> (c:u32) { wrap c = a + b }
mod mac(in1:u16, in2:u16) -> (out:u32@[4]) {
stage[3] tmp = mul(a=in1, b=in2) // pipe call: 3 stages
stage[3] in1_d = in1 // pure 3-cycle delay
stage[1] out@[4] = acc(a=tmp@[3], b=in1_d@[3]) // alignments typechecked
}
- Outputs are always declared by name in
-> (...); the body assigns them.returnis a terminator only —return Xis a syntax error. - Name your call arguments (
f(a=1, b=2)); parentheses always (noarg()). - UFCS
x.f(args)works only whenfdeclaresselffirst;ref selfneeds amutreceiver.refis written at declaration and call. initis the only implicit hook (the constructor, acomb); there are no getter/setter hooks. Overload by gathering:const add = [add1, add2]— a call dispatches to the first gathered lambda that can accept it (same argument rules as a direct call), resolved at compile time.
Details: Lambdas, Pipelining, Fluid, Instantiation.
Registers and time¶
reg counter:u8 = 0 // '= 0' is the reset value (nil ⇒ no reset)
const q = counter // a bare name reads the current q value
counter += 1 // write with plain =/+=; lands at the cycle boundary
const eoc = counter.[defer] // RHS-ONLY end-of-cycle read (wiring, not a flop) (TBD)
const old = past[2](counter) // value 2 cycles ago (inserts flops) (TBD)
Clock and reset are implicit; customize at the declaration:
reg c:u8:[clock_pin=ref clk2, reset_pin=ref rst2, sync=false] = 3
(_pin attributes connect wires, so they take ref; sync=false selects an
asynchronous reset). Memories are register arrays: reg mem:[256]u32 = 0.
Details: Statements, Attributes, Memories.
Control flow¶
if cond { y = 1 } elif other { y = 2 } else { y = 3 } // also an expression
match state { // exactly one arm runs; `else` is MANDATORY
== State.Idle { if start { state = State.Run } }
else { state = State.Idle }
}
for i in 0..<N { acc += f(i) } // loops fully unroll: bounds must be comptime
unique ifasserts mutually exclusive conditions (one-hot mux; replaces tri-state).- There are no
when/unlesstrailing gates and no runtime-bounded loops. - Ranges:
0..=7(inclusive),0..<8(exclusive),2..+3(size-based).
Details: Statements, Assertions.
Bits vs elements vs cycles¶
v#[3] // bit 3 v#[1..=4] // bit slice
v#sext[0..=2] // sign-extended slice
v#|[..] // or-reduce (int 0/1); also #& #^ #+ (popcount)
t[0] // tuple/array element
out@[4] // cycle typecheck (never inserts flops)
#[] is bits, [] is elements, @[] is cycles — never mix them.
Details: Variables and types, Internals.
A complete example¶
enum State = (Idle, Run, Done)
mod fsm(start:bool, fin:bool) -> (busy:bool@[0]) {
reg state:State = State.Idle
busy = state == State.Run
match state {
== State.Idle { if start { state = State.Run } }
== State.Run { if fin { state = State.Done } }
else { state = State.Idle }
}
}
test "fsm starts" {
const f = fsm(start=true, fin=false)
assert(not f.busy) // q value: still Idle this cycle
step
const f2 = fsm(start=false, fin=false)
assert(f2.busy) // Run after one cycle
}
Details: Verification for test/step/temporal
operators.
Coming from Verilog¶
| Verilog | Pyrope |
|---|---|
module m(...) |
mod m(...) -> (out:T@[N]) (or pipe[N]/comb) |
reg [7:0] x + reset |
reg x:u8 = 0 |
wire [7:0] x / blocking = |
mut x:u8 = 0 |
x <= y (non-blocking) |
x = y on a reg (defers automatically) |
parameter N = 8 |
comptime const N = 8 |
always @(posedge clk) / @(*) |
implicit — reg vs mut |
case ... endcase |
match x { == v {...} else {...} } |
x[6:3] |
x#[3..=6] |
{a, b} concat |
per-range LHS bit assigns into a typed destination |
4'b10x? |
0ub10?? |
| tri-state / one-hot mux | unique if |
testbench initial |
test "name" { ... step ... } |
More: Hardware design, vs other languages.
Common pitfalls¶
return Xis always wrong — assign the named output, then barereturn.- Outputs must be named in
-> (...); the clause is mandatory (-> ()for none; onlyselfmethods may omit it). matchwithout a finalelsearm is a parse error.constis not comptime; writecomptimeexplicitly..[defer]is RHS-only; there is no deferred-write form.@[N]never inserts flops;stage[N]does (mod-only). Apipecall needsstage[N]at the call site.- No bool/int mixing:
if 5 {}is a type error — writeif 5 != 0 {}. - Narrowing assignments need
wrap/sat. - Loop bounds must be comptime (loops unroll). No runtime loops, no comprehensions.
0b1010is invalid — write0ub1010/0sb1010.- Integer
[]indexing selects positional tuple entries only; named fields are name-access only. - Enum comparisons use names (
State.Idle), never raw integers.
Where to go next¶
- Hardware design mindset — why HDLs differ from software
- Basics — literals, operators, comments
- Tuples — the core data structure, enums
- Variables and types / Attributes
- Assertions / Statements
- Lambdas / Instantiation / Pipelining / Fluid
- Type system / Struct types
- Memories — arrays, SRAMs,
__memory,regref - Verification — tests, temporal library
- Internals / LNAST — compiler view
- Standard library (TBD)
- Implementation status — documented features not yet implemented in LiveHD (marked "TBD" throughout the chapters)