Skip to content

Basic syntax

Comments

Comments begin with //, there are no multi-line comments

// comment
a = 3 // another comment

Constants

Integers

Pyrope has unlimited precision signed integers. Any literal starting with a digit is a likely integer constant.

0xF_a_0 // 4000 in hexa. Underscores have no meaning
0b1100  // 12 in binary
0sb1110 // -2 in binary (sb signed binary)
33      // 33 in decimal
0o111   // 73 in octal
0111    // 111 in decimal (some languages use octal here)

Since powers of two are very common, Pyrope decimal integers can use the K, M, G, and T modifiers.

assert 1K == 1024
assert 1M == 1024*1024
assert 1G == 1024*1024*1024
assert 1T == 1024*1024*1024*1024

Several hardware languages support unknown bits (?) or high-impedance (z). Pyrope aims at being compatible with synthesizable Verilog, as such ? is also supported in the binary encoding.

0b?             // 0 or 1 in decimal
0sb?            // 0 or -1 in decimal
0b?0            // 0 or 2 in decimal
0sb0?0          // 0 or 2 in decimal

The Verilog high impedance z is not supported. An explicit bus construct must be used instead.

Like in many HDLs, Pyrope has unknowns ?. The x-propagation is a source of complexity in most hardware models. Pyrope has x or ? to be compatible with Verilog existing designs. This means that inside the Pyrope compiler, the constant operations with unknowns are compatible with Verilog semantics. When the simulation is performed, the expectation is to randomly generate a 0 or 1 for each unknown (?) bit.

The advice is not to use ? besides match statement pattern matching. It is less error prone to use the default value (zero or empty string), but sometimes it is easier to use nil when converting Verilog code to Pyrope code. The nil means that the numeric value is invalid. If any operation is performed with nil, the result is an assertion failure. The only thing allowed to do with nil is to copy it. While the nil behaves like an invalid value, the 0sb? behaves like an unknown value that still can be used in arithmetic operations. E.g: 0sb? | 1 is 1 but nil | 1 is an assertion error.

Notice that nil is a state in the integer basic type, it is not a new type by itself, it does not represent an invalid pointer, but rather an invalid integer. Also important is that the compiler will guarantee that all the nil arithmetic or decision uses are eliminated at compile time or a compile error is generated.

Strings

Pyrope accepts single line strings with a single quote (') or double quote ("). Single quote does not have escape character, double quote supports escape sequences.

a = "hello \n newline"
b = 'simpler here'
  • \n: newline
  • \\: backslash
  • \": double quote
  • `: backtick quote
  • \xNN: hexadecimal 8 bit character (2 digits)
  • \uNNNN: hexadecimal 16-bit Unicode character UTF-8 encoded (4 digits)

Pyrope allows string interpolation only when double quote is used ("bla {expression:format_style} bla"). The format style is like C++23 std::format.

let num       = 2
let color     = "blue"
let extension = "s"

let txt1 = "I have {num:d} {color} potato{extension}"
let txt2 = string("I have {:d} {} potato{}", num, color, extension)
cassert txt1 == txt2 == "I have 2 blue potatos"

let txt3 = 'I have {num}'         // single quote does not do interpolation
cassert txt3 == "I have \{num\}"  // \{ escapes the interpolation

Integers and strings can be converted back and forth:

var a:string = "127"
var b:int = a        // same as var b = int(a)
var c:string = b     // same as var c = string(b)
assert a == c
assert b == 0x7F
assert a == b        // compile error, 'a' and 'b' have different types

Newlines and spaces

Spaces do not have meaning but new lines do. Several programming languages like Python use indentation level (spaces) to know the parsing meaning of expressions. In Pyrope, spaces do not have meaning, and newlines combined with the first token after newline is enough to decide the end of statement.

By looking at the first character after a new line and the last one the previous line, it is possible to know if the rest of the line belongs to the previous statement or it is a new statement.

If the line starts with an alphanumeric ([a-z0-9] that excludes operators like or, and) value or an open parenthesis ((), the rest of the line belongs to a new statement.

var (a,b,c,d) = _
a = 1
  + 3           // 1st stmt
(b,c) = (1,3)   // 2nd stmt
cassert a == 4 and b == 1 and c == 3

d = 1 +         // OK, but not formatted to style
    3

This functionality allows parallelizing the parsing and elaboration in Pyrope. More important, it makes the code more readable, by looking at the beginning of the line, it is possible to know if it is a new statement or a continuation of the last one. It also helps to standardize the code format by allowing only one style.

Identifiers

An identifier is any non-reserved keyword that starts with an underscore or an alphabetic character. Since Pyrope is designer to support any synthesizable Verilog automatic translation, any sequence of characters between backticks (`) can form a valid identifier. The identifier uses the same escape sequence as strings.

