Blog › ICP guides

BCPL developer on retainer: typeless word model, hardcoded packet size, vector indirect, and BCPL on monthly retainer

October 3, 2026 · ~13 min read

A BCPL developer was maintaining a legacy telecommunications switching controller — a system that had been running in production since before C was the obvious choice for systems programming. The controller processed packets of control data, and one function was central to packet integrity: CALC.CHECKSUM. The function computed a checksum by iterating over a packet structure and summing its 32-bit words. For years, the packet structure had been 8 words long, and CALC.CHECKSUM iterated over exactly 8 words to produce the checksum. Then the developer added a new field to the packet structure — a sequence number, appended as the 9th word at offset V!8.

The packet structure definition was updated. The MANIFEST constants naming each field offset were updated. But CALC.CHECKSUM contained a hardcoded literal 8 as its iteration count — the number of words to sum. The function had no reference to a MANIFEST constant for the packet word count; the 8 had been typed directly into the loop as a plain numeric literal. After the structure change, CALC.CHECKSUM still iterated over exactly 8 words, computed the checksum from the first 8 words, and returned it. The 9th word — the new sequence number field — was never included.

The checksum mismatch was intermittent: 4 packets per hour arrived with a sequence number value that differed from zero, changing the word that CALC.CHECKSUM silently omitted. Those packets failed remote endpoint checksum verification. The symptom was a low-rate verification failure with no obvious cause, because the packet structure looked correct, the MANIFEST field offsets were correct, and the function body did not raise any error. Fix: updated the iteration constant from 8 to 9 inside CALC.CHECKSUM. Checksum mismatches: 4 per hour to 0.

The reason this class of bug is systematically invisible in BCPL is that BCPL has no struct type system. A packet structure is not a type declaration that the compiler tracks; it is a word-offset table, a set of MANIFEST constants that say “field X lives at word offset N.” When you add a field by adding a new MANIFEST constant, the compiler does not know that any function iterating over “all words in a packet” should also cover the new one. There is no sizeof equivalent, no array length expression derived from the structure definition, no compile-time check that the iteration count matches the number of defined fields. The hardcoded literal and the structure definition are two separate things that a human must keep synchronized. When they diverge, the program runs without error and produces wrong results only for inputs that exercise the missed field.

BCPL: the typeless word model and the ancestor of C

BCPL (Basic Combined Programming Language) was designed by Martin Richards at the Cambridge Computer Laboratory in 1967. It descended from CPL (Combined Programming Language, developed at Cambridge and London in the early 1960s), which was itself influenced by ALGOL 60. Ken Thompson used BCPL as the direct model for B (developed at Bell Labs in 1969), and Dennis Ritchie used B as the direct model for C (1972). The lineage BCPL → B → C is one of the most consequential chains in programming language history: nearly every systems programming language in common use today traces its syntax and low-level memory model through C, and through C back to BCPL.

BCPL's defining characteristic is its typeless word model: there is exactly one data type, the WORD, which is the natural machine word of the target architecture — typically 32 bits on the platforms where BCPL was used for telecommunications and embedded systems work. There is no distinction between integers, pointers, booleans, and characters at the language level. A WORD can be used as an integer in arithmetic, as an address in an indirect operation, or as a boolean in a conditional — the programmer decides. This makes BCPL extremely close to the machine and extremely concise, but it means that all type safety is a matter of programmer discipline, not compiler enforcement.

Array indexing in BCPL is pointer arithmetic with word-sized steps. The vector indirect operator ! reads or writes a word. If V is a vector (an address), then V!0 reads the word at address V, V!1 reads the word at address V+1 (one word forward), and V!N reads the word at address V+N. Field access in a packet structure is exactly this: PACKET!0 is the first field, PACKET!1 is the second, and so on. MANIFEST constants give names to the offsets: MANIFEST $( PKT.TYPE = 0; PKT.FLAGS = 1; PKT.SEQNO = 8 $). Access to the sequence number field is then P!PKT.SEQNO, which expands to P!8. The MANIFEST constant names the intent; the ! operator performs the word-level indirection.

