PCD Patterns & Best Practices
This guide covers the patterns that experienced PCD developers use, and the anti-patterns that cause bugs and certification failures.
Pattern 1: The CLI Dispatch Pattern
The canonical way to build a command-line tool in PCD:
PC my_tool {
let cmd = MC_63.ENV("ARGV_1");
if (cmd == "build") {
// ... build logic ...
OUTPUT "OK";
}
if (cmd == "check") {
// ... check logic ...
OUTPUT "OK";
}
if (cmd == "help") {
let _ = MC_58.WRITE("Usage: my_tool [build|check|help]\n");
OUTPUT 0;
}
// Fallback: always reached if no command matches
let _ = MC_58.WRITE("Unknown command: " + cmd + "\n");
OUTPUT 1;
}
Why it works: Each if block ends with OUTPUT, so the CMF sees all paths as terminated. The final fallback catches everything else. Φ_c = 1 guaranteed.
Pattern 2: Bounded Iteration (loop(N) + guard)
Always use loop(N) instead of while:
// PATTERN: scan until condition, with bounded iteration
fn find_char(text, target) {
let tlen = MC_43.LEN(text);
loop(tlen) as i {
let ch = MC_45.CHAR_AT(text, i);
if (ch == target) {
return i;
}
}
return -1;
}
For early termination with state:
fn scan_until_space(text, start) {
let tlen = MC_43.LEN(text);
let pos = start;
loop(tlen) as _i {
if (pos < tlen) {
let ch = MC_45.CHAR_AT(text, pos);
if (ch == " ") {
return pos;
}
let pos = pos + 1;
}
}
return tlen;
}
Pattern 3: Accumulator with Loop-Carried Variables
fn sum_array(arr) {
let n = MC_43.LEN(arr);
let total = 0;
loop(n) as i {
let elem = arr[i];
let total = total + elem; // same name = loop-carried
}
return total;
}
Critical rule: The rebinding let total = total + elem must use the exact same variable name as the outer binding. If you accidentally use a different name, the outer variable won’t update.
// BAD: typo creates new variable, `total` stays 0
let total = 0;
loop(n) as i {
let totl = total + arr[i]; // "totl" ≠ "total" → new slot!
}
// total is still 0!
Pattern 4: Builder Pattern with Structs
struct Config {
host: string,
port: i64,
timeout: i64,
verbose: i64
}
fn default_config() {
return Config {
host: "localhost",
port: 8080,
timeout: 30,
verbose: 0
};
}
fn with_host(cfg, h) {
return Config {
host: h,
port: cfg.port,
timeout: cfg.timeout,
verbose: cfg.verbose
};
}
fn with_port(cfg, p) {
return Config {
host: cfg.host,
port: p,
timeout: cfg.timeout,
verbose: cfg.verbose
};
}
Since structs are values (immutable), each with_* function returns a new struct. This is the idiomatic PCD way to configure objects.
Pattern 5: Policy Circuit Completeness
Every policy circuit must cover all possible inputs. Use a final fallback:
// GOOD: exhaustive — every input produces a result
fn policy(action_type, resource) {
if (action_type == "write") {
if (starts_with(resource, "/tmp/")) {
return "ALLOW";
}
return "BLOCK";
}
if (action_type == "read") {
return "ALLOW";
}
// CRITICAL: fallback for any unknown action type
return "BLOCK";
}
// BAD: what if action_type is "execute"?
fn policy(action_type, resource) {
if (action_type == "write") {
return "BLOCK";
}
if (action_type == "read") {
return "ALLOW";
}
// No fallback! Φ_c < 1
}
Pattern 6: Error Propagation with try/catch
fn safe_process(path) {
try {
let data = MC_56.READ(path);
let processed = transform(data);
return processed;
} catch (err) {
return "ERROR: " + err;
}
}
For nested operations:
fn pipeline(input_path, output_path) {
try {
let raw = MC_56.READ(input_path);
let parsed = parse(raw);
let result = compute(parsed);
MC_57.WRITE_FILE(output_path, stringify(result));
return "OK";
} catch (err) {
return "FAILED: " + err;
}
}
Pattern 7: Map/Filter/Reduce over Arrays
Use stdlib closures for functional array processing:
import "stdlib/array.pcd";
fn process_scores(scores) {
// Filter passing scores
let passing = filter(scores, fn(s) { s >= 60 });
// Scale by 1.1 (using integer math: multiply by 11, divide by 10)
let scaled = map(passing, fn(s) {
let (q, _r) = MC_03.DIV8(s * 11, 10);
return q;
});
// Sum
let total = reduce(scaled, 0, fn(acc, s) { acc + s });
return total;
}
Anti-Pattern 1: Using while Instead of loop(N)
// ❌ AVOID: while has SSA bug
let pos = 0;
while (pos < max) {
let pos = pos + 1;
}
// ✅ USE: loop(N) is correct and deterministic
let pos = 0;
loop(max) as _i {
if (pos < max) {
let pos = pos + 1;
}
}
Anti-Pattern 2: Recursive Functions for Large Data
// ❌ AVOID: recursion hits MAX_DEPTH=256 and is slow
fn sum_recursive(arr, idx) {
if (idx >= len(arr)) { return 0; }
return arr[idx] + sum_recursive(arr, idx + 1);
}
// ✅ USE: iterative loop
fn sum_iterative(arr) {
let n = len(arr);
let total = 0;
loop(n) as i {
let total = total + arr[i];
}
return total;
}
Recursion in PCD is limited to 256 depth and incurs high overhead (all ~3592 vars saved per call frame in BIR). Use loop(N) for data processing.
Anti-Pattern 3: String Building in Tight Loops
// ❌ AVOID: O(N²) — each concat creates a new string
let result = "";
loop(1000) as i {
let result = result + "x"; // copies entire string each time
}
// ✅ USE: array accumulation + join
let parts = [];
loop(1000) as i {
let parts = push(parts, "x");
}
let result = join(parts, "");
Anti-Pattern 4: Not Destructuring DIV8
// ❌ WRONG: result is a Tuple, not an integer
let result = MC_03.DIV8(10, 3);
let doubled = result * 2; // TYPE ERROR
// ✅ CORRECT: always destructure
let (q, r) = MC_03.DIV8(10, 3);
let doubled = q * 2;
Anti-Pattern 5: Missing Fallback in Match
// ❌ BAD: what if cmd is "delete"?
let action = match cmd {
"add" => do_add(),
"sub" => do_sub()
};
// ✅ GOOD: always include wildcard
let action = match cmd {
"add" => do_add(),
"sub" => do_sub(),
_ => "unknown"
};
Anti-Pattern 6: Using return (X == Y) in Functions Called with == 0
// ❌ SUBTLE BUG: comparison result may not behave as expected
fn is_match(a, b) {
return (a == b); // BIR may not produce 0/1 as expected
}
if (is_match(x, y) == 0) { ... }
// ✅ SAFE: explicit 0/1 returns
fn is_match(a, b) {
if (a == b) {
return 1;
}
return 0;
}
This is a known PCD pattern requirement. Always use explicit if/return for boolean-returning functions instead of return (X == Y). The BIR representation of comparison results is not guaranteed to be exactly 0 or 1 in all contexts.
- Pre-compute lengths: Call
MC_43.LEN() once outside the loop, not every iteration
- Use
loop(N) bounds tightly: loop(1000) when you only need 10 iterations wastes cycles
- Prefer iterative over recursive: Each recursive call saves all variables (~3592 in brikc.pcd)
- Use array.push + join over string concat: O(N) instead of O(N²)
- Use multi-level division for number→string:
divmod10_safe with 10000→1000→100→10 peeling instead of naive loop(20000)
Compilation Target Tips
| Target | Best For | Notes |
|---|
| Native x86-64 | Production CLIs, policy circuits | Standalone ELF, no dependencies |
| BIR | Development, debugging, bootstrap | Interpreted, slower but inspectable |
| Rust | Integration into Rust projects | Idiomatic output with ownership |
| JavaScript | Browser/Node.js deployment | ES module, tree-shakeable |
| Python | Data science integration | Pure Python, type-annotated |
| WASM | Sandboxed execution | No filesystem by default |