Small Pyrope - Summary¶
This document provides essential guidance for LLMs generating Small Pyrope code, highlighting unique syntax and semantics that differ from mainstream programming languages.
Core Language Identity¶
Small Pyrope is a hardware description language with these fundamental characteristics:
-
Everything is a tuple - the core data structure
-
Structural typing only - no nominal types in Small Pyrope
-
Compile-time elaboration for all control flow
-
Explicit timing model with cycles for hardware simulation
Critical Syntax Differences from Mainstream Languages¶
1. Storage Classes (NOT variable mutability)¶
comptime const SIZE = 16 // Compile-time constant
comptime mut counter = 0 // Mutable at compile time (elaboration)
const my_constant = 42 // Immutable after assignment (NOT compile-time)
mut my_wire = 0 // Combinational (no persistence across cycles)
reg my_state = 0 // Register (persistent across cycles)
const means immutable, NOT compile-time. comptime is a prefix modifier (not a storage class) that can be applied to const or mut. comptime alone is shorthand for comptime const. Don't confuse with const/let/var from JavaScript — these represent hardware storage types. Semicolons are optional and behave like a newline.
2. Function Types are Hardware Semantics¶
comb add(a:u8, b:u8) -> (result:u8) { result = a + b } // Combinational logic
pipe[1] counter() -> (reg count:u8) { count += 1 } // Moore machine (1-cycle pipeline)
mod alu(in1, in2) -> (out) { /* explicit timing */ } // Dataflow / orchestration with timing
comb/pipe/mod are NOT just function modifiers — they define hardware implementation strategy. mod is the only kind that can orchestrate pipelined calls (await[N], @[N]). Small Pyrope does not support runtime function captures; pass runtime values as arguments. Visible comptime bindings are lexical.
3. Bit Selection Syntax¶
mut value = 0ub1010_1100
mut bits = value#[3..=6] // Extract bits 3-6 (NOT array indexing)
value#[3] = 0 // Set bit 3 (NOT array assignment)
#[...] is bit selection, NOT array/hash access. Use [...] for array indexing.
Literal Pitfall: _ is only a digit separator in numeric literals (12_34__ == 1234). ? marks don't-care/unknown bits inside a binary literal (e.g., 0ub101?). There is no bare _ sink and no bare ? default — use nil for invalid values and 0sb? for fully-unknown bits.
4. Tuple-Centric Everything¶
mut point = (x=10, y=20) // Named tuple (like struct)
mut array = (1, 2, 3, 4) // Indexed tuple (like array)
mut mixed = (x=1, 2, y=3) // Mixed named/indexed
// Access patterns
cassert point.x == 10 // Named access
cassert array[2] == 3 // Array-style access
5. Ranges with Multiple Operators¶
mut range1 = 1..=5 // Inclusive: 1,2,3,4,5
mut range2 = 0..<4 // Exclusive: 0,1,2,3
mut range3 = 2..+3 // Size-based: 2,3,4 (3 elements starting at 2)
cassert range1 == (1,2,3,4,5)
cassert range2 == (0,1,2,3)
cassert range3 == (2,3,4)
..+ is size-based, not addition.
6. Type Annotations and Attributes¶
mut data:u32:[max=1000, min=0] = 0 // Type with constraints
reg counter:[reset_pin=rst] = 0 // Hardware attributes
cassert counter.[bits] == 8 // Read and check attribute
::[attr=value] (or :Type:[attr=value]) and are immutable afterwards. Use name.[attr] to read attribute values. Check by comparing: foo.[attr] == value. For one-off overflow, use the statement-level prefix: wrap result = a + b or sat result = x + y.
7. Assignment Operators in Hardware Context¶
reg counter = 0
counter += 1 // Immediate update
counter.[defer] += 1 // Deferred to end of cycle
.[defer] is the end-of-cycle value, and
past[n](x) reads n cycles ago. To snapshot 'q' before later in-cycle
updates, copy it into a local (const counter_q = counter).
8. Memory Declaration Syntax¶
reg memory:[256]u32 = 0 // Simple memory
reg dual_port:[1024]u16:[ // Complex memory with attributes
rdport=(0,1),
wrport=(2),
latency=1
] = 0
// Port access uses .port[] for clarity
mut out = ram.port[0][addr]:[rdport=0] // Read port 0
:[...] syntax.
Hardware-Specific Semantics¶
Cycle-Based Execution¶
stepadvances simulation by one clock cycle- Register updates happen at cycle boundaries
- Combinational logic (
mut) updates immediately pipeis a Moore machine (outputs always registered), may useregfor internal storagemodhas no constraints on registers or outputsmodhas two pipeline-timing mechanisms:await[N](declaration modifier that pipelines the whole RHS over N cycles) andfoo@[N](pure timing type check, works on LHS and RHS uses)
No Runtime Loops¶
// This is COMPILE-TIME elaboration, not runtime loop
for i in 0..=7 {
memory[i] = init_value
}
Testing and Assertions¶
assert condition // Runtime assertion (hardware check)
cassert compile_time_expr // Compile-time assertion
test "description" { // Test block with simulation
step // Advance clock cycle
}
Common LLM Mistakes to Avoid¶
- Don't use familiar keywords incorrectly:
classdoesn't exist - use tuplesfunction/def/fndoesn't exist - usecomb/pipe/modwhile/forloop bounds must becomptime(unrolled at elaboration)-
%(modulo) is debug-only (too expensive for single-cycle hardware) -
Don't assume array-like syntax everywhere:
arr#[i]for bit selectionarr[i]for element access-
tuple.fieldortuple.0for tuple access -
Don't ignore storage classes:
- Always use
const/mut/regappropriately comptimeis a prefix modifier, not a storage class (comptime mutis valid)-
Understand hardware implications
-
Don't forget hardware timing:
- Use
stepin tests -
Understand register vs. wire behavior
-
Don't apply software intuitions to
ref: - In hardware, all signals are wires — there is no copy cost
refexists for semantic reasons (allowing mutation), not performancecomb(ref self)is valid and still purely combinational —refis just an implicit output-
Consider cycle boundaries
-
Don't use mainstream patterns:
- No classes, inheritance, or OOP
- No runtime dynamic allocation
- No exception handling
Quick Reference for Common Patterns¶
Variable Declaration¶
comptime const PI = 3 // Compile-time constant (no FP, so no 3.14)
mut temp = calculation() // Combinational
reg accumulator = 0 // Persistent register
Function Definition¶
comb pure_function(x:u8) -> (y:u8) { y = x + 1 }
pipe[1] stateful_function() -> (reg counter:u8) { counter += 1 }
cassert pure_function(5) == 6
Memory Operations¶
reg ram:[64]u32 = 0
ram[addr] = data // Write
mut read_data = ram[addr] // Read
Control Flow¶
if condition { /* ... */ } // Standard conditional
match value { // Pattern matching
case 0 { /* ... */ } // `case` is alias for `==`
== 1 { /* ... */ } // `==` also works
else { /* ... */ }
}
Testing¶
test "my test" {
mut result = my_function(input)
assert result == expected
step // Advance simulation
}
Key Takeaway for LLMs¶
Small Pyrope looks like a software language but has hardware semantics. Every construct maps to actual hardware - registers, wires, memories, and logic gates. Generate code thinking about digital circuits, not software programs.