Variables and types¶
A variable is an instance of a given type. The type may be inferred from use. The basic types are Boolean, function, Integer, Range, and String. All those types can be combined with tuples.
Variable scope¶
Scope constrains variables visibility. There are three types of scope delimitation in Pyrope: code block scope, lambda scope, and tuple scope. Each has a different set of rules constraining the variable visibility. Overall, the variable/field is visible from declaration until the end of scope.
Pyrope uses mut or const to declare a variable, but all the declarations must
have a value. _ is used to specify the default value (false for boolean,
0 for integer, "" for string, undefined lambda for lambda, and 0..=0 for
range).
Every declaration starts with one of six kind keywords:
| Kind | Category | Implicit mutability |
|---|---|---|
const |
data | immutable |
mut |
data | mutable |
reg |
data | mutable, persists across cycles |
comb |
lambda | immutable (always) |
pipe |
lambda | immutable (always) |
mod |
lambda | immutable (always) |
The three data kinds take = expression; the three lambda kinds take a
parameter list and a body. Data declarations:
const variable [:type] [:[attribute list]] = expressionmut variable [:type] [:[attribute list]] = expressionreg variable [:type] [:[attribute list]] = reset_expression
Lambda declarations:
comb name[comptime_params][args] [-> outputs] { body }pipe[N] name[comptime_params][args] [-> outputs] { body }mod name[comptime_params][args] [-> outputs] { body }
This rule applies uniformly, including inside tuple literals: any
named field must start with one of these kind keywords. Bare
field = value inside a data-tuple literal is a compile error.
A positional (unnamed) field is just a value expression and inherits
its mutability from the enclosing tuple. To override that mutability on a
single positional field, prefix the value with const or mut —
(1, const 3) is two positional fields, the second one immutable.
Positional fields take no name slot, so const _ = 3 is not valid (and
bare _ is reserved as a future placeholder, see
Identifiers).
const point = (mut x:u8 = 0, mut y:u8 = 0)
const counter_iface = (
,mut value:u8 = 0
,comb read(self) -> (v:u8) { v = self.value }
,comb inc(ref self) { wrap self.value += 1 }
,mod tick(ref self, enable:bool) { self.value += 1 when enable }
)
mut y = (1, const 3) // 2nd field positional and immutable
Named-argument passing in calls (foo(a=3, b=4)) is not a declaration
— the names are matched against the callee's declared parameters — so no
kind keyword is required there.
The [...] slot after a lambda name declares explicit comptime parameters.
It is not a capture list. Lambdas can lexically read visible comptime bindings
from enclosing scopes, but runtime const, mut, and reg declarations from
enclosing lambda scopes are not visible in nested lambdas unless passed as
normal inputs. The compiler records lexical comptime references as explicit
comptime dependencies of the lambda.
assert a == 3 // error: undefined variable 'a'
mut a = 3
{
assert a == 3
a = 33 // OK. assign 33
a:int = 33 // OK, assign 33 and check that 'a' has type int
const b = 4
const a = 3333 // error: variable shadowing
mut a = 33 // error: variable shadowing
}
assert b == 3 // error: undefined variable 'b'
assert a == 3 // error: undefined variable 'a'
comptime const A = 3
comptime const X = A + 1
comb f1() -> (r) {
cassert A == 3
// A = 33 // error: comptime const is immutable
const b = 4
// const A = 3333 // error: variable shadowing
// mut A = 33 // error: variable shadowing
r = b + 3
}
assert f1() == 7
assert b == 3 // error: undefined variable 'b'
mut a = 3
comb f2() {
// assert a == 3 // error: runtime outer variable not visible
}
comb f3[ff:int=A]() {
cassert ff == 3 // OK, default uses visible comptime A
// ff = 3 // error: comptime parameter is immutable
}
mut base = 3
const r1 = (
,mut a = base+1 // tuple fields must use a kind keyword
,const c = {assert a == 4; 50}
)
r1.a = 33 // error: 'r1' is immutable variable
mut r2 = (mut a=100, const c=(mut next=a+1, const e=next+30))
assert r2 == (a=100,c=(next=101, e=131)) // checks values not mutability
r2.a = 33 // OK
r2.c.next = 33 // error: 'r2.c' is immutable variable
const r3 = (a = 1) // error: tuple field missing kind keyword
-
Shadowing is not allowed in lambdas or code blocks. Tuple field initializers follow program order and can read earlier tuple fields by name.
-
Data tuple literals do not have a
selfbinding. Theselfname is only available when declared as a lambda argument, such as in tuple methods. -
Tuple upper scope variables are always immutable.
-
Lambdas lexically see only visible comptime bindings from upper scopes. Runtime upper scope variables must be passed explicitly.
-
A variable is visible from definition until the end of scope in program order.
Since lambda inputs and comptime parameters are always immutable, it is not
allowed to declare them as mut and redundant to declare them as const.
comb f3(mut x) { x + 1 } // error: inputs are immutable
comb f4[mut x:int](z) { x + z } // error: comptime parameters are immutable
Tuple scope is also useful for declaring function default values:
comb example(a:int, b:int=a+5) -> (result:int) {
result = a + b
}
cassert example(a=3) == (3+3+5)
cassert example(6,7) == (6+7)
cassert example(6) == (6+6+5)
assert example(b=3) !=0 // error: undefined `a` argument
Basic types¶
Pyrope has 8 basic types:
boolean: eithertrueorfalseenum: enumeratedcomb: A function or pure combinational logicint: which is signed integer of unlimited precisionmod: A module with state/clock or side-effectsrange: A one hot encoding of values1..=3 == 0ub1110string: which is a sequence of charactersvariant: An union without typecast
All the types except functions can be converted back and forth to an integer.
Integer or int¶
Integers have unlimited precision and they are always signed. Unlike most other
languages, there is only one type for integer (unlimited), but the type system
allows to add constraints to be checked when assigning the variable contents.
Notice that the type is the same (u32 is the same type as i3, they just have
different constraints). As a result, u32 does u16 and u16 does u32 are both
true as type-structure checks; assignment still performs the additional range
and precision checks described in the attribute section:
int: an unlimited precision integer number.unsigned: An integer basic type constrained to be a natural number.u<num>: An integer basic type constrained to be a natural number with a maximum value of \(2^{\texttt{num}}\). E.g:u10can go from zero to 1024.i<num>: an integer 2s complement number with a maximum value of \(2^{\texttt{num}-1}-1\) and a minimum of \(-2^{\texttt{num}}\).int(a..<b): integer basic type constrained to be betweenaandb.
mut a:int = ? // any value, no constrain
mut b:unsigned = ? // only positive values
mut c:u13 = ? // only from 0 to 1<<13
mut d:int:[range=20..=30] = ? // only values from 20 to 30 (both included)
mut d:int:[min=-5, max=5] = ? // only values from -5 to 6 (6 not included)
mut e:int:[min=-1, max=0] = ? // 1 bit integer: -1 or 0
Integers can have 3 value (0,1,?) expression or a nil. Section
Integers has more details, but those values can not be
part of the type requirement.
Integer typecast accepts strings as input. The string must be a valid formatted Pryope number or an assertion is raised.
Boolean¶
A boolean is either true or false. Booleans can not mix with integers in
expressions unless there is an explicit typecast (int(false)==0 and
int(true)==-1) or the integer is a 1 bit signed integer (0 and -1). Unlike
integers, booleans do not support undefined value. A typecast from integer to
boolean will raise an assertion when the integer has undefined bits (?) or
nil.
const b = true
const c = 3
if c { call(x) } // error: 'c' is not a boolean expression
if c!=0 { call(x) } // OK
mut d = b or false // OK
mut e = c or false // error: 'c' is not a boolean
const e = 0xfeed
if e#[3] { // OK, bit extraction for single bit returns a boolean
call(x)
}
cassert 0 == (int(true) + 1) // explicity typecast
cassert 1 == (int(false) + 1) // explicity typecast
cassert boolean(33) or false // explicity typecast
String input typecase is valid, but anything different than ("0", "1", "-1", "true", "TRUE", "t", "false", "FALSE", "f") raises an assertion failure.
Logical and arithmetic operations can not be mixed.
const x = a and b
const y = x + 1 // error: 'x' is a boolean, '1' is integer
Functions (comb/pipe/mod)¶
Functions have several options (see Functions), but from a high level they provide a sequence of statements and they have a tuple for input and a tuple for output. Functions can have explicit comptime parameters and can lexically read visible comptime bindings from enclosing scopes. Like strings, functions are always immutable objects but they can be assigned to mutable variables.
Range¶
Ranges are very useful in hardware description languages to select bits. They are 3 ways to specify a closed range:
first..=last: Range from first to the last element, both includedfirst..<last: Range from first to last, but the last element is not includedfirst..+size: Range from first tofirst+size. Since there issizeelements, it is equivalent to writefirst..<(first+last).
When used inside selectors ([range]) the ranges can be open (no first/last specified)
or use negative numbers. Ranges only work with positive numbers, a negative
number is to specify the distance from last.
[first..<-val]is the same as[first..<(last-val+1)]. The advantage is that thelastor size in the tuple can be unknown.[first..]is the same as[first..=-1].
const a = (1,2,3)
cassert a[0..] == (1,2,3)
cassert a[1..] == (2,3)
cassert a[..=1] == (1,2)
cassert a[..<2] == (1,2)
cassert a[1..<10] == (2,3)
const b = 0ub0110_1001
cassert b#[1..] == 0ub0110_100
cassert b#[1..=-1] == 0ub0110_100
cassert b#[1..=-2] == 0ub0110_100 // unsigned result from bit selector
cassert b#sext[1..=-2] == 0sb110_100
cassert b#[1..=-3] == 0sb10_100
cassert b#[1..<-3] == 0ub0_100
cassert b#[0] == false
A range is a separate tuple. As such it can not directly compare with tupes. It requires an explicit conversion. If the range does not contain negative values, it can be converted to an integer back and forth which corresponds to a one-hot encoding.
Range type cast from integers use the same one-hot encoding. It is not possible to type cast from tuple to range, but it is possible from range to tuple.
const c = 1..=3
cassert int(c) == 0ub1110
cassert range(0ub01_1100) == 2..=4
assert range(1,2,3) // error: typecast not allowed
cassert (1,2,3) == tuple(1..=3)
In most cases, the range can be used in contructs like for for positive and
negative numbers. The tuple typecast is not needed, but if placed the
semantic is the same. The same tuple typecast is also optional when doing a
comparison. Both ranges a step to change the step.
cassert int(0..=10 step 2) == 0ub101_0101_0101
cassert tuple(0..=10 step 2) == ( 0,2,4,6,8,10)
cassert tuple(10..=0 step -2) == (10,8,6,4,2, 0)
cassert (10..=0 step -2) == (10,8,6,4,2, 0)
cassert -1..=2 == (-1,0,1,2)
const x = -1..=2
cassert (i for i in 0..=10 step 2) == (0,2,4,6,8,10)
Since the range is an integer, a decreasing range should have the same meaning
that an increasing range (1..=3 == 3..=1) but to avoid mistakes/confusions,
Pyrope generates a compile error in decreasing ranges.
assert 5..=0 // error: 5 + 1 never reaches 0
assert 5..=0 step -1 == (5,4,3,2,1,0)
A closed range can be converted to a single integer or a tuple. A range
encoded as an integer is a set of one-hot encodings. As such, there is no
order, but in Pyrope, ranges always have the order from smallest to largest.
The step expr can be added to indicate a step or step function. This is only
possible when both begin and end of the range are fully specified.
cassert((0..<30 step 10) == (0,10,20)) // ranges and tuples can combined
cassert((1..=3) ++ 4 == (1,2,3,4)) // tuple and range ops become a tuple
cassert 1..=3 == (1,2,3)
cassert((1..=3)#[..] == 0ub1110) // convert range to integer with #[..]
String¶
Strings are a basic type, but they can be typecasted to integers using the ASCII sequence. The string encoding assigns the lower bits to the first characters in the string, each character has 8 bits associated.
const a = 'cad' // c is 0x63, a is 0x61, and d is 0x64
const b = 0x64_61_63
cassert a == string(b) // typecast number to string
cassert int(a) == b // typecast string to number
cassert a#[..] == b // typecast string to number
Like ranges, strings can also be seen as a tuple, and when tuple operations are performed they are converted to a tuple.
cassert "hello" == ('h','e','l','l','o')
cassert "h" ++ "ell" == ('h','e','l','l') == "hell"
Type declarations¶
Each variable has a type, either implicit or explicit, and as such, it can be used to declare a new type.
Pyrope provides a type keyword for type declarations. It is equivalent to
a const whose value is a type (tuple shape or lambda signature); type
simply makes the intent explicit and is the recommended spelling when
declaring types ahead. It is recommended to start type names with
Uppercase. Complicated lambda types cannot be written inline in a
foo:Type annotation — declare them ahead with type and reference them
by name.
comb check_is_green(self) { self.color == "green" }
type IsGreen = comb(self)
mut bund1 = (mut color:string = "", mut value:s33 = nil)
x:bund1 = nil // OK, declare x of type bund1 with default values
bund1.color = "red" // OK
bund1.is_green = check_is_green
x.color = "blue" // OK
type Typ = (mut color:string = "", mut value:s33 = nil, mut is_green:IsGreen = nil)
y:Typ = nil // OK
Typ.color = "red" // error:
Typ.is_green = check_is_green
y.color = "red" // OK
type Bund3 = (mut color:string = "", mut value:s33 = nil)
z:Bund3 = nil // OK
Bund3.color = "red" // error:
Bund3.is_green = check_is_green // error: (const can not add fields)
z.color = "blue" // OK
assert x equals Typ // same type structure
assert z equals Typ // same type structure
assert x equals z // same type structure
assert y is Typ
assert Typ is Typ
assert z !is Bund3
assert z !is Typ
assert z !is bund1
Type checks¶
A :Type annotation is only valid at a declaration site (mut, reg,
const, comb, pipe, mod, lambda parameters, lambda return types, and
tuple field declarations). Once the variable is declared, the type is set
for its whole existence.
To check that an existing value matches a type, use the does or is
operator inside cassert/assert. To convert a value to a type, call the
type as a constructor — u8(value).
mut a = true // infer a is a boolean
cassert a does bool // type check on an existing variable
foo = a or false // ordinary use; no inline type annotation
Attributes¶
Attributes is the mechanism that the programmer specifies some special checks/functionality that the compiler should perform. Attributes are associated to variables either by setting them at declaration or by reading their value at use sites. Some example of attribute use is to mark statements compile time constant, read the number of bits in an assertion, give placement hints, or even interact with the synthesis flow to read timing delays.
A key difference between attributes and tuple fields is that attributes are always compile time and the compiler flow has special meaning functionality for them.
Pyrope does not specify all the attributes, the compiler flow specifies them. There are some built-in required attributes like checking the number of bits.
Reading attributes should not affect a logical equivalence check. Setting attributes can have a side-effect because it can change bits used for wrap/saturate or change pins like reset/clock in registers. Additionally, attributes can affect assertions, so they can stop/abort the compilation.
There are two operations that can be done with attributes: set and read. The two operations have distinct syntax so the reader (and the parser) can never confuse them.
-
Set (
var::[attr]when no explicit type;var:Type:[attr]when a type is given — the colon between the type and the attribute block is not doubled): only allowed at declaration sites —mut,reg,const,comb,pipe,mod— and on tuple fields at the point they are introduced. The set binds the attribute to all uses of the variable. If no value is given, the attribute is set totrue. E.g:mut foo:uint:[max=300] = 4,reg counter::[clock_pin=clk1] = 0,const c::[debug] = 3,mut x2:complex:[valid=false] = 0. -
Read (
var.[attr]): allowed everywhere a normal expression is allowed. Returns the attribute's current value (any type — usually integer, boolean, or string). If the attribute was not set, the read returnsnil. E.g:tmp.[bits] < 30,assert.[failed],x.[size],i.[max].
A small subset of attributes correspond to a runtime hardware signal
rather than to compile-time metadata — for example valid (the per-cycle
optional bit) and defer (the end-of-cycle register port). For these the
.[attr] form can also appear on the left-hand side of an assignment to
drive the underlying wire: self.[valid] = v != 33,
reg.[defer] = rhs. Compile-time-only attributes (max, bits,
comptime, debug, file, …) are read-only at use sites; bind them with
::[…] at the declaration.
Since attributes are always compile time, the read happens at elaboration
time. To turn a read into a check, wrap it in cassert (or assert):
cassert y.[comptime] // 'y' must be comptime
cassert y.[bar] == 3 // 'y.[bar]' must equal 3
cassert tmp.[bits] < 30
To check whether an attribute is set at all (without caring about its
value), compare against nil. Inside .[...] reads, comparisons against
nil are exempt from "the attribute must be defined" — they return true
or false rather than erroring at compile time:
cassert foo.[attr1] != nil // attr1 was set on 'foo'
cassert xx.[attr2] == nil // attr2 was never set on 'xx'
cassert foo.[attr1] == 2 // value check (errors at compile time if unset)
Since conditional code can depend on an attribute, which results in executing a different code sequence that can lead to the change of the attribute. This can create a iterative process. It is up to the compiler to handle this, but the most logical is to trigger a compile error if there is no fast convergence.
// comptime as prefix modifier
comptime const foo:u32 = xx // enforce that foo is comptime constant
yyy = xx // yyy does not check comptime
cassert yyy.[comptime] // now, checks that 'yyy' is comptime
// reading attributes
if bar == 3 {
tmp = bar
cassert bar.[comptime]
}
cassert tmp.[bits] < 30 and not tmp.[comptime]
Pyrope allows to assign the attribute to a variable or a function call. Not to statements because it is confusing if applied to the condition or all the sub-statements.
comptime mut z = xx // z is a comptime variable
if cond.[comptime] { // cond is checked to be compile time constant
comptime const x = a + 1 // x is comptime
}else{
comptime const x = b // x is comptime
}
if cond.[comptime] { // checks if cond is comptime
const v = cond
if cond {
puts "cond is compile time and true"
}
}
The programmer could create custom attributes but then a LiveHD compiler pass
to deal with the new attribute is needed to handle based on their specific
semantic. To understand the potential Pyrope syntax, this is a hypothetical
poison attribute that marks a tuple field.
const bad = (a=3, b::[poison=true]=4)
const b = bad.b
cassert b.[poison] and b==4
Attributes control fields like the default reset and clock signal. This allows to change the control inside procedures. Notice that this means that attributes are passed by reference. This is not a value copy, but a pass by reference. This is needed because when connecting things like a reset, we want to connect to the reset wire, not the current reset value.
reg counter:u32 = 0
reg counter2::[clock_pin=clk1]=0
reg counter3::[reset_pin=rst2]=0
In the long term, the goal is to have any synthesis directive that can affect the correctness of the result to be part of the design specification so that it can be checked during simulation/verification.
There are 3 main classes of a attributes that all the Pyrope compilers should always implement: Bitwidth, comptime, debug.
Variable attribute list¶
In the future, the compiler may implement some of the following attributes, as such, these attribute names are reserved and not allowed for custom attribute passes:
clock: indicate a signal/input is a clock wirecritical: synthesis time criticalitydebug(sticky): variable use for debug only, not synthesis alloweddelay: synthesis time delaydeprecated: to generate special warnigns about usagedonttouch: do not touch/optimize awayfile: to print the file where the variable was declaredinline,noinline: to indicate if a module is inlinedinp_delay,out_delay: synthesis optimizations hintskeep: same as donttouch but shorterkey: variable/entry key nameleft_of,right_of,top_of,bottom_of,align_with: placement hintsconstandmut: is the variable declared asconstand/ormutloc: line of code informationmax_delay,min_delay: synthesis optimizations checked at simulationmax_load,max_fanout,max_cap: synthesis optimization hintsmulticycle: number of cycles for optimizations checked at simulationpipeline: pipeline related informationprivate: variable/field not visible to import/regrefrandandcrand: simulation and compile time random number generationreset: indicate a signal/input is a reset wiresize: Number of entries in tuple or arraytypename: type name at variable declarationvalid,retry: for elastic pipelineswarn: is a boolean what when set to false disables compile warnings for associated variable
Registers and pipestage attribute list¶
Registers have the following attributes:
sync: true by default, when false selects an asynchronous reset (posedge only)initial: reset value when reset is highclock: connected toclockby defaultreset: connected toresetby defaultnegreset: active low reset signalposclk: true by default, selects a posedge or negnedge flopretime: allow to retime across the registerdefer: read or write the end-of-cycle value.reg.[defer] = rhsdelays the write until the end of the current cycle (becomes the next cycle's 'q').reg.[defer]on the RHS reads the final value at the end of the current cycle. See Pipelining.
Pipestage accept the same register attributes but also two more:
lat: latency for the pipestagenum: Number of unitsi used when the pipestage is not fully pipelined.
Memories attribute list¶
Memories are arrays with persistence like registers. As such, some of the attributes are similar to registers, but unlike registers they can have multiple clocks.
addr: Tuple of address ports for the memory.bits: The number of bits for each memory entrysize: The number of entries. Total size in bits is \(size x bits\).clock: Optional clock pin,clockby default. A tuple is possible to specify the clock for each address port.din: Tuple for memory data in port. The read ports must be hardwired to0.enable: Tuple for each memory port. Write or read enable (read ports can have enable too).fwd: Forwarding guaranteed (true/false). If fwd is false, there is no guarantee, it can have fwd or not.latency: Number of cycles (0or1) when the read is performedwensize: Write enable size allows to have a write mask. The default value is 1, a wensize of 2 means that there are 2 bits in theenablefor each port. a wensize 2 with 2 ports has a total of 2+2+2 enable bits. Bit 0 of the enable controls the lower bits of the memory entry selected.rdport: Indicates which of the ports are read and which are written ports.posclk: Positive edge clock memory for all the memory clocks. The default istruebut it can be set tofalse.
Lambda attribute list¶
Lambda attributes allow Introspection which requires some attributes.
inputs: returns the input tuple from the lambdaoutputs: returns the output tuple from the lambda
Bitwidth attribute list¶
To set constraints on integer, boolean, and range basic types, the compiler has a set of bitwidth related attributes:
max: the maximum value allowedmin: the minimum value allowedubits: Maximum number of bits to represent the unsigned value. The number must be positive or zerosbits: Maximum number of bits, and the number can be negativebits: read-only; returns the number of bits currently required to represent the variable's value (var.[bits]in assertions)wrap: allows to drop bits that do not fit on the left-hand side. It performs sign extension if needed.saturatekeeps the maximum or minimum (negative integer) that fits on the left-hand side.
Debug and verification attribute list¶
The following attributes are debug-only — they are elided from synthesis and
only valid inside assert, cover, test, and waitfor contexts:
rising: true on the cycle where the signal transitions from 0/false to non-zero/true (same asrose(sig)from the temporal library)falling: true on the cycle where the signal transitions from non-zero/true to 0/false (same asfell(sig))changed: true on any cycle where the signal differs from its previous value (same aschanged(sig))timeout: interpreted only bywaitfor; bounds the wait toNcycles (e.g.,waitfor(ref done_rising, timeout=1000)after bindingmut done_rising = done.[rising])
See Verification for the full temporal library and debug constructs.
The integer type constructor allows to use a range to set max/min, but it is syntax sugar for direct attribute set.
opt1:uint:[max=300] = 0
opt2:int:[min=0,max=300] = 0 // same
opt3::[min=0,max=300] = 0 // same
opt4:int:[range=0..=300] = 0 // same
cassert opt1.[ubits] == 0 // opt1 initialized to 0, so 0 bits
opt1 = 200
cassert opt1.[ubits] == 8 // last assignment needs 9 sbits or 8 ubits
wrap and saturate control how the right-hand side of an assignment
narrows into the left-hand side when the value would otherwise overflow.
Two forms are supported:
- Sticky — set as an attribute at declaration. Every assignment to that variable is then wrapped (or saturated) automatically.
- Per-statement — use
wraporsatas a statement-level prefix modifier (similar tocomptimeordebug). The modifier controls the narrowing for that single assignment.
a:u32 = 100
b:u10 = 0
c:u5 = 0
d:u5 = 0
w:u5:[wrap] = 0 // sticky: every assignment to 'w' wraps
b = a // OK, no precision lost
wrap c = a // OK, same as c = a#[0..<5] (since 100 is 0ub1100100, c==4)
c = a // error: 100 overflows the maximum value of 'c'
w = a // OK, 'w' has a wrap set at declaration
sat c = a // OK, c == 31
c = 31
d = c + 1 // error: '32' overflows the maximum value of 'd'
wrap d = c + 1 // OK, d == 0
sat d = c + 1 // OK, d == 31
sat d += 1 // OK, compound assignment
sat x:bool = c // error: saturate only allowed in integers
The prefix is part of the assignment statement; it controls the narrowing
of the final RHS into the LHS. To narrow individual sub-expressions
independently, factor them into intermediate variables with their own
sticky ::[wrap] or ::[saturate].
comptime modifier¶
Pyrope borrows the comptime functionality from Zig. comptime is a prefix
modifier that can be applied to const or mut to indicate that the variable
must be resolvable at compile/elaboration time. comptime alone is shorthand
for comptime const.
comptime const SIZE = 16
comptime const a = 1 // same as above
comptime mut counter = 0 // mutable at compile time (updated during elaboration)
comptime const b = a + 2 // OK, comptime const
comptime c = rand // error: 'c' is not resolvable at compile time
cassert SIZE == 16
cassert b == 3
The comptime status can still be queried with .[comptime]:
cassert a.[comptime]
To avoid too frequent comptime directives, Pyrope treats all the variables that start with uppercase as compile time constants.
comptime const Xconst1 = 1 // obvious comptime
comptime const Xvar2 = rand // error: 'Xvar2' is not compile time constant
debug attribute¶
In software and more commonly in hardware, it is common to have extra statements and state to debug the code. These debug functionality can be more than plain assertions, they can also include code.
The debug attribute marks a mutable or immutable variable. At synthesis, all
the statements that use a debug can be removed. debug variables can read
from non debug variables, but non-debug variables can not read from debug.
This guarantees that debug variables, or statements, do not have any
side-effects beyond debug statements.
mut a = (b::[debug]=2, c = 3) // a.b is a debug variable
const c::[debug] = 3
Assignments to debug variables also bypass protection access. This means that
private variables in tuples can be accessed (read-only). Since assert marks
all the results as debug, it allows to read any public/private variable/field.
x:(_priv=3, zz=4) = ?
const tmp = x._priv // error:
const tmp::[debug] = x.priv // OK
assert x._priv == 3 // OK, assert is a debug statement
Register¶
Both mutable and immutable variables are created every cycle. To have
persistence across cycles the reg type must be used.
reg counter:u32 = 10
mut not_a_reg:u32 = 20
In reg, the right-hand side of the initialization (10 in the
counterexample) is called only during reset. In non-register variables, the
right-hand side is called every cycle. Most of the cases reg is mutable but
it can be declared as immutable.
Public vs private¶
All variables are public by default. To declare a variable private within the
tuple or file the private attribute must be set.
The private has different meaning depending on when it is applied:
-
When applied to a tuple entry (
(field::[private] = 3)), it means that the entry can not be accessed outside the tuple. -
When applied to a
pipestagevariable (mut foo::[private] = 3), it means that the variable is not pipelined to the next type stage. Section pipestage has more details. -
When is applied to a pyrope file upper scope variable (
reg top_reg:[private] = 0), it means that animportcommand or register reference can not access it across files. Section typesystem has more details.
Operators¶
There are the typical basic operators found in most common languages except exponent operations. The reason is that those are very hardware intensive and a library code should be used instead.
All the operators work over signed integers.
Unary operators¶
!aornot alogical negation~abitwise negation-aarithmetic negation
Binary integer operators¶
a + badditiona - bsubstractiona * bmultiplicationa / bdivisiona & bbitwise anda | bbitwise ora ^ bbitwise xora ~& bbitwise nanda ~| bbitwise nora ~^ bbitwise xnora >> barithmetic right shifta#[..] >> blogical right shifta << bleft shift
In the previous operations, a and b need to be integers. The exception is
a << b where b can be a tuple. The << allows having multiple values
provided by a tuple on the right-hand side or amount. This is useful to create
one-hot encodings.
cassert 1<<(1,4,3) == 0ub01_1010
Binary boolean operators¶
a and blogical anda or blogical ora implies blogical implicationa !and blogical nanda !or blogical nora !implies blogical not implication
Tuple/Set operators¶
a in bis elementain tupleba !in btrue when elementais not in tuplebtuple(a)convertsato tuple,acan be a boolean, range, integer, string, or already a tuple
Most operations behave as expected when applied to signed unlimited precision integers.
The a in b checks if values of a are in b. Notice that both can be
tuples. If a is a named tuple, the entries in b match by name, and then
contents. If a is unnamed, it matches only contents by position.
cassert (1,2) in (0,1,3,2,4)
cassert (1,2) in (a=0,b=1,c=3,2,e=4)
cassert (a=2) !in (1,2,3)
cassert (a=2) in (1,a=2,c=3)
cassert (a=1,2) in (3,2,4,a=1)
cassert (a=1,2) !in (1,2,4,a=4)
cassert (a=1) !in (a=(1,2))
The a in b has to deal with undefined values (nil, 0sb?). The LHS with an undefined
will be true if the RHS has the same named entry either defined or undefined.
cassert (x=nil,c=3) in (x=3,c=3)
cassert (x=nil,c=3) in (x=nil,c=3,d=4)
cassert (c=3) !in (c=nil,d=4)
a ++ bconcatenate two tuples. If field appears in both, concatenate field. The a field is defined in one tupe and undefined in the other, the undefined value is not concatenated.
cassert ((a=1,c=3) ++ (a=1,b=2,c=nil)) == (a=(1,1), c=3, b=2)
cassert ((1,2) ++ (a=2,nil,5)) == (1,2,a=2,5)
cassert ((x=1) ++ (a=2,nil,5)) == (x=1,a=2,nil,5)
cassert ((x=1,b=2) ++ (x=0sb?,3)) == (x=1,b=2,3)
(,...b)in-place insertb. Behaves likea ++ bbut it triggers a compile error if both have the same defined named field.
cassert (1,b=2,...(3,c=3),6) == (1,b=2,3,c=3,6)
cassert (1,b=2,...(nil,c=3),0sb?,6) == (1,b=2,nil,c=3,0sb?,6)
Type operators¶
a has bchecks ifatuple has thebfield wherebis a string or integer (position).
cassert((a=1,b=2) has "a")
a does bis true whenahas all the tuple structure required byba equals bsame as(a does b) and (b does a)a case bsame as(a does b)plus value matching for every defined value inb. Values inbthat are undefined (nil,0sb?) act as wildcards.a is bis a nominal type check. Equivalent toa.[typename] == b.[typename]
Each type operator also has the negated (a !does b) == !(a does b), (a
!equals b) == !(a equals b), a !case b == !(a case b)
The does performs just name matching when the required tuple is fully named.
It reverts to name and position matching when some of the required tuple entries
are unnamed. Values are ignored by does; use case when the values should be
matched too.
cassert (b=100,a=333,e=40,5) does (a=1,b=3)
cassert (a=100,300,b=333,e=40,5) does (a=1,3)
cassert (b=100,300,a=333,e=40,5) !does (a=1,3)
cassert u32 does u16
cassert u16 does u32
cassert u32 !does string
cassert (100,30) does 30
cassert 30 !does (30,200)
cassert (a=3) !does (30,a=200)
cassert (a=3) !does (a=30,200)
cassert (3) !does (30,a=200)
cassert (3) !does (a=30,200)
A a case b first checks a does b, then checks that every defined value in
b has the same value in a. Undefined values in b (nil, 0sb?) do not
participate in the value check and act as wildcards. This can be used in any
expression but it is quite useful for match ... case patterns.
match (a=1,b=3) {
case (a=1) { cassert true }
else { cassert false }
}
match const t=(a=1,b=3); t {
case (a=1 ,c=4) { cassert false }
case (b=nil,a=1) { cassert t.b==3 and t.a==1 }
else { cassert false }
}
An x = a case b can be translated to:
___0 = a does b
___1 = b in a
x = ___0 and ___1
Reduce and bit selection operators¶
The reduce operators and bit selection share a common syntax
variable#op[sel] where:
-
variableis a tuple where all the tuple fields and subfields must have a explicit type size unless the tuple has 1 entry. -
opis the operation to perform|: or-reduce.&: and-reduce.^: xor-reduce or parity check.+: pop-count.sext: Sign extends selected bits.zext: Zero sign extends selected bits (default option)
-
selcan be a close-range like1..<=4or(1,4,6)or an open range like3... Internally, the open range is converted to a close-range based on the variable size.
The or/and/xor reduce have a single bit signed result (not boolean). This means
that the result can be 0 (0sb0) or -1 (0sb1). pop-count and zext have
always positive results. sext is a sign-extended, so it can be positive or
negative.
If no operator is provided, a zext is used by default. The bit selection without
operator can also be used on the left-hand side to update a set of bits.
The or-reduce and and-reduce are always size insensitive. This means that to perform the reduction it is not needed to know the number of bits. It could pick more or fewer bits and the result is the same. E.g: 0sb111 or 0sb111111 have the same and/or reduce. This is the reason why both can work with open and close ranges.
This is not the case for the xor-reduce and pop-count. These two operations are
size insensitive for positive numbers but sensitive for negative numbers. E.g:
pop-count of 0sb111 is different than 0sb111111. When the variable is negative
a close range must be used. Alternatively, a zext must be used to select
bits accordingly. E.g: variable#[0..=3]#+[..] does a zext and the positive result
is passed to the pop-count. The compiler could infer the size and compute, but
it is considered non-intuitive for programmers.
const x = 0ub1_0110 // positive
const y = 0s1_0110 // negative
cassert x#[0,2] == 0ub10
cassert y#[100,200] == 0ub11 and x#[100,200] == 0
cassert y#sext[0,100,200] == 0sb110 and x#sext[1,100,200] == 0ub001
cassert x#|[..] == -1
cassert x#&[0,1] == 0
cassert x#+[0..=5] == x#+[0..<100] == 3
assert y#+[0..=5] // error: 'y' can be negative
cassert y#[..]#+[..] == 3
cassert y#[0..=5]#+[..] == 3
cassert y#[0..=6]#+[..] == 4
mut z = 0ub0110
z#[0] = 1
cassert z == 0ub0111
z#[0] = 0ub11 // error: '0ub11` overflows the maximum allowed value of `z#[0]`
Note
It is important to remember that in Pyrope all the operations use signed
numbers. This means that an and-reduce over any positive number is always going
to be zero because the most significant bit is zero, E.g: 0xFF#&[..] == 0. In
some cases, a close-range will be needed if the intention is to ignore the sign.
E.g: 0xFF#&[0..<8] == -1.
The bit selection operator only works with ranges, boolean, and integers. It
does not work with tuples or strings. For converting in these object a union:
must be used.
Another important characteristic of the bit selection is that the order of the
bits on the selection does not affect the result. Internally, it is a bitmask
that has no order. For the zext and sext, the same order as the input
variable is respected. This means that var#[1,2] == var#[2,1]. As a result,
the bit selection can not be used to transpose bits. A tuple must be used for
such an operation.
mut v = 0ub10
cassert v#[0,1] == v#[1,2] == v#[..] == v#[0..=1] == v#[..=1] == 0ub10
mut trans = 0
trans#[0] = v#[1]
trans#[1] = v#[0]
cassert trans == 0ub01
Precedence¶
Pyrope has very shallow precedence, unlike most other languages the programmer should explicitly indicate the precedence. The exception is for widely expected precedence.
- Unary operators (not,!,~,?) bind stronger than binary operators (+,++,-,*...)
- Comparators can be chained (a<=c<=d) same as (a<=c and c<=d)
- mult/div precedence is only against +,- operators.
- Parenthesis can be avoided when a expression left-to-right has the same result as right-to-left.
| Priority | Category | Main operators in category |
|---|---|---|
| 1 | unary | not ! ~ ? |
| 2 | mult/div | *, / |
| 3 | other binary | ..,^, &, -,+, ++, <<, >>, in, does, has, case, equals, to |
| 4 | comparators | <, <=, ==, !=, >=, > |
| 5 | logical | and, or, implies |
assert((x or !y) == (x or (!y)) == (x or not y))
assert((3*5+5) == ((3*5) + 5) == 3*5 + 5)
a = x1 or x2==x3 // same as b = x1 or (x2==x3)
b = 3 & 4 * 4 // error: use parenthesis for explicit precedence
c = 3
& 4 * 4
& 5 + 3 // error: use parenthesis for explicit precedence
c2 = 3
& (4 * 4)
& (5 + 3) // OK
d = 3 + 3 - 5 // OK, same result right-left
e = 1
| 5
& 6 // error: use parenthesis for explicit precedence
f = (1 & 4)
| (1 + 5)
| 1
g = 1 + 3
* 1 + 2
+ 5 // OK, but not nice
g1= 1 + (3 * 1)
+ 2
+ 5 // OK
g2= (1 + 3)
* (1 + 2)
+ 5 // OK
h = x or y and z// error: use parenthesis for explicit precedence
i = a == 3 <= b == d
assert i == (a==3 and 3<=b and b == d)
Comparators can be chained, but only when they follow the same type or the direction is the same.
assert a <= b <= c // same as a<=b and b<=c
assert a < b <= c // same as a< b and b<=c
assert a == b <= c // error: chained only allowed with same comparator
assert a <= b > c // error: not same direction
Optional¶
The ? is used by several languages to handle optional or null pointer
references. In non-hardware languages, ? is used to check if there is valid
data or a null pointer. This is the same as checking the .[valid] attribute
with a more friendly syntax.
Pyrope does not have null pointers or memory associated management. Pyrope uses
? to handle .[valid] data. Instead, the data is left to behave without the
optional, but there is a new "valid" field associated with each tuple entry.
Notice that it is not for each tuple level but each tuple entry.
There are 4 explicitly interact with valids:
-
tup.f1?reads the valid for fieldf1from tupletup -
tup?.f1.f2returns0ubs0if tuple fieldsf1orf2are invalid -
tup.f1? = condexplicitly sets the fieldf1valid tocond -
a = b op cvariableawill be valid ifbANDcare valid
The optional or valid attached to each variable and tuple field is implicitly computed as follows:
-
Non-register variables are initialized with valid unless
_is used in the initialization which explicitly clears the valid attribute. -
Registers set the valid after reset, but if the reset clears the valid, there is not guaranteed on attribute
[valid]during reset. If the register does not have a reset signal, the register is always valid unless explicitly cleared. -
Left-hand side variables
validsare set to the and-gate of all the variable valids used in the expression -
memory/arrays do not tend to have reset signals. As such they are always valid unless the memory has explicit reset code. In which case the valid behaves like in flops.
-
Writing to a register updates the register valid based on the din valid, or when the attribute
[valid]is explicitly managed. -
conditionals (
if) update valids independently for each path -
A tuple field has the valid set to false if any of the tuple fields is invalid
-
The valid computation can be overwritten with the
[valid]attribute. This is possible even during reset.
Observation
The variable valid calculation is similar to the Elastic 'output_written' from Liam but it is not an elastic update because it does not consider the abort or retry.
The previous rules will clear a valid only if an expression has no valid, but the only way to have a non-valid is if the inputs to the lambda are invalid or if the valid is explicitly clear. The rules are designed to have no overhead when valid are not used. The compiler should detect that the valid is true all the time, and the associated logic is removed.
Most statements evaluate independent of the valid expression. Expressions will
evaluate the same if any of the inputs is valid or invalid. The valid attribute
is computed in parallel to avoid being in the critical path. The exception are
the verification statements like asserts and printing statatements like puts.
These statements are gated or not performed if any of the inputs is invalid. To
ignore the valid check, the always command can be appended before and as a
result the statments will evaluate every cycle independent of the reset/valid
status.
mut v1:u32 = ? // v1 is zero every cycle AND not valid
assert v1.[valid] == false
mut v2:u32 = 0 // v2 is zero every cycle AND valid
assert v2.[valid] == true
cassert v1?
cassert not v2?
assert v1 == 0 and v2 == 3 // data still same as usual
v1 = 0sb? // OK, poison data
v2 = 0sb? // OK, poison data, and update valid
assert v2? // valid even though data is not
assert v1 != 0 // usual verilog x logic
assert v2 != 0 // usual verilog x logic
const res1 = v1 + 0 // valid with just unknown 0sb? data
const res2 = v2 + 0 // valid with just unknown 0sb? data
assert res1?
assert res2?
reg counter:u32 = 0
always assert counter.reset implies !counter?
valid can be overwritten by the setter method:
const custom = (
,mut data:i16 = nil
,comb setter(ref self, v) {
self.data = v
self.[valid] = v != 33
}
)
mut x:custom = nil
cassert x?
x.data = 33
cassert not x?
x.data = 100
cassert x?
The contents of the tuple field do not affect the field valid bit. It is data-independent. Tuples also can have an optional type, which behaves like adding optional to each of the tuple fields.
const complex = (
,reg v1:string = "foo"
,mut v2:string = nil
,comb setter(ref self, v) {
self.v1 = v
self.v2 = v
}
)
mut x1:complex = ?
mut x2:complex:[valid=false] = 0 // toggle valid, and set zero
mut x3:complex = 0
x3.[valid] = false // set invalid
assert x1.v1 == "" and x1.v2 == ""
assert not x2? and not x2.v1? and not v2.v2?
assert x2.v1 == "" and x2.v2 == ""
assert x2?.v1 == "" and x2?.v1 != "" // any comparison is false
// When x2? is false, any x2?.foo returns 0sb? with the associated x rules
x2.v2 = "hello" // direct access still OK
assert not x2? and x2.v1 == "" and x2.v2 == "hello"
x2 = "world"
assert x2? and x2?.v1 == "world" and x2.v1 == "world"
Variable initialization¶
Variable initialization indicates the default value set every cycle and the
optional (.[valid] attribute).
The const and mut statements require an explicit initialization value for
each cycle. There are exactly two ways to produce an undefined value:
nil— the variable is invalid (.[valid]==false). Reading it is an assertion error at simulation and a compile error at elaboration wherever the compiler can prove the read. Usenilwhen there is no meaningful value yet.0sb?(and related bit-literal forms like0ub101?,0ub??10) — unknown bits, behaving like Verilogx. The variable is still valid from the optional standpoint; only the bits are unknown. Use this for don't-care states or deliberately unobserved bits.
The bare _ sink and the bare ? shorthand have been removed in favor of
these two explicit values. Every initialization must supply a concrete
expression — a literal (0, false, "", 0sb?), nil, or a normal
expression.
mut a:int = 0
cassert a==0 and a.[valid] and a?
mut b:int = nil
cassert b==nil and b.[valid] == false and not b?
b = 0
cassert b==0 and b.[valid] and b?
mut d:[] = () // empty tuple literal
cassert d != nil and d.[valid]
mut e:int = 0sb? // valid but with unknown bits
cassert e.[valid] and e != 0 // any comparison against `?` is unknown
The same rules apply when a tuple or a type is declared. Tuple fields must also use explicit initial values:
const a = "foo"
mut at1 = (
,const a:string = a // copy enclosing 'a' as the initial value
)
cassert at1.a == "foo"
mut at2 = (
,mut a:string = nil // invalid field
)
cassert at2.a.[valid] == false
at2.a = "torrellas"
cassert at2.a == "torrellas" and at2[0] == "torrellas"
Conditional paths affect variable initialization and values. If all the conditional paths assign a value, the valid will be true. If only one path assigns a value, the valid will be set only on that path, but the data may always have the path.
mut x:int = nil
mut y:int = 2
mut z:int = nil
if rand {
x = 3
y = 4
z = 5
}else{
z = 6
}
assert rand implies x.[valid]
assert x.[valid] implies rand
assert y.[valid]
assert rand implies y == 4
assert !rand implies y == 2
assert z.[valid]
assert rand implies z == 5
assert !rand implies z == 6
For structured bindings where one of the return values is unused, name the variable and treat the name as the documentation:
comb weird_pick_bits(b:u32) -> (x:u1, unused:u4) {
(x=b#[2..<3], unused=b#[5])
}
comb fcall_returns_2_values() -> (xx, yy) {
xx = 3
yy = 7
}
const (a, b_unused) = fcall_returns_2_values()
cassert a == 3