Blog › ICP guides

BCPL developer on retainer: word-level addressing, ! indirect operator, BYTESPERWORD, MANIFEST constants, GLOBAL words, and BCPL systems programming on monthly retainer

November 3, 2026 · ~18 min read

A BCPL systems program managing a table of fixed-size records was producing three wrong values per computation run. The program stored records in a vector (BCPL’s contiguous block of machine words) and accessed individual records using pointer arithmetic: to access record number i, the code computed !(base + i * RECSIZE), where RECSIZE was a MANIFEST constant defined as 8 (representing the 8 bytes in each record). On the 16-bit embedded platform where the program ran, BCPL’s ! operator (indirect/index) addressed memory in word units — two bytes per word. The RECSIZE value of 8 was the correct byte count, but the ! operator treated it as a word count: !(base + i * 8) jumped 8 words (16 bytes) per record instead of 4 words (8 bytes). For records at index 0, the arithmetic was correct (0 * 8 = 0 in both byte and word counts). For index 1, the code accessed the word at offset 8 words instead of 4 words, reading two records beyond the intended record boundary. Three computations per run used records at index 1 or higher, producing wrong values from adjacent record data. The BCPL developer on retainer diagnosed the addressing unit mismatch: RECSIZE must be expressed in word counts for use with the ! operator on this platform. The fix changed the MANIFEST declaration to RECSIZE = 8 / BYTESPERWORD, which the BCPL compiler evaluated at compile time using the platform constant BYTESPERWORD = 2, producing RECSIZE = 4. Wrong values per computation run: 3 → 0.

The work log entry read “fixed record table pointer arithmetic, 14h.” It names the symptom and the duration. It cannot explain to a client why the ! operator in BCPL indexes in word units rather than byte units (BCPL is a typeless language where all values are machine words; the ! operator is defined in terms of words because the machine’s natural addressing unit is words on the platforms BCPL was designed for in the late 1960s; byte-level access requires the separate % operator), why BYTESPERWORD is a platform-specific constant in BCPL programs rather than a fixed value (BCPL was designed to be highly portable across machines with different word sizes; 16-bit platforms have BYTESPERWORD = 2; 32-bit platforms have BYTESPERWORD = 4; programs that hard-code byte counts in ! arithmetic are implicitly non-portable), why the bug appeared only for records at index 1 and higher (index 0 produces offset 0 in both byte and word units; the addressing unit difference only manifests for non-zero indices), or why using RECSIZE = 8 / BYTESPERWORD in the MANIFEST declaration makes the program portable across word sizes without source code changes (the BCPL compiler evaluates the MANIFEST expression at compile time using the platform’s BYTESPERWORD value; the resulting constant is always in word units regardless of word size). The 14 hours of addressing unit analysis, BYTESPERWORD platform audit, all-index pointer arithmetic verification, and record-layout byte/word conversion design are not visible in the diff beyond a changed constant definition.

BCPL’s typeless word model: !, %, rv, lv, and BYTESPERWORD

BCPL was designed by Martin Richards at Cambridge in 1967 as a radical simplification of CPL: a systems programming language with a single data type (the machine word) and no type checking. Every BCPL value is a machine word. Pointers are words (memory addresses). Integers are words. Characters are words (one character per word unless packed). The typeless design enables BCPL programs to perform low-level memory manipulation without casts or type annotations — a design choice that directly influenced Ken Thompson and Dennis Ritchie when they created B and C at Bell Labs. BCPL’s memory model is word-addressed: the fundamental unit of addressing is the machine word, not the byte. On a 16-bit machine, a word is two bytes; on a 32-bit machine, a word is four bytes. The constant BYTESPERWORD is the system-provided bridge between byte-sized lengths and word-sized addresses.

The ! operator in BCPL is the indirect (dereference) and subscript operator. Used as a prefix: !p dereferences the pointer p, loading the word at the address p. Used as an infix: v!i is equivalent to !(v + i), loading the word at address v + i (where addition is in word units). In assignments, !p := value stores a word at address p; v!i := value stores a word at v + i. The key invariant: all arithmetic in ! expressions is in word units. Record-sized strides, field offsets, and array dimensions must all be expressed in words when used with !. The % operator is the byte-subscript operator: s%i accesses the ith byte of the memory starting at word address s (0-indexed). The % operator is used for character access in strings and for byte-granularity hardware register access. !p and p%0 through p%(BYTESPERWORD-1) access the same word through different granularity views.

The rv (right-value) and lv (left-value) operators provide address arithmetic. lv x (equivalently, @ x) returns the address of variable x as a word value. rv p is synonymous with !p: it dereferences the pointer p. The lv/@ operator is BCPL’s address-of: LET p = @ x makes p a pointer to x; !p := 42 then sets x to 42 through the pointer. This is BCPL’s manual pointer design, entirely analogous to C’s & and * operators. The common retainer mistake with lv/@: using @ x on a BCPL variable declared with LET inside a procedure body and storing the result for use after the procedure returns — the address is a stack address, and it becomes invalid after the procedure exits, exactly the stack-address-escape bug that C programmers know as returning a pointer to a local variable. In BCPL, the problem is structurally identical because procedure-local variables live on the call stack.

BYTESPERWORD is provided by the BCPL system as a constant equal to the number of bytes in a machine word on the target platform. BCPL programs written for portability across word sizes use BYTESPERWORD in MANIFEST expressions to convert byte counts to word counts: MANIFEST $( RECSIZE = RECORD_BYTES / BYTESPERWORD $) computes the word-count equivalent of a byte-sized record size at compile time. String storage in BCPL uses one word per character in the simplest representation; PACKSTRING and UNPACKSTRING provide conversion to and from packed byte-per-character representation. The platform-specific string packing means that s%0 (the first byte of the word at address s) holds the string’s length byte in BCPL’s counted string format; s%i for i from 1 to s%0 holds the string’s characters. This is BCPL’s built-in string representation: a length-prefixed byte array packed into words, with the length byte at byte offset 0.

BCPL procedures, control flow, and VALOF expressions

BCPL procedure definitions: LET name(args) BE block declares a procedure named name taking the listed arguments. The procedure body is a block: a sequence of statements enclosed in $( ... $) delimiters (BCPL’s block syntax, equivalent to C’s curly braces). Procedures that return values use RESULTIS value to return from the innermost VALOF block containing the RESULTIS. The VALOF construct is BCPL’s block expression: VALOF $( ... RESULTIS expr $) is an expression (usable anywhere an expression is expected) that executes the block and returns the value given to RESULTIS. A common BCPL idiom for a function with a value return: LET square(n) = VALOF $( RESULTIS n * n $). The = form (rather than BE) is syntactic sugar for a procedure body that is a single VALOF expression. Procedures that do not return a value use RETURN to exit early; falling off the end of a procedure with a BE body is equivalent to RETURN with no value.

BCPL’s control flow constructs: IF condition DO statement executes statement if condition is true; UNLESS condition DO statement executes if condition is false. The conditional with two branches: TEST condition THEN statement1 ELSE statement2 (not IF/ELSE, because IF has no ELSE in BCPL). In a block context, TEST/THEN/ELSE is BCPL’s two-branch conditional. Iteration: WHILE condition DO statement repeats while condition is true; UNTIL condition DO statement repeats until condition becomes true; FOR i = start TO end DO statement iterates with i from start to end inclusive, incrementing by 1; FOR i = start TO end BY step DO statement uses the specified step. Loop termination: BREAK exits the innermost loop; LOOP continues to the next iteration. REPEATWHILE condition and REPEATUNTIL condition are post-test loop forms: the body executes once before the condition is tested. SWITCHON expression INTO $( CASE value: statement ... DEFAULT: statement $) is BCPL’s case dispatch, analogous to C’s switch.

BCPL procedures are first-class values: a procedure name evaluates to the machine address of the procedure code. This enables higher-order programming: LET apply(f, x) = VALOF $( RESULTIS f(x) $) takes a procedure reference and an argument and calls the procedure. Passing procedures as arguments: apply(square, 5) passes the square procedure and the argument 5. BCPL’s first-class procedure design was a direct predecessor of C function pointers. The common retainer pattern for table-driven dispatch in BCPL: a vector of procedure references (LET dispatch_table = TABLE handler_a, handler_b, handler_c) indexed by a command code (dispatch_table!command_code(args)). The TABLE declaration is BCPL’s vector-literal syntax: the compiler allocates a vector initialized with the listed values. BCPL’s TABLE design is the precursor to C’s compound literal arrays and vtable dispatch patterns.

BCPL’s conditional expression condition -> true_value, false_value is the ternary operator: it evaluates to true_value if condition is true, otherwise false_value. This is equivalent to C’s condition ? true_value : false_value. BCPL’s boolean values: any non-zero word is true; zero is false (exactly as in C). Comparison operators return 1 for true and 0 for false. Logical operators: & (logical AND), | (logical OR), ~ (logical NOT). Bitwise operators: AND, OR, XOR, EQV, NEQV, NOT for bitwise operations on word values. Note the naming convention: BCPL uses keyword-form names (AND, OR) for bitwise operations and symbol-form (&, |) for logical operations — the opposite of C, which uses &&/|| for logical and &/| for bitwise. This reversal is a common retainer bug in BCPL programs written by programmers with a C background.

BCPL MANIFEST, STATIC, GLOBAL, section linking, and string operations

BCPL’s constant and variable declaration taxonomy: MANIFEST $( NAME = value ... $) declares compile-time constants. MANIFEST names are replaced by their values at compile time with no runtime storage. STATIC $( name = initial_value ... $) declares module-level variables that persist for the program’s lifetime (initialized to the given value at program start). STATIC variables are stored in a data segment and retain their values across procedure calls. GLOBAL $( name : word_number ... $) declares global words at specific offsets in the global vector. The global vector is a machine-word array shared across all separately-compiled sections of a BCPL program; every section that declares the same global name at the same word number accesses the same runtime word. This is BCPL’s cross-section communication mechanism: two sections that both declare GLOBAL $( total_count : 50 $) are accessing the same location in the global vector at offset 50. The global vector has a fixed size (typically 1000 words); global word number 0 holds the address of the global vector itself; global words 1 through some implementation-defined boundary are reserved for the BCPL runtime.

The distinction between MANIFEST, STATIC, and GLOBAL is the most common scoping mistake in BCPL retainer code. MANIFEST: pure compile-time alias, no storage, no runtime overhead, correct for constants used only within one section. STATIC: module-level persistent storage, private to the section, not accessible from other sections, correct for per-module state. GLOBAL: shared runtime word visible to all sections, correct for cross-section shared state (the program-wide counter, the error flag, the global configuration). Using STATIC when GLOBAL is needed causes two sections to each have their own private copy of a variable that should be shared. Using GLOBAL when MANIFEST is appropriate wastes a global vector slot and adds an indirection. The retainer task of constant/variable scope audit: for each MANIFEST, STATIC, and GLOBAL declaration, verify the intended scope and access pattern, and correct the declaration form to match.

BCPL’s section linking system: a BCPL program can be divided into separately-compiled sections. Each section is compiled to an object module and linked with other sections at link time. SECTWORD N declares that the section requires N words of code space. SECTNAME "name" names the section for the linker’s symbol table. GET "libfile" includes a library file at compile time (analogous to C’s #include): the included file’s definitions (MANIFESTs, GLOBAL declarations, procedure forward declarations) become visible in the including section. Library files conventionally contain MANIFEST and GLOBAL declarations that are shared across all sections that include the library; they do not contain executable code (which goes in separately-compiled sections). The standard BCPL library (provided by the BCPL runtime) includes standard I/O procedures, string manipulation, and mathematical functions.

BCPL string operations: strings in BCPL are represented as counted byte arrays. A string literal "hello" is stored as a sequence of bytes starting at byte position 0 with the length (5 for "hello"), followed by the character bytes. s%0 accesses the length byte; s%i for i from 1 to s%0 accesses the character bytes. PACKSTRING(src, dest) converts a null-terminated external string (one character per word, as produced by C-style interfaces) to BCPL’s packed byte format in dest. UNPACKSTRING(src, dest) converts BCPL’s packed format to unpacked one-character-per-word format. These conversions are required whenever BCPL code exchanges strings with external C libraries or hardware interfaces that use different string representations. The retainer task of string interface design: for every external function call that passes or receives a string, verify that the correct PACK or UNPACK conversion is applied at the call site. Missing UNPACKSTRING before a C library call passes the BCPL length byte to the C function as the first character, producing the systematic wrong-first-character class of bug.

BCPL’s design legacy: the direct ancestor of C

BCPL’s historical significance is its direct line of descent to C and every C-family language. Ken Thompson created B in 1969 by simplifying BCPL for the PDP-7 at Bell Labs; Dennis Ritchie evolved B into C in 1972 for the PDP-11. The core design decisions that BCPL contributed to C: the typeless-word model (which became C’s implicit conversions between pointer types and integers in early C); the ! operator (which became C’s * dereference operator); the LET/BE procedure declaration (which became C’s function definition); the $( ... $) block syntax (which became C’s { ... }); and the MANIFEST system (which became C’s #define preprocessor macros). BCPL’s approach to string representation (length-prefixed byte arrays) influenced the design of Pascal strings; C’s null-terminated strings were a departure from BCPL’s convention. The word-addressed memory model that makes BCPL’s ! operator index in words rather than bytes was a pragmatic reflection of 1960s hardware reality; modern C on byte-addressed hardware uses pointer types to track addressing granularity, the abstraction that BCPL’s typeless design omitted.

BCPL remains in use today primarily in three contexts: embedded systems programming on resource-constrained platforms where BCPL’s small runtime footprint and direct hardware access are advantages over C; legacy operating system and firmware codebases (most notably the Tripos operating system, the BCPL-written predecessor of AmigaDOS); and programming language history research. The BCPL reference implementation is Martin Richards’s own implementation, maintained at Cambridge and available for modern platforms. The CINTSYS/CINTCODE interpretive BCPL implementation provides a portable execution environment for BCPL programs without a native-code BCPL compiler.

How HourTab tracks BCPL developer retainer hours

BCPL retainer work shares the invisible-work problem common to all systems programming language retainers, with the additional challenge that BCPL’s most important retainer tasks — word-vs-byte addressing unit repair, MANIFEST vs STATIC vs GLOBAL scope correction, PACKSTRING/UNPACKSTRING external interface design, section linking global vector layout analysis, and !/% operator boundary auditing — produce diffs whose surface area is small relative to the analytical work required. Changing MANIFEST $( RECSIZE = 8 $) to MANIFEST $( RECSIZE = 8 / BYTESPERWORD $) is a diff with one expression; the value is correct word-addressed pointer arithmetic for all record indices on the target platform, elimination of out-of-bounds record access for all non-zero indices, and a MANIFEST definition that is portable across 16-bit and 32-bit BCPL platforms without further source changes. Changing a STATIC declaration to MANIFEST for a compile-time constant is a diff with one keyword; the value is eliminated global vector slot consumption, resolved cross-section naming conflict, and correct scoping for a value that was never intended to be mutable runtime state. Adding UNPACKSTRING before an external C library call is a diff with one function call; the value is correct string format at the call site, elimination of length-byte-as-first-character misinterpretation in all subsequent external calls, and a correct interface wrapper that handles the BCPL/C string format boundary.

HourTab gives BCPL developers a public retainer-hours URL they send to clients — typically embedded systems engineering teams working on resource-constrained platforms where BCPL remains in production use, legacy firmware and operating system teams maintaining BCPL-based codebases (Tripos, AmigaDOS descendants), and programming language research groups studying BCPL’s role in the lineage from ALGOL 60 to C — at the start of an engagement. For BCPL retainers, each work log entry should name the mechanism (! (indirect/index) operator word-addressing analysis; % (byte-subscript) operator byte-addressing design; BYTESPERWORD platform constant usage; rv/lv/@ idiom design; pointer arithmetic unit audit; LET name(args) BE block procedure authorship; RESULTIS return value design; VALOF block expression design; TEST/THEN/ELSE conditional design; WHILE/DO and FOR i = 1 TO n DO loop design; REPEATUNTIL/REPEATWHILE post-test iteration; SWITCHON/OF/CASE/DEFAULT case dispatch; MANIFEST compile-time constant design; STATIC module-level persistent state; GLOBAL word declaration for cross-section shared state; GET 'libfile' library inclusion; SECTWORD/SECTNAME section linking; global vector layout analysis; PACKSTRING/UNPACKSTRING format conversion; string%0 length-byte access; % byte-subscript string traversal; external C interface string wrapper design), the specific procedure name and the addressing unit or constant scoping problem, and the before/after observable metric. BCPL retainers are often compared to Forth developer retainers for low-level stack-based systems programming work, to Simula developer retainers for 1960s-era language systems engineering (both languages were designed in the same period and influenced the entire subsequent landscape of programming language design), and to Objective-C developer retainers for C-derived language systems work. The distinction from Forth is the memory model: BCPL’s word-addressed ! operator with separate % byte access is the defining retainer issue in BCPL code, not the stack-based computation model that characterizes Forth work. HourTab’s work log makes the word-vs-byte addressing unit analysis and BYTESPERWORD portability design visible to clients who would otherwise see only the symptom — wrong values at non-zero record indices — and not understand why the fix required understanding BCPL’s word-addressed memory model and the compile-time constant portability of the MANIFEST/BYTESPERWORD idiom.

Track BCPL developer retainer hours without the status emails

HourTab gives BCPL developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.

See HourTab pricing →

FAQ: BCPL developer retainers

What does a BCPL developer on retainer typically do?

A BCPL developer on monthly retainer covers four principal service areas: word-level memory model analysis and pointer arithmetic (! (indirect/index) operator word-addressing analysis; % (byte-subscript) operator byte-addressing design; BYTESPERWORD platform constant usage; rv/lv/@ address-of idiom; pointer arithmetic unit audit for all RECSIZE and record-stride computations); procedure design and control flow (LET name(args) BE block procedure authorship; RESULTIS return value design; VALOF block expression design; TEST/THEN/ELSE conditional; WHILE/DO, FOR i = 1 TO n DO, REPEATUNTIL/REPEATWHILE iteration; SWITCHON/OF/CASE dispatch; first-class procedure values and TABLE dispatch); constant and linking declarations (MANIFEST compile-time constant design; STATIC module-level persistent state; GLOBAL word declaration for cross-section shared state; GET 'libfile' library inclusion; SECTWORD/SECTNAME section linking; global vector layout analysis); and string and character operations (PACKSTRING/UNPACKSTRING format conversion; string%0 length-byte access; % byte-subscript string traversal; external C interface string wrapper design).

What BCPL work is most commonly underlogged in a retainer?

Word-vs-byte addressing repair (RECSIZE set in bytes on 16-bit word-addressed platform; ! operator offset by factor of BYTESPERWORD; 3 wrong values/computation run; corrected RECSIZE = 8 / BYTESPERWORD in MANIFEST; wrong values: 3/run → 0; 14–26 hrs invisible in addressing unit analysis, platform audit, and pointer arithmetic verification), MANIFEST vs STATIC constant scope repair (STATIC used for compile-time constant consumed a global vector slot conflicting with another section’s GLOBAL declaration; 2 corrupted global vector words/program load; changed to MANIFEST; corrupted words: 2/load → 0; 10–18 hrs invisible in global vector layout analysis, STATIC vs MANIFEST semantic audit, and section linking conflict resolution), and PACKSTRING/UNPACKSTRING format repair (BCPL string passed to external C function without UNPACKSTRING; C function read length byte as first character; 7 wrong string displays/session; added UNPACKSTRING to work buffer before external call; wrong displays: 7/session → 0; 9–16 hrs invisible in string format analysis and external interface wrapper design).

What are typical BCPL developer retainer rates?

Entry-level BCPL developers (1–2 years, LET procedure definitions, basic ! operator usage, MANIFEST constants, FOR/WHILE loop constructs) bill at $70–$125/hr. Mid-level BCPL engineers (2–4 years, word-vs-byte addressing analysis, BYTESPERWORD portability design, STATIC vs GLOBAL vs MANIFEST semantic distinctions, PACKSTRING/UNPACKSTRING string format conversion, SECTWORD/SECTNAME section linking) bill at $120–$210/hr. Senior BCPL architects (4–8 years, complete BCPL systems program architecture, complex global vector layout design, multi-section program linking, embedded systems hardware register access via pointer arithmetic, BCPL compiler toolchain work) bill at $175–$315/hr. Monthly retainer ranges: $2,000–$5,000/mo advisory (15–25 hrs), $7,000–$18,000/mo for full BCPL platform engagements.

What should a BCPL developer retainer agreement include?

A BCPL developer retainer agreement should specify: memory model scope (! word-addressing analysis; % byte-addressing design; BYTESPERWORD platform constant usage; rv/lv/@ address-of idiom; pointer arithmetic unit audit); procedure design scope (LET name(args) BE block authorship; RESULTIS return; VALOF block expression; TEST/THEN/ELSE conditional; WHILE/DO/FOR/REPEATUNTIL/REPEATWHILE iteration; SWITCHON/OF/CASE/DEFAULT dispatch; first-class procedure TABLE design); constant and linking scope (MANIFEST/STATIC/GLOBAL scope design; GET 'libfile' inclusion; SECTWORD/SECTNAME linking; global vector layout); string scope (PACKSTRING/UNPACKSTRING format conversion; string%0 length-byte; % byte-subscript string traversal; external C interface wrapper); and hour logging format (procedure name; operation type — addressing unit, constant scope, or string format; before/after error metric; platform BYTESPERWORD value).

How should BCPL developer retainer hours be logged?

Log each BCPL retainer session with: advisory category (! operator word-addressing analysis; % operator byte-addressing design; BYTESPERWORD platform constant usage; rv/lv/@ idiom; pointer arithmetic unit audit; LET name(args) BE block procedure authorship; RESULTIS return; VALOF block expression; TEST/THEN/ELSE conditional; WHILE/DO/FOR/REPEATUNTIL/REPEATWHILE iteration; SWITCHON/OF/CASE/DEFAULT dispatch; MANIFEST/STATIC/GLOBAL scope design; GET 'libfile' inclusion; SECTWORD/SECTNAME linking; global vector layout; PACKSTRING/UNPACKSTRING format conversion; string%0 length-byte access; external C interface string wrapper), the specific procedure name and the addressing unit or constant scoping problem (RECSIZE set in bytes on 16-bit word-addressed platform; ! operator offset by BYTESPERWORD; 3 wrong values/run; corrected RECSIZE = 8 / BYTESPERWORD; wrong values: 3/run → 0), and the before/after metric (wrong values/run: 3 → 0; corrupted global vector words/load: 2 → 0; wrong string displays/session: 7 → 0). Include BYTESPERWORD platform value, OS, and whether the fix required addressing unit correction, constant scope change, string format conversion, or global vector layout redesign.