`foo is . strange!\nidentifier` = 4
`for` = 3
cassert `for`+1 == `foo is . strange!\nidentifier`

Using the backtick, Pyrope can use any string as an identifier, even reserved keywords. Identifiers are case sensitive like Verilog, but the compiler issues errors for non ` escaped identifiers that do not follow these conditions in order:

  • Identifiers with a single character followed by a number can be upper or lower case.
  • An all upper case variable must be a compile time constant comptime.
  • Types should either: (1) start the first character uppercase and everything else lower case; (2) be all lower case and finish with _t.
  • All the other identifiers that start with an alpha character [a-z] are always lower case.

Semicolons

Semicolons are not needed to separate statements. In Pyrope, a semicolon (;) has the same meaning as a newline. Sometimes it is possible to add semicolons to separate statements. Since newlines affect the meaning of the program, a semicolon can do too.

a = 1 ; b = 2

Printing and debugging

Printing messages is useful for debugging. puts prints a message and the string is formatted using the c++23 std::format. There is an implicit newline printed. The same without a newline can be achieved with print.

a = 1
puts "Hello a is {a}"

Pyrope does string interpolation, and it has attributes to access line of code and file name. Since tracing or debugging variables is quite common, the dbg statement behaves like puts and also prints the line of code and file name for easier tracing.

a = 1

puts "{}:{} a:{} tracing a", a::[file], a::[loc], a
puts "{a::[file]}:{a::[loc]} a:{a} tracing a"        // Same

The previous statements print "foo:3 a:1 tracing a" in the 3 cases. The line of code corresponds to the latest update of variable, not the dbg statement.

Since many modules can print at the same cycle, it is possible to put a relative priority between puts (priority). If no relative priority is provided, a default 0 priority is provided. Messages are kept to the end of the cycle, and then printed in alphabetical order for a given priority. This is done to be deterministic. Higher priority (higher value) are printed after lower priority. Messages generated by assertions also get serialized like puts statements but have the highest priority.

To avoid breaking down different puts inside the same method. All the puts in a given cycle are shown together.

This example will print "hello world" even though there are 2 puts/prints in different files.

// src/file1.prp
puts(priority=2, " world")

// src/file2.prp
print(priority=1, "hello")

The available puts/print arguments: * priority: relative order to print in a given cycle. * file: file to send the message. E.g: stdout, stderr, my_large.log,...

A related command to the puts is the format it behaves like print but returns a string.

puts/print are a bit special. In most languages, IO operations like puts are considered to have side-effects. In Pyrope, the puts can not modify the behavior of the synthesized code and it is considered a non-side-effect lambda call. This allows to have puts calls in functions.

Lambda or Routines

Pyrope only supports anonymous lambdas, but the lambdas can have attributes that restrict the lambda functionality to combinational only (comb or fun), pipeline stages that have all the outputs with the same dela (pipe), or lambdas that connect multiple combinational or pipeline stages but require explicit timing use (flow) to connect operations. Lambda section has more details on the allowed syntax.

var f = fun(a, b) { a + b }

Pyrope naming for consistency:

  • fun or comb is pure combinational logic (zero cycles)

  • pipe[N] is a fixed N-cycle pipeline

  • pipe[A..=B] is a flexible A-to-B cycle pipeline

  • async is a reserved keyword for future asynchronous pipeline stages.

  • flow is a module with arbitrary internal pipelining, but mostly connecting blocks, no combinational logic

  • comb or pipe that uses a self parameter is also called a method

Evaluation order

Statements are evaluated one after another in program order. The main source of conflicts come from expressions.

The expression evaluation order is important if the elements in the expression can have side effects. Pyrope constrains the expressions so that no matter the evaluation order, the synthesis result is the same.

Languages like C++11 do not have a defined order of evaluation for all types of expressions. Calling call1() + call2() is not defined. Either call1() first or call2() first.

In many languages, the evaluation order is defined for logical expressions. This is typically called the short-circuit evaluation. Some languages like Pascal, Rust, Kotlin have different and/or to express conditional evaluation. In Pascal, there is an and/or and and_then/or_else (conditional). In Rust &/| and &&/|| (conditional). In Kotlin &&/|| and and/or (conditional). Pyrope uses has the and/or without short-circuit, and the and_then/or_else with explicit short-circuit.

The programmer can explicitly set an evaluation order by using short-circuit expressions like and_then, or_else, or control expressions (if/else, match, for). An expression can have many function calls because those have no side-effects, and hence the evaluation order is not important.

A pipe can update state internally and has one or more cycle delays. As such, pipe statements can do many calls to fun/comb lambdas, but not to other pipe lambdas. pipe lambdas can only be called inside flow lambdas.

Expressions also can have a code blocks ({ }) as long as there are no side-effects. In a way, expression code blocks can be seen as a type of functions that are called immedialy after definition.

var a = {var d=3 ; d+1} + 100 // OK
assert a == (3+1+100)
assert a == {3+1+100}  // same, expression evaluated as 104 and returned

For most expressions, Pyrope is more restrictive than other languages because it wants to be a fully defined deterministic independent of implementation. To handle logging/messaging in fun calls, Pyrope treats puts as a special instruction. Pyrope runtime delays the puts output until the end of the cycle. See the Printing section above for more details.

To illustrate the evaluation order, it is useful to see a Verilog example. The following Verilog sequence evaluates differently in VCS and Icarus Verilog. Pyrope treats puts and assertion messages in a special way. The reason why some methods may be called is dependent on the optimization (in this case, testing(1) got optimized away by vcs).

module test();

function testing(input [0:3] a);
  begin
    $display("test called with %d",a);
    testing=1;
  end
endfunction

initial begin
  if (0 && testing(1)) begin
    $display("test1");
  end

  if (1 && testing(2)) begin
    $display("test2");
  end

  if (0 || testing(3)) begin
    $display("test3");
  end

  if (1 || testing(4)) begin
    $display("test4");
  end
end
test called with  1
test called with  2
test2
test called with  3
test3
test called with  4
test4
test called with  2
test2
test called with  3
test3
test called with  4
test4
test called with 2
test2
test called with 3
test3
test4

If an order is needed and a function call can have debug side-effects or synthesis side-effects, the statement must be broken down into several statements, or the and_then and or_else operations must be used.

var r1 = mcall1() or  mcall2()  // compile error, non-deterministic


var r2 = mcall1() and mcall2()  // compile error, non-deterministic


var r3 = mcall1() +   mcall2()  // compile error
// compile error only if mcall1/mcall2 can have side effects
var r1 = fcall1()
r1  = fcall2() unless r1

var r2 = fcall1()
r2  = fcall2() when r2

var r3 = fcall1()
r3 += fcall2()
var r1 = fcall1() or_else fcall2()


var r2 = fcall1() and_then fcall2()


var r3 = fcall1()
r3 += fcall2()

Basic gates

Pyrope allows a low level or structural direct basic gate instantiation. There are some basic gates to which to which the compiler translates Pyrope code to. These basic gates are also directly accesible:

  • __sum for addition and substraction gate.
  • __mult for multiplication gate.
  • __div for divisions gate.
  • __and for bitwise and gate
  • __or for bitwise or gate
  • __xor for bitwise xor gate
  • __ror for bitwise reduce-or gate
  • __not for bitwise not gate
  • __get_mask for extrating bits using a mask gate
  • __set_mask for replacing bits using a mask gate
  • __sext for sign-extension gate
  • __lt for less-than comparison gate
  • __ge for greater-equal comparison gate
  • __eq for equal comparison gate
  • __shl for shift left logical gate
  • __sra for shift right arithmetic gate
  • __lut for Look-Up-Table gate
  • __mux for a priority multiplexer
  • __hotmux for a one-hot excoded multiplexer
  • __memory for a memory gate
  • __flop for a flop gate
  • __latch for a latch gate

Each of the basic gates operate always over signed integers like Pyrope, but their semantics vary. A more detailed explanation is available at LiveHD cell type section.

Pyrope does not have a "mod" operator. The semantics for "mod" with signed integers are different in languages like Python and C. It is a complex operator to provide hardware support.

Initialization

Each variable declaration (var or let) must have an assigned value. The type default value is _ (0 integer, "" string, false boolean, nil otherwise)

a  = 3        // compile error, no previous let or var

var b = 3
b  = 5        // OK
b += 1        // OK

let cu3 = if runtime { 3 }else{ 5 }

let d = "hello"  // OK
d = "bar"        // compile error, 'd' is immutable
var d = "bar"    // compile error, 'd' already declared

var e = _        // OK, no type or default value, just scope declaration
e:u32 = 33       // OK

var Foo = 33     // compiler error, 'let Foo = 33'
Foo  = 33        // compiler error, `Foo` already declared as immutable

When the variable is a tuple or a range style, the default initialization is nil. 0sb? can not be applied to ranges or tuples value because it is restricted for integers. nil should be used in those cases.

var tup = nil

if cond::[comptime] {
  tup = (a=1,b=2)
}else{
  tup = (a=1,b:u4=3,c=3)
}

cassert tup.a == 1
cassert cond implies tup.b==2
cassert !cond implies tup.b==3

Variables with first character upper case are comptime. This means that the contents must be known/fix at compilation time.

var A_xxx = something             // comptime
var A_yyy::[comptime] = something // also comptime, redundant but legal