Byte addressing uses the separate % operator: V%N reads the Nth byte from address V. This is distinct from word addressing and a common source of bugs: using % where ! is required reads a byte rather than a full word, producing a value that looks plausible but is only the low byte of the intended word. The address-of operator @ produces the address of a variable: @X is the address of X, which is the same integer as the address. BCPL bindings use LET for sequential declaration and AND for simultaneous (mutually recursive) binding: LET F(X) = ... AND G(Y) = ... defines F and G as mutually visible to each other.

MANIFEST declares compile-time constants: MANIFEST $( WORDS.IN.PACKET = 9 $). GLOBAL declares global variables by number in the BCPL global vector: GLOBAL $( CALC.CHECKSUM : 42; SEND.PACKET : 43 $). The global vector is an array of words indexed by number; a GLOBAL declaration assigns a function or variable to a specific numbered slot. STATIC declares module-level state that persists across calls but is not globally accessible by number. The GLOBAL numbering scheme is the mechanism that makes cross-module global vector collision possible: if two separately compiled modules both declare a global at slot 42, one will overwrite the other at load time, producing a function call that invokes the wrong module’s function or a variable read that returns the wrong module’s data.

CINTCODE, vector indirect, and BCPL’s data model

BCPL was originally defined in terms of CINTCODE, a virtual machine (interpretive code) designed by Martin Richards as part of the BCPL implementation. CINTCODE is a stack-based bytecode interpreter: BCPL source compiles to CINTCODE bytecodes, which can be interpreted directly on any platform with a CINTCODE interpreter. This made BCPL highly portable in an era when cross-platform compilation was difficult. The CINTCODE interpreter then executed on the host machine. Alternatively, a BCPL compiler could translate CINTCODE to native machine code for a specific target. The CINTCODE virtual machine has a stack, an accumulator register (A), and a program counter. Procedure calls use the stack; local variables are stack-allocated; the GLOBAL vector is a separate array of words accessible from any procedure.

The vector indirect operator ! is central to BCPL’s data model. In CINTCODE terms, V!N is an indirect load: compute the address V + N (word offset), load the word at that address into the accumulator. There is no distinction at the machine level between reading an integer field and reading a pointer field: both are word indirect loads. When a packet structure has 9 fields and the function iterating over it uses a hardcoded 8 as the word count, the CINTCODE interpreter executes exactly 8 indirect loads and stops. The 9th indirect load — V!8 — never executes. No trap is raised. The accumulator holds the sum of 8 words. The function returns. The returned checksum is numerically valid; it is just wrong because it omits one word.

This is the structural contrast between BCPL and C that matters for migration. In C, a struct has a sizeof: sizeof(struct packet_t) is a compile-time constant derived from the struct definition, and if you add a field to the struct, sizeof automatically grows. A loop written as for (i = 0; i < sizeof(pkt)/sizeof(uint32_t); i++) automatically covers all fields after a struct extension. In BCPL, the packet structure has no sizeof. The programmer must maintain a separate MANIFEST constant naming the total word count of the structure and must ensure that every function that iterates over the whole structure references that MANIFEST constant rather than a hardcoded literal. When the MANIFEST constant is used everywhere, adding a new field requires updating one constant. When a hardcoded literal is used inside a function body, the function body must be found and updated separately — and it will not be found by a search for the MANIFEST constant name.

BCPL’s address model has a specific implication that creates confusion during C migration: in BCPL, a vector V and the integer that is the address of V are the same value. There is no pointer type distinct from an integer type. LET V = GETVEC(9) allocates a 9-word vector and assigns its address (an integer) to V. V!3 adds 3 to V (the integer) and reads the word at the result. In C, this distinction matters enormously: int *p and intptr_t n are different types; assigning one to the other requires an explicit cast and is a source of portability and correctness bugs. BCPL code that uses a word both as an integer and as an address in the same function is correct BCPL but requires careful analysis to migrate to C, because the C type system will not accept the same variable being passed both to a function expecting int and to a function expecting int * without explicit casts.

