Skip to main content

Tutorial: Your First PCD Program

This tutorial takes you from zero to a working PCD program in 15 minutes. By the end, you will have:
  • Installed the brikc compiler
  • Written and compiled a PCD program
  • Understood monomers, EVA composition, and Φ_c certification
  • Built a real policy circuit

Step 1: Install brikc

curl -fsSL https://brik64.dev/install | sh
Verify the installation:
brikc --version
# brikc v4.0.0-beta.2 (fixpoint: 7229cfcd...)

brikc check --self
# ✓ Self-compilation fixpoint verified
brikc check --self verifies that the compiler binary matches the known fixpoint hash. If this passes, you have an authentic, unmodified BRIK-64 compiler.

Step 2: Hello World

Create a file called hello.pcd:
PC hello {
    let msg = "Hello, Digital Circuitality!\n";
    let len = MC_43.LEN(msg);
    MC_33.WRITE(1, msg, len);
    OUTPUT 0;
}
What’s happening here:
  • PC hello { ... } — defines a circuit named “hello”
  • MC_43.LEN(msg) — calls monomer 43 (String family: LEN) to get the string length
  • MC_33.WRITE(1, msg, len) — calls monomer 33 (I/O family: WRITE) to write to stdout (fd=1)
  • OUTPUT 0 — the circuit’s terminal value (exit code 0)
Run it:
brikc run hello.pcd
# Hello, Digital Circuitality!
Check its certification:
brikc check hello.pcd
# ✓ Circuit closed: Φ_c = 1.000
# ✓ All monomers verified
# ✓ Ω = 1

Step 3: Understanding Monomers

Monomers are the 64 Core atomic operations (plus 64 Extended) of BRIK-64. Think of them as logic gates — each one does exactly one thing, and each one is formally verified. Every monomer is called with MC_XX.NAME(args):
PC monomer_demo {
    // Arithmetic: MC_00.ADD8 adds two numbers (saturating at 255)
    let sum = MC_00.ADD8(100, 50);       // 150

    // String: MC_40.CONCAT joins strings
    let greeting = MC_40.CONCAT("sum = ", "150");

    // I/O: MC_58.WRITE prints to stdout
    let _ = MC_58.WRITE(greeting + "\n");

    OUTPUT 0;
}
Key rule: Monomers always return Value::I64, except MC_03.DIV8 which returns a tuple:
let (quotient, remainder) = MC_03.DIV8(17, 5);
// quotient = 3, remainder = 2
Forgetting to destructure MC_03.DIV8 is the #1 beginner mistake. Always use let (q, r) = MC_03.DIV8(a, b).

Step 4: Variables, Functions, and Loops

Variables

All variables are immutable. “Updating” a variable creates a new binding:
let x = 10;
let x = x + 1;    // new SSA slot, x is now 11

Functions

fn double(n) {
    return n * 2;
}

fn greet(name) {
    return "Hello, " + name + "!";
}

Loops

Always prefer loop(N) over while:
// Good: bounded, deterministic
let sum = 0;
loop(100) as i {
    let sum = sum + i;
}

// With index variable
loop(10) as i {
    let _ = MC_58.WRITE("iteration " + from_int(i) + "\n");
}
while loops have a known SSA bug in the current planner. Always use loop(N) with an if guard instead:
// Instead of: while (pos < max) { ... }
// Use:
loop(max) as _i {
    if (pos < max) {
        // your logic here
        let pos = pos + 1;
    }
}

Step 5: Using the Standard Library

PCD has a standard library written entirely in PCD:
import "stdlib/math.pcd";
import "stdlib/string.pcd";
import "stdlib/array.pcd";
import "stdlib/fmt.pcd";

PC stdlib_demo {
    // Math
    let x = abs(-42);           // 42
    let m = max(10, 20);        // 20
    let s = sqrt(144);          // 12

    // String
    let parts = split("a,b,c", ",");    // ["a", "b", "c"]
    let upper = upper("hello");          // "HELLO"
    let found = contains("hello world", "world");  // true

    // Array
    let nums = [3, 1, 4, 1, 5];
    let sorted = sort(nums);             // [1, 1, 3, 4, 5]
    let doubled = map(nums, fn(x) { x * 2 });  // [6, 2, 8, 2, 10]

    // Formatting
    let msg = format("Sum={}, Max={}", [sum, m]);

    OUTPUT msg;
}

