How Did B Language Syntax Differ From C?

The transition from Ken Thompson's B programming language to Dennis Ritchie's C marked the foundation of modern systems programming. While C directly inherited B's expression-driven design, brace-delimited blocks, and control-flow keywords like if, while, and return, it diverged sharply in syntax to accommodate hardware evolution. Most notably, C introduced explicit data types, complex pointer declaration syntax, and preprocessor directives, completely replacing B's typeless single-word memory model, archaic assignment operators, and runtime vectors.

Typeless Declarations vs. Explicit Type Systems

The most fundamental syntactic distinction lies in how variables and functions are declared. B was a typeless language designed around a single architectural machine word. Every variable, parameter, or array cell held one word of data, leaving the interpretation of values entirely to the operators acting upon them.

Declarations in B used the auto keyword for local stack variables and extrn for external variables:

auto x, y, z;
extrn buffer;

Because B lacked primitive types like char, float, or double, variable definitions contained no type specifiers. When Dennis Ritchie developed C, the primary goal was to support byte addressing on the PDP-11 architecture. Consequently, C replaced B's typeless declaration style with mandatory type keywords:

int x, y;
char z;
extern char *buffer;

Array Allocation and Pointer Mechanics

In B, array declarations had explicit allocation semantics tied directly to memory pointers. Defining an array v[10] created a dedicated named pointer variable v initialized to point to a contiguous block of 10 words. Accessing an element relied on the vector evaluation rule where v[i] translated syntactically and semantically to *(v + i).

auto v[10];
v[2] = 5;

In contrast, C eliminated the dedicated pointer variable allocated for arrays. In C, declaring int v[10] allocates only the array storage itself. The identifier v automatically decays to a pointer to its first element in expressions, removing the overhead of maintaining an independent pointer word in memory. Furthermore, C formalized pointer dereferencing with dedicated unary operators (* and &) during declaration and usage, whereas B relied heavily on unary * (indirection) without explicit pointer types.

Compound Assignment Operators

B inherited assignment operators directly from BCPL, which led to syntactic ambiguity. In B, compound assignments placed the assignment operator before the arithmetic operator:

  • =+ meant add and assign
  • =- meant subtract and assign
  • =\* meant multiply and assign

This order frequently created lexer ambiguities. For example, the expression x =- 1 could be parsed either as subtracting 1 from x (x = x - 1) or as assigning negative one to x (x = -1). To resolve this parser confusion, C flipped the syntax order to the modern format used today:

x += 1;
x -= 1;
x *= 2;

Function Definitions and Return Syntax

Function headers in B did not specify return types or parameter types. Parameters were listed in parentheses, and local declarations occurred inside the function body using auto:

sum(a, b) {
    auto total;
    total = a + b;
    return(total);
}

In early K&R C, functions gained return types, and parameter declarations were placed between the parameter list and the opening brace:

int sum(a, b)
int a, b;
{
    int total = a + b;
    return total;
}

Modern C further standardized this into ANSI/ISO function prototypes where types accompany parameters directly inside the parentheses. Additionally, B typically enclosed return values in parentheses as return(expr);, whereas C formalized return as a keyword where parentheses around the returned expression are optional.

Strings, Characters, and Macro Handling

B handled character data by packing multiple characters into a single machine word. Single quotes in B defined string-like constants that packed characters directly:

x = 'hello';

Because B lacked an integrated preprocessor, constant replacement, file inclusions, and macro evaluations had to be handled externally or via language-specific built-ins. C separated single characters from character strings by enforcing single quotes for individual character literals ('a') and double quotes for null-terminated byte strings ("hello"). C also introduced the #include and #define preprocessor syntax, formalizing macro expansion and header inclusion directly into the compilation pipeline.