Typical BCPL retainer work and what it looks like in a work log

Hardcoded word-count bugs are the largest category of BCPL retainer work that produces no visible artifact. The pattern is always the same: a packet structure or data table gains a new field; the MANIFEST constants naming field offsets are updated because they are obviously near the structure definition; a function elsewhere in the codebase that iterates over “all words” uses a hardcoded literal that was correct for the old structure size and is now one short. The function produces a wrong result only for inputs where the new field contains a non-zero value. In a telecommunications switching controller, sequence numbers start at zero at system boot and increment; during normal operation they are non-zero, so the bug manifests immediately in production traffic and not at all in cold-start smoke tests. Work log entry: “CALC.CHECKSUM: iteration count hardcoded as 8; packet structure extended to 9 words with addition of PKT.SEQNO at offset V!8; word 8 not included in checksum sum; symptom: 4 checksum mismatches per hour at remote endpoint; fix: updated literal 8 to 9 in loop body; mismatches: 4/hr → 0; 3h.”

Byte vs word addressing misuse is the second category. BCPL provides both ! (word indirect) and % (byte indirect) because packet structures are typically word-aligned but string data and some protocol fields are byte-addressed. A developer familiar with C string handling may reach for % when reading a multi-byte integer field, producing a value that is only the low byte of the intended 32-bit word. The symptom is a plausible-looking but wrong integer: a field that should read 0x0001A3FF reads 0xFF, and the erroneous value passes range checks because 0xFF is a valid byte value. Diagnosis requires reading the packet structure definition, identifying the field’s intended size (word or byte), and verifying that the access operator matches. Work log entry: “DECODE.HEADER: field at offset PKT.LENGTH accessed with % (byte indirect) instead of ! (word indirect); length field is a 32-bit word; only the low byte was read; packet length appeared as 255 when the actual length was 65535; packets with length > 255 were rejected as malformed; fix: replaced P%PKT.LENGTH with P!PKT.LENGTH; malformed rejections: 17/min → 0; 2h.”

GLOBAL vector number collisions are the third category. The BCPL GLOBAL vector assigns functions and variables to numbered slots, and the numbering is the programmer’s responsibility. In a large system with multiple separately compiled modules, two modules can independently assign different globals to the same slot number. The collision is not detected at compile time, because each module compiles independently and the global vector is not checked for conflicts until all modules are loaded together. The symptom is a function call that dispatches to the wrong function, or a global variable read that returns a value written by a different module. The bug may lie dormant when the system is tested with a single module loaded and only appear in production when all modules are present. Diagnosis requires reading all GLOBAL declarations across all modules, building a slot-number-to-module map, and identifying duplicates. Work log entry: “modules SWITCH.CTL and ROUTE.MGR both declare a global at slot 42; SWITCH.CTL uses slot 42 for HANDLE.CONNECT; ROUTE.MGR uses slot 42 for ROUTE.TABLE.PTR; at load time, ROUTE.MGR overwrites the slot with a data pointer; HANDLE.CONNECT calls transfer to a data address and fault; fix: renumbered ROUTE.MGR’s slot 42 to slot 87 (next free); faults: uncontrolled → 0; 4h.”

CINTCODE stack overflow in recursive procedures is the fourth category. BCPL supports recursive procedures and the CINTCODE virtual machine has a fixed stack. Deep or unbounded recursion exhausts the stack, producing an overflow that terminates the procedure or corrupts the return address chain. In CINTCODE, the stack overflow may manifest as an interpreter error code rather than a processor-level fault, making the symptom appear as a controlled error return rather than a crash — which can cause the calling code to treat the overflow as a recoverable error and retry, potentially triggering repeated overflow. The fix is typically to rewrite the recursive procedure iteratively, using an explicit stack data structure, or to increase the CINTCODE stack depth configuration if the recursion depth is bounded but the default stack is too small. Work log entry: “TRACE.ROUTE procedure: recursive traversal of routing tree; tree depth can reach 32 levels on complex configurations; CINTCODE default stack of 1000 words exhausted at depth 21 (each frame uses 47 words); interpreter returned stack-overflow error code; caller retried indefinitely; fix: rewrote TRACE.ROUTE iteratively using an explicit 32-element work queue; stack overflows: recurring → 0; 6h.”

