C⏚ v2.0.0Updated 2026-05-12·Language

Concepts

The Introduction gave you the mental model: tasks, networks, ports, on a clock. This chapter sharpens that model along three axes - bit-accurate types, a three-phase cycle schedule, and the determinism that falls out of it.

Types are bit-accurate

Every C⏚ type has an exact bit width. There are no implicit promotions, no padding, no sign-extension surprises. A u8 is eight bits. An i6 is six bits. Arithmetic widens exactly as the math says it should:

u8 a = 200;
i6 b = -20;
i9 c = a + b;     // 180, in 9 bits, signed

Three layers cover everything:

  • Fixed-width integers - i2 through i64, u2 through u64, the C aliases (short, int, long), the OpenCL ones (uint, ulong), and bool.
  • Custom-width integers - declared with a compile-time expression: unsigned<words * 32>, int<addrWidth>.
  • Arrays - C-like syntax, constant dimensions: u32 sbox[256]; bool flags[3][16].

typedef gives a type a domain name without introducing a new type:

typedef u8 pixel;
typedef int<addrWidth> addr_t;

The full table of widths, the unification rules for mixed signed and unsigned operands, and the arithmetic widening table all live in Declarations → Types. This chapter only commits to the mental model: bit widths are exact, widening is explicit, the compiler has nothing to guess.

Execution is cycle-by-cycle

One pass through a task's loop() is one clock cycle. When many tasks run together inside a network, who fires when?

A network runs one cycle at a time, in three phases:

One cycle in a C⏚ network: execute, commit, propagate

  1. Execute. Every task runs once. Reads see the current committed value of every port.
  2. Commit. Values written during execute become the new current values.
  3. Propagate. Combinational tasks fire along the producer-to-consumer dependency chain.

The execute/commit split is where the subtlety lives. Consider a producer that writes a count and a consumer that prints it:

network N {
  t1 = new task {
    out push uint counter;
    uint count;
    void loop() {
      count++;
      counter.write(count);
    }
  };
  t2 = new task {
    void loop() {
      print("count = ", t1.counter.read());
    }
  };
}

On the first cycle, t1 writes 1. But t2 also runs in the same execute phase, and its read happens before commit. So t2 sees the value counter held at the end of the previous cycle, which is 0.

cycle 0:  count = 0
cycle 1:  count = 1
cycle 2:  count = 2

The one-cycle lag is the registered-output behaviour of a push port, and it matches how a flip-flop behaves in real silicon. Bare ports skip commit; see Declarations for the full qualifier reference.

Deterministic by construction

The three-phase schedule has one big consequence: C⏚ designs are deterministic. Run the same testbench twice and you get exactly the same trace, regardless of which task the simulator chose to fire first inside a phase.

Three rules enforce that guarantee:

  • State is private. Variables declared inside a task are invisible to every other entity. Nobody else can read or write them.
  • Ports are single-writer. Every push, stream, or confirm output has exactly one writer. The compiler rejects code that tries to wire two writers to the same port.
  • Reads see the committed snapshot. Inside execute, every read returns the value committed at the end of the previous cycle. Two tasks reading the same port in the same cycle see the same value.

Combinational entities are the calibrated exception. They fire in the propagate phase along the dependency graph and update their outputs inside the same cycle. The graph must be acyclic; combinational loops are not supported.

This is why the bytecode simulator and the generated Verilog produce identical traces. They obey the same schedule because the schedule is the language semantics, not an implementation choice.


Next: Declarations for variables, functions, and ports in detail. Tasks and Networks cover the entity syntax.