Step 6: Conditionals and Pattern Matching

if / else

fn classify(n) {
    if (n > 0) {
        return "positive";
    } else if (n < 0) {
        return "negative";
    } else {
        return "zero";
    }
}

match

fn day_name(d) {
    return match d {
        1 => "Monday",
        2 => "Tuesday",
        3 => "Wednesday",
        4 => "Thursday",
        5 => "Friday",
        6 => "Saturday",
        7 => "Sunday",
        _ => "invalid"
    };
}

Step 7: Structs

struct Point {
    x: i64,
    y: i64
}

fn distance_squared(a, b) {
    let dx = a.x - b.x;
    let dy = a.y - b.y;
    return dx * dx + dy * dy;
}

PC struct_demo {
    let p1 = Point { x: 0, y: 0 };
    let p2 = Point { x: 3, y: 4 };
    let d2 = distance_squared(p1, p2);
    // d2 = 25
    OUTPUT d2;
}

Step 8: Error Handling

PC safe_read {
    try {
        let data = MC_56.READ("input.txt");
        let _ = MC_58.WRITE("Read " + from_int(len(data)) + " bytes\n");
        OUTPUT 0;
    } catch (err) {
        let _ = MC_58.WRITE("Error: " + err + "\n");
        OUTPUT 1;
    }
}
try/catch is a statement, not an expression. You cannot write let x = try { ... }. Assign inside the block instead.

Step 9: Build a Real Policy Circuit

This is where PCD shines. A policy circuit intercepts an AI action and returns ALLOW or BLOCK:
import "stdlib/string.pcd";
import "stdlib/json.pcd";
import "stdlib/array.pcd";

PC file_write_policy {
    // Read the proposed action from stdin
    let action_json = MC_56.READ("/dev/stdin");
    let action = parse(action_json);

    let path = get(action, "path");
    let agent = get(action, "agent_id");

    // Rule 1: Only /tmp/ writes allowed
    if (!starts_with(path, "/tmp/")) {
        OUTPUT "BLOCK: writes only allowed to /tmp/";
    }

    // Rule 2: Only known agents
    let known = ["claude-code", "codex-cli"];
    if (!contains(known, agent)) {
        OUTPUT "BLOCK: unknown agent " + agent;
    }

    // Rule 3: No dotfiles
    if (contains(path, "/.")) {
        OUTPUT "BLOCK: dotfile writes forbidden";
    }

    OUTPUT "ALLOW";
}
Compile and test:
# Compile to native binary
brikc build file_write_policy.pcd -o policy

# Test with a valid action
echo '{"path":"/tmp/output.txt","agent_id":"claude-code"}' | ./policy
# ALLOW

# Test with a blocked action
echo '{"path":"/etc/passwd","agent_id":"claude-code"}' | ./policy
# BLOCK: writes only allowed to /tmp/

# Test with unknown agent
echo '{"path":"/tmp/test.txt","agent_id":"rogue-agent"}' | ./policy
# BLOCK: unknown agent rogue-agent
Why Φ_c = 1 matters here: The compiler formally verifies that every possible input to this circuit produces either “ALLOW” or “BLOCK: …”. There is no input that causes undefined behavior, a crash, or an ambiguous result.

Step 10: Compile to Multiple Targets

The same PCD compiles to 5 backends:
# Native x86-64 ELF (standalone, no dependencies)
brikc build policy.pcd -o policy

# Rust module
brikc emit --rust policy.pcd -o policy.rs

# JavaScript ES module
brikc emit --js policy.pcd -o policy.mjs

# Python module
brikc emit --python policy.pcd -o policy.py

# WebAssembly
brikc build --target wasm32 policy.pcd -o policy.wasm
All five outputs are semantically equivalent — same input always produces same output, regardless of target.

What to Learn Next