Skip to main content

PCD Type System

PCD uses a structural type system with inference. The planner transforms programs into SSA (Static Single Assignment) form before type checking. Types flow through the circuit — the TCE (Thermodynamic Coherence Engine) verifies that types are consistent across all paths before Φ_c = 1 can be certified.
PCD does not require explicit type annotations. The planner infers all types from usage. Type errors are caught at compile time, before any code generation.

Primitive Types

i64 — Default Integer

The default integer type in PCD. All integer literals are i64 unless used as monomer arguments (see u8 below):
let x = 42;          // i64
let y = -100;        // i64
let z = 0;           // i64
Range: −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Arithmetic operations on i64 values use standard 64-bit signed integer semantics. Overflow is not trapped — it wraps.
let a = 1000000000;
let b = a * a;    // 1_000_000_000_000_000_000 (fits in i64)

u8 — Monomer Argument Type

u8 is the type expected by most arithmetic and logic monomers. The planner automatically coerces i64 and bool values to u8 at monomer call boundaries:
let a = 10;          // i64
let b = 20;          // i64
let c = MC_00.ADD8(a, b);   // a and b coerced to u8 at call site
// c is Value::I64 (monomers return i64, not u8)
Component monomers always return Value::I64, not Value::U8. Even though ADD8 works on 8-bit operands internally, the return value is always widened to i64. The only exception is MC_03.DIV8 which returns a Tuple.
Explicit u8 coercion is rarely needed — the planner handles it at monomer boundaries. However, control monomers (MC_24–MC_31) are an exception: MC_24.IF requires a bool condition, and the planner preserves bool values for these monomers rather than coercing them to u64.

bool — Boolean

Boolean literals are true and false:
let flag = true;
let done = false;
In arithmetic and logic monomers, bool is coerced to u8: true → 1, false → 0. In control monomers (id 24–31), bool is preserved as-is. Comparison operators produce bool:
let gt = a > b;    // bool
let eq = a == b;   // bool
Logical operators operate on bool:
let both = flag1 && flag2;   // bool
let either = flag1 || flag2; // bool
let inv = !flag;             // bool
Bool ↔ i64 coercion in comparisons: The planner handles mixed bool/i64 comparisons in CmpEq:
let result = MC_00.ADD8(1, 1);   // returns i64(2)
let check = (result == 2);        // i64 vs i64 → bool (OK)
let check2 = (flag == 1);         // bool vs i64 → coerced comparison (OK)

string — UTF-8 Text

Strings are UTF-8 encoded, immutable values:
let s = "hello, world";
let multiline = "line1\nline2\nline3";
String operations are performed via the String monomer family (MC_40–MC_47):
let len  = MC_43.LEN(s);             // i64
let sub  = MC_42.SUBSTR(s, 0, 5);   // string
let cat  = MC_40.CONCAT(s, "!");     // string
let ch   = MC_45.CHAR_AT(s, 0);     // string (single character)
let up   = MC_44.UPPER(s);          // string
The + operator is sugar for MC_40.CONCAT:
let greeting = "Hello, " + name + "!";
// equivalent to:
let greeting = MC_40.CONCAT(MC_40.CONCAT("Hello, ", name), "!");

Composite Types

Array

Arrays are ordered, homogeneous collections. Literal syntax:
let nums = [1, 2, 3, 4, 5];
let names = ["alice", "bob", "carol"];
let empty = [];
Indexing uses bracket notation:
let first = nums[0];
let last  = nums[4];
Push creates a new array (arrays are immutable — push returns a new array):
let nums2 = nums.push(6);   // [1, 2, 3, 4, 5, 6]
Length:
let n = MC_43.LEN(nums);    // i64
Iterating over arrays:
let arr = [10, 20, 30];
let len = MC_43.LEN(arr);
let sum = 0;
loop(len) as i {
    let elem = arr[i];
    let sum = sum + elem;
}
Array indices must be i64. Out-of-bounds access produces an error that can be caught with try/catch.

Tuple

Tuples are fixed-size heterogeneous collections. They are primarily used for multiple return values:
let pair = (10, "hello");
let triple = (1, 2, 3);
Destructuring:
let (a, b) = (10, 20);
let (q, r) = MC_03.DIV8(17, 5);    // q=3, r=2
The most important tuple in PCD is the return value of MC_03.DIV8:
let (quotient, remainder) = MC_03.DIV8(100, 7);
// quotient = 14, remainder = 2
MC_03.DIV8 is the only monomer that returns a Tuple. All other arithmetic monomers return Value::I64. Forgetting to destructure DIV8’s result is a common error.

Struct

