Skip to main content

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.

Performance Tips

  1. Pre-compute lengths: Call MC_43.LEN() once outside the loop, not every iteration
  2. Use loop(N) bounds tightly: loop(1000) when you only need 10 iterations wastes cycles
  3. Prefer iterative over recursive: Each recursive call saves all variables (~3592 in brikc.pcd)
  4. Use array.push + join over string concat: O(N) instead of O(N²)
  5. Use multi-level division for number→string: divmod10_safe with 10000→1000→100→10 peeling instead of naive loop(20000)

Compilation Target Tips

TargetBest ForNotes
Native x86-64Production CLIs, policy circuitsStandalone ELF, no dependencies
BIRDevelopment, debugging, bootstrapInterpreted, slower but inspectable
RustIntegration into Rust projectsIdiomatic output with ownership
JavaScriptBrowser/Node.js deploymentES module, tree-shakeable
PythonData science integrationPure Python, type-annotated
WASMSandboxed executionNo filesystem by default