Pyrope Standard Library¶
Pyrope has two layers of library code: the built-in std namespace, which is
part of the language, and the import("prp") library, which is still a
wish-list.
Built-in std namespace¶
std is a built-in namespace. Every file sees it without an import, and it
is not a file on the import path: import("prp") below is a different, much
larger library.
std.clog2(x) returns the number of bits needed to index x entries, with
Verilog $clog2 semantics: the smallest n such that (1 << n) >= x. It is
comptime only; x <= 0 or a runtime x is a compile error.
cassert(std.clog2(1) == 0)
cassert(std.clog2(13) == 4)
cassert(std.clog2(16) == 4)
cassert(std.clog2(17) == 5)
mod pick<N=4>(v:Unsigned(bits=N), sel:Unsigned(bits=std.clog2(N))) -> (b:U1@[0]) {
b = v#[sel]
}
std.clog2(N) is the width of an index into N entries; N.[bits] is the
width of the value N (with comptime const N = 16, N.[bits] == 5 but
std.clog2(N) == 4). See Bitwidth.
Memory image preload¶
std.readmemh(file) and std.readmemb(file) initialize a registered memory
once when a simulation starts:
mod program(addr:U8) -> (instruction:U32@[]) {
reg code:[256]U32 = std.readmemh("program.hex")
instruction = code[addr]
}
Declare a sized element type (U32 above). The filename is one positional
comptime string; relative paths resolve against the source file containing the
call. A literal or a comptime const is accepted. The call supplies startup
contents, not a register reset value: reset does not reload the image. Normal
memory writes still work.
Files contain whitespace-separated hex or binary words, optional underscores,
// or /* ... */ comments, and hexadecimal @address directives in either
format. Entry zero comes first. Words are truncated to the declared width;
short words are zero-extended. X/Z/? digits are unknown bits (the two-state
simulator resolves them using sim.unknown_zero). Unlisted entries retain the
startup fill, currently zero for memories. Missing files, malformed tokens,
unterminated comments, and out-of-range addresses fail simulation explicitly.
The generated simulator opens the file on each fresh simulation initialization, even when compilation and generated C++ are reused. Changing just the image therefore needs no HDL edit or recompile. Checkpoint restore overlays the freshly initialized state; the image must still be available at startup. Exported IR retains the resolved file dependency rather than embedding its contents.
For synthesis, these memories represent externally initialized storage, such as
memory loaded through a scan interface. The image does not become a hardware
ROM constant. LiveHD preserves the native memory boundary, including memories
with no functional RTL writer; exported Verilog gives that boundary a blackbox
attribute and a behavioral simulation model. A downstream implementation must
supply the memory macro/loading mechanism. This builtin does not generate a scan
chain. Native LiveHD formal reasoning uses stable symbolic contents until
ordinary writes; it does not specialize the proof to the image values.
For an independent Yosys check, lgcheck defines
LIVEHD_FORMAL_MEMORY_MODEL while reading the sources. This exposes the emitted
behavioral memory body through its default synthesis black-box boundary. The
same image files must be accessible to both sides. That check elaborates the
supplied image; it is not an image-independent proof of externally loaded
contents. Leave the macro undefined when preserving the synthesis boundary.
Whole-memory bulk update/reset is not supported with file preload.
The native Slang reader lowers unconditional initial $readmemh(file, memory)
and $readmemb calls to the same representation. It supports constant filenames
and local, zero-based, one-dimensional memories with integral elements. SV
filenames retain runtime working-directory semantics. Explicit address bounds,
conditional/timed calls, and mixed initial assignments are rejected. The Yosys
reader has not been updated to this external-initialization contract.
The import("prp") library¶
TBD
The import("prp") standard library is not implemented yet; the rest of
this chapter is the wish-list for it. See
Implementation status.
This is a list of functionality that import("prp") should produce.
Basic operations¶
All the LNAST node have an associated function matching name to simplify the
creation of operations: plus, minus, mult, div, mod, ror...
const prp = import("prp")
cassert(prp.plus(first=1, second=2, third=3) == 6) // three named fields captured in a
Library code:
comb plus(...a:Signed) -> (r:Signed) {
r = 0
for e in a {
r += e
}
}
Array/Tuple operators¶
Size of length¶
Sample use:
const x = (1,2,23)
cassert(prp.len(x) == 3)
Library code:
comb len(x) -> (r) { r = x.[size] }
map¶
Sample use:
const x = (1,2,3)
comb inc(a) -> (r) { r = a + 1 }
cassert(x.map(inc) == (2,3,4))
Library code:
comb map(self, f) -> (r:[]) { // self: the tuple, so `x.map(inc)` is a UFCS call
r = nil
for e in self {
r = (...r, f(e))
}
}
filter¶
Sample use:
comb not_two(a) -> (r) { r = a != 2 }
cassert((1,2,3).filter(not_two) == (1,3))
Library code:
comb filter(self, f) -> (r:[]) {
r = nil
for e in self {
if f(e) {
r = (...r, e)
}
}
}
reduce¶
Sample use:
cassert((1,2,3).reduce(prp.plus) == 6)
Library code:
comb reduce(self, op) -> (res) {
if self.[size] <= 1 {
res = self
return
}
res = self[0]
for i in self[1..] {
res = op(lhs=res, rhs=i) // both arguments are captured, as in `prp.plus`
}
}
Strings¶
String helpers follow the C++23 string_view naming where possible:
const s = "hello"
cassert(s.len() == 5)
cassert(s.find("ll") == 2)
cassert(s.substr(pos=1, count=3) == "ell")
File I/O¶
File access comes from a cpp
import; there is no special grammar. It is debug-only simulation functionality
(used inside test blocks), not synthesizable:
type Fs = ( read_file: comb(path:String) -> (data:String) )
const fs:Fs = cpp("file_io")
test vectors.load {
const cfg = fs.read_file("config.txt")
puts(cfg)
}
Data structures¶
const q = prp.queue.make<T=Signed>(depth=16)
q.push(1)
assert(not q.empty())
const v = q.pop()
Math and utilities¶
assert(prp.math.gcd(a=12, b=18) == 6)
const s = prp.str.join(parts=("a", "b"), sep=",")
TODO¶
It would be nice to have the same methods (and names) as the c++20 std::views
adaptors so that it is easier for developers to get familiar. E.g: filter,
transform, drop, join, split, reverse, common, counted...
https://en.cppreference.com/w/cpp/ranges
Simulation arguments¶
Test signatures are the preferred interface for named simulation arguments:
test bench.run(cycles:U32=1000000) {
// `cycles` uses the supplied value or this test's default.
}
Run with lhd sim bench.prp bench.run +cycles=10000000, or run the generated
executable with ./drv.bin --test bench.run +cycles=10000000. Simulator controls
keep their -- prefix. --arg name=value on lhd sim and --name=value on the
generated executable remain compatibility aliases; use +name=value in new code.
Explicit reads inside a test use the same supplied arguments:
const verbose = std.testplusarg("verbose")
const cycles = std.valueplusarg(name="cycles", default=1000000)
const firmware = std.valueplusarg(name="firmware", default="program.hex")
Names match exactly and are case sensitive. The first occurrence wins. A bare
+verbose is present; +file= supplies an empty string. Signature defaults and
accessor defaults are local: neither makes std.testplusarg return true.
std.valueplusarg(name="cycles") requires a numeric value; a missing value is an error.
Both arguments are named: default may also be a String, so an unnamed
std.valueplusarg("cycles", default=1) is ambiguous.
A string default selects string parsing. Numeric access currently uses signed
64-bit integers; sized numeric signature parameters additionally check their
range. Bool signature parameters accept bare flags or true, false, 1,
and 0. Strings can also be signature parameters with a string default.
Result rows record ordered supplied arguments and resolved parameters.
Argument changes participate in A/B workload identity; checkpoint replay rejects
changed arguments and checkpoints lacking argument metadata.
Pyrope accessors currently require a test block. General Pyrope module debug
execution remains pending; argument-dependent hardware is rejected.
Slang also supports $test$plusargs and $value$plusargs in untimed, simulation-only
initial blocks. These use SV prefix matching and the first matching supplied
argument. $value$plusargs returns success and updates its destination on a match;
a miss returns zero and leaves the destination unchanged. Supported conversions
are %d, %b, %h/%x, %o, and %s, with integral destinations up to 64 bits
or strings. The runtime is two-state.
The block can contain private variables, blocking assignments, basic arithmetic
and comparisons, conditionals, immediate assertions, and literal-format
$display/$write/$fatal calls. Debug variables must belong to one block and
cannot be ports or referenced by hardware, including through hierarchy. Put reads
inside the initial block, rather than module-scope declaration initializers.
Timed processes, real or wider destinations, user subroutine calls, and other
unsupported constructs produce diagnostics.
Initialization survives saved LNAST/LGraph and runs once per elaborated instance
at simulation startup. Emitted SV preserves it under synthesis translate_off.
Pyrope source emission of these SV blocks is explicitly rejected until module
debug emission exists; use ln: or lg: imports to preserve them.