Structs are named product types with labeled fields: Definition:
struct Token {
    kind:  i64,
    value: string,
    pos:   i64
}
Construction:
let tok = Token { kind: 1, value: "hello", pos: 0 };
Field Access:
let k = tok.kind;
let v = tok.value;
Structs are values — they are immutable. To “update” a field, construct a new struct:
let tok2 = Token { kind: tok.kind, value: "world", pos: tok.pos + 5 };
Nested Structs:
struct Point { x: i64, y: i64 }
struct Rect { origin: Point, width: i64, height: i64 }

let r = Rect {
    origin: Point { x: 0, y: 0 },
    width:  100,
    height: 50
};
let ox = r.origin.x;

Closures

Closures are first-class function values that capture their enclosing scope:
let add = fn(x, y) { x + y };
let result = add(3, 4);   // 7
Capturing outer variables:
let factor = 5;
let scale = fn(x) { x * factor };   // captures `factor`
let scaled = scale(10);   // 50
Passing closures as arguments (higher-order functions):
fn apply_twice(f, x) {
    return f(f(x));
}
let triple = fn(n) { n * 3 };
let result = apply_twice(triple, 2);   // 2 * 3 * 3 = 18
Returning closures:
fn make_adder(n) {
    return fn(x) { x + n };
}
let add5 = make_adder(5);
let result = add5(10);   // 15

Type Coercion at Monomer Boundaries

The PCD planner applies automatic coercion rules when calling monomers:
When a bool is passed to an arithmetic or logic monomer, it is coerced: true → 1, false → 0.
let flag = true;
let result = MC_00.ADD8(flag, 5);   // true coerced to 1, result = 6
Control monomers like MC_24.IF require actual bool values. The planner skips bool → u64 coercion for these monomers.
let cond = x > 0;
let r = MC_24.IF(cond, "pos", "neg");   // cond stays bool
i64 values passed to arithmetic monomers are narrowed to u8 (low 8 bits). For values that fit in 0–255 this is lossless.
let a = 200;                       // i64
let b = MC_00.ADD8(a, 55);         // a truncated to u8(200), result = 255 as i64
Regardless of the monomer family, all single-value monomers return Value::I64. Only MC_03.DIV8 returns Value::Tuple([i64, i64]).
let x = MC_00.ADD8(1, 2);    // Value::I64(3)
let y = MC_11.NOT8(255);     // Value::I64(0)
let z = MC_43.LEN("hello"); // Value::I64(5)
let (q, r) = MC_03.DIV8(10, 3);  // Value::Tuple([I64(3), I64(1)])

SSA Form and the Planner

The PCD planner transforms programs into SSA (Static Single Assignment) form before type checking and optimization. Understanding SSA is important for writing correct loop-carried variables.

SSA Versioning

In SSA form, every assignment creates a new version of a variable:
let x = 0;        // x_0
let x = x + 1;   // x_1 = x_0 + 1
let x = x * 2;   // x_2 = x_1 * 2

Loop-Carried Variables

The planner’s bind_let function always uses version 0 for loop-carried rebindings. This ensures the variable slot is shared across iterations:
let counter = 0;          // counter_0 = 0
loop(10) as i {
    let counter = counter + 1;   // rebinding → same slot (version 0 rule)
}
// counter_0 = 10 after loop
If you accidentally introduce a new variable name inside a loop body (different from the outer binding), the planner creates a new SSA slot and the outer variable does not update. Always use the exact same name for loop-carried mutations.

Type Inference in SSA

The planner infers types by propagating them through SSA edges:
  1. Literal 42i64
  2. fn(x, y) { x + y } — parameters are polymorphic until first use; + constrains to i64
  3. Monomer returns → always i64 (except DIV8 → Tuple)
  4. true/falsebool
  5. [...]array (element type inferred from contents)
  6. (a, b)Tuple (heterogeneous)

Value Types vs. Reference Semantics

PCD uses value semantics for all types. There are no references or pointers. Passing a struct or array to a function copies the value:
fn modify(arr) {
    let arr = arr.push(99);   // creates new array
    return arr;
}

let original = [1, 2, 3];
let modified = modify(original);
// original is still [1, 2, 3]
// modified is [1, 2, 3, 99]
This property is key to the determinism guarantee: PCD programs cannot have aliasing bugs, shared mutable state, or race conditions.
Value semantics is what makes Φ_c = 1 verification tractable. With reference semantics, proving coherence would require alias analysis. With value semantics, the TCE can verify each path independently.

Type Summary Table

TypeLiteral ExampleNotes
i6442, -7, 0Default integer type
u8(coerced from i64)Used at monomer boundaries
booltrue, falseCoerced to u8 in arithmetic
string"hello"UTF-8, immutable
array[1, 2, 3]Homogeneous, value semantics
tuple(a, b)Heterogeneous, fixed size
structPoint { x: 0, y: 0 }Named fields, value semantics
closurefn(x) { x + 1 }First-class function value
Tuple([i64, i64])(DIV8 return)Only from MC_03.DIV8