Skip to content

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 SIZE = 16          // Compile-time constant (shorthand for comptime const)
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)
LLM Pitfall: 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)
flow alu(in1, in2) -> (out) { /* explicit timing */ }  // Dataflow with timing
LLM Pitfall: comb/pipe/flow are NOT just function modifiers - they define hardware implementation strategy. Small Pyrope does not support function capture variables; pass values as arguments.

3. Bit Selection Syntax

mut value = 0b1010_1100
mut bits = value#[3..=6]        // Extract bits 3-6 (NOT array indexing)
value#[3] = 0                   // Set bit 3 (NOT array assignment)
LLM Pitfall: #[...] is bit selection, NOT array/hash access. Use [...] for array indexing. Literal Pitfall: _ is only a digit separator (12_34__ == 1234), while ? is a don't-care/unknown bit in binary literals. Use ? for default/uninitialized values (e.g., mut x = ?).

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
assert point.x == 10            // Named access
assert 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)
LLM Pitfall: Three different range operators with different semantics. ..+ 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
Attributes are set only at declaration with :[attr=value] and are immutable afterwards. Use ::[attr] to read attribute values. Check by comparing: foo::[attr] == value. For one-off overflow, use typecast syntax: (expr):Type:[wrap=true].

7. Assignment Operators in Hardware Context

reg counter = 0
counter += 1                    // Immediate update
counter@[] += 1                // Deferred to end of cycle
LLM Pitfall: Register updates can be immediate or deferred. Use @[0] for current value, @[] for end-of-cycle defer, @[-1] for previous cycle. @[1] (next cycle) is only allowed in debug contexts like assert.

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
LLM Pitfall: Memory attributes go AFTER the type, using :[...] syntax.

Hardware-Specific Semantics

Cycle-Based Execution

  • step advances simulation by one clock cycle
  • Register updates happen at cycle boundaries
  • Combinational logic (mut) updates immediately
  • pipe is a Moore machine (outputs always registered), may use reg for internal storage
  • mod has no constraints on registers or outputs
  • flow has three timing mechanisms: delay[N] (operation latency), var@[N] (use at cycle N), :@[N] (timing type check on LHS)

No Runtime Loops

// This is COMPILE-TIME elaboration, not runtime loop
for i in 0..=7 {
    memory[i] = init_value
}
LLM Pitfall: Loops must be compile-time bounded and are unrolled, not runtime constructs.

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

  1. Don't use familiar keywords incorrectly:
  2. class doesn't exist - use tuples
  3. function/def/fn doesn't exist - use comb/pipe/flow/mod
  4. while/for loop bounds must be comptime (unrolled at elaboration)
  5. % (modulo) is debug-only (too expensive for single-cycle hardware)

  6. Don't assume array-like syntax everywhere:

  7. arr#[i] for bit selection
  8. arr[i] for element access
  9. tuple.field or tuple.0 for tuple access

  10. Don't ignore storage classes:

  11. Always use const/mut/reg appropriately
  12. comptime is a prefix modifier, not a storage class (comptime mut is valid)
  13. Understand hardware implications

  14. Don't forget hardware timing:

  15. Use step in tests
  16. Understand register vs. wire behavior

  17. Don't apply software intuitions to ref:

  18. In hardware, all signals are wires — there is no copy cost
  19. ref exists for semantic reasons (allowing mutation), not performance
  20. comb(ref self) is valid and still purely combinational — ref is just an implicit output
  21. Consider cycle boundaries

  22. Don't use mainstream patterns:

  23. No classes, inheritance, or OOP
  24. No runtime dynamic allocation
  25. No exception handling

Quick Reference for Common Patterns

Variable Declaration

comptime PI = 3.14              // Compile-time constant
mut temp = calculation()        // Combinational
reg accumulator = 0             // Persistent register

Function Definition

comb pure_function(x:u8) -> (y:u8) { y = x + 1 }
pipe stateful_function() -> (reg counter:u8) { counter += 1 }

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.