Track BCPL developer retainer hours without the status emails

When a 3-hour session diagnoses a hardcoded word-count bug in CALC.CHECKSUM — reading the packet structure definition, counting the word offsets, finding the literal 8 inside the function body that should be 9, verifying the fix across all packet types, and confirming that checksum mismatches drop to zero — the work log must name the function, the wrong constant, the missed field, and the checksum mismatch count before and after. HourTab gives your BCPL retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the function and the constant. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks BCPL developer retainer hours

BCPL retainer work is invisible by the same mechanism that makes hardcoded field-count bugs dangerous: the BCPL compiler sees no inconsistency between a packet structure with 9 MANIFEST field constants and a function that iterates over a literal 8. The two facts live in different places in the source code. There is no reference between them. No diagnostic, no warning, no error. The function runs correctly for packets where the 9th field is zero, and runs incorrectly for packets where the 9th field is non-zero — and the incorrectness is a wrong checksum value, not a fault or an exception. The wrong checksum propagates to the remote endpoint, which rejects the packet. The symptom is a low-rate remote rejection that is easy to attribute to network noise rather than a software defect, delaying diagnosis by days or weeks.

The work log needs to name the mechanism: which function, which hardcoded constant, which MANIFEST field offset was missed, what the observable symptom was (checksum mismatch count, packet rejection rate, remote endpoint error code), and what the fix was. A log entry that says “fixed checksum bug, 3h” is not auditable. A log entry that says “CALC.CHECKSUM: loop iterated over literal 8 words; packet extended to 9 words with PKT.SEQNO at V!8; word 8 not summed; symptom: 4 checksum mismatches/hr at remote endpoint; fix: literal 8 updated to 9; mismatches: 4/hr → 0; 3h” is auditable.

HourTab gives BCPL developers a public retainer-hours URL they send to clients — telecommunications carriers maintaining legacy switching controllers, industrial automation companies maintaining embedded BCPL firmware, and institutions with decades-old BCPL codebases where the cost of full translation to a modern language exceeds the cost of continued maintenance. For BCPL retainers, each work log entry should name the mechanism: which function, which hardcoded literal, which MANIFEST constant was inconsistent, which operator was misused. Comparative context: BCPL retainer work has conceptual overlap with other low-level and legacy language environments — assembly language retainers share the characteristic that both languages operate below the C abstraction level, and assembly retainers similarly involve implicit dependencies (register use, instruction side effects) that are invisible without understanding the execution model; ALGOL retainers cover a language from the same 1960s era, and BCPL’s CPL ancestor traces directly to ALGOL, making ALGOL and BCPL maintenance contexts the closest historical relatives in the legacy language space; and COBOL retainers involve similar fixed-field arithmetic hardcoded in legacy programs — when a COBOL record layout changes and a COMPUTE or MOVE statement references a field by an old position constant, the same class of mismatch occurs that the BCPL CALC.CHECKSUM bug represents, with the same root cause: a hardcoded structural assumption that was not updated when the underlying structure changed.

FAQ: BCPL developer retainers

What does a BCPL developer on retainer typically do?

A BCPL developer on monthly retainer covers hardcoded word-count bug diagnosis (identifying where a function like CALC.CHECKSUM iterates over a literal constant for the old packet word count and misses newly added fields; the fix is updating the constant and verifying all call sites); byte vs word addressing misuse (identifying where % byte-indirect is used where ! word-indirect is required, producing wrong data reads from packet structures); GLOBAL vector number collision analysis (identifying where two modules define globals at the same numbered slot, causing one module’s global to overwrite another’s at runtime); CINTCODE stack overflow diagnosis in recursive procedures; and BCPL to C migration support (mapping BCPL’s typeless word model to C int and pointer types, with careful attention to the places where BCPL treats an address and the integer value of that address as the same thing, while C distinguishes int * from intptr_t).

What BCPL work is most commonly underlogged?

Hardcoded word-count bugs are the most systematically underlogged BCPL retainer work. When a packet structure gains a new field, MANIFEST constants naming field offsets are typically updated because they are near the structure definition. Iteration constants — the number of words to loop over — are separate literals that may appear only in a function body, not near the structure definition. CALC.CHECKSUM iterating over a literal 8 misses a 9th field without any compiler error, type error, or runtime fault. The checksum simply omits the last word. The bug is latent until a packet arrives where the 9th word is non-zero — which may be infrequent in cold-start testing and only apparent when comparing checksums with a remote endpoint. Diagnosis requires reading the function body, counting the word offsets in the structure definition, and comparing the iteration literal to the structure size. 3 to 5 hours invisible per occurrence.

What are typical BCPL developer retainer rates?

Entry-level BCPL developers with experience in typeless word model maintenance and MANIFEST constant management typically bill at $55 to $95 per hour. Mid-level BCPL programmers with experience covering CINTCODE virtual machine debugging, byte vs word addressing diagnosis, and GLOBAL vector collision analysis typically bill at $80 to $145 per hour. Senior BCPL developers with deep knowledge of BCPL’s CPL and ALGOL ancestry, BCPL to C migration patterns, and legacy telecommunications or embedded system maintenance typically bill at $115 to $210 per hour. Monthly retainer ranges: $1,200 to $2,400 per month for advisory engagements covering hardcoded constant audits and packet structure change review (8 to 16 hours per month); $2,000 to $5,000 per month for active legacy maintenance including CINTCODE debugging, GLOBAL vector audits, and BCPL to C migration work.

What should a BCPL developer retainer agreement include?

A BCPL developer retainer agreement should specify: platform and word size scope (the BCPL WORD type maps to the machine word width; a 32-bit platform gives 32-bit words, a 16-bit platform gives 16-bit words; this affects all offset arithmetic and checksum computations); CINTCODE vs native compilation scope (the original Martin Richards CINTCODE virtual machine vs. BCPL compiled to native code on a specific target; CINTCODE stack overflow behaviour differs from native stack behaviour); GLOBAL vector scope (which module owns which global vector slot numbers; collisions are not detected at compile time and must be audited manually; the agreement should specify whether cross-module GLOBAL audit is in scope); operator scope (! for word-indirect addressing; % for byte-indirect addressing; @ for address-of; MANIFEST and STATIC declaration maintenance); and BCPL to C migration scope (whether the retainer covers mapping BCPL word values to C int vs pointer types; this is the most error-prone part of migration because BCPL vectors and their addresses are the same value, while C int * and intptr_t are distinct types).

How should BCPL developer retainer hours be logged?

Log each BCPL retainer session with: the function name where the bug was diagnosed (e.g., CALC.CHECKSUM); the literal or MANIFEST constant that was wrong (e.g., iteration count of 8 when the packet structure had grown to 9 words); the field that was missed (e.g., the 9th word at offset V!8, the newly added PKT.SEQNO field); the symptom (e.g., 4 checksum mismatches per hour at remote endpoint); and the fix (e.g., updated literal 8 to 9 inside loop body; checksum mismatches: 4/hr → 0). For byte vs word addressing bugs: the function name, the operator used (% or !), the operator required, the wrong value produced, and the fix. For GLOBAL vector collisions: the two module names, the shared global slot number, the two different globals assigned to that slot, and the resolution (renumbering one module’s globals). For CINTCODE stack overflow: the recursive procedure name, the recursion depth at overflow, and whether the fix was an iterative rewrite or a stack depth increase.