L'auteur, Sebastian, a étudié la première édition de "The C Programming Language", le document de justification du C89 et les sources du compilateur C de la 7e édition d'Unix pour documenter le dialecte pré-ANSI appelé K&R C. Il note que la grammaire contextuelle, si décriée, semblait moins absurde dans son contexte originel : le premier compilateur C fonctionnait en une seule passe sur des machines à mémoire réduite, le parseur suivait déjà les identifiants et les portées. La portée des prototypes de fonction n'existait pas encore, ce qui évitait les problèmes de portée modernes qui rendent la grammaire pénible aujourd'hui.
Le système de types était très différent. void était une invention de l'ANSI ; les fonctions renvoyaient implicitement int, et char * servait de pointeur générique car tous les pointeurs étaient assignables entre eux. long double n'existait pas, mais long float était synonyme de double. Le mot-clé signed n'existait pas ; seul unsigned était disponible, et il agissait avec short et long comme adjectif de int. Un seul adjectif était permis, donc long int était valide mais pas long ni unsigned short int. L'ordre était libre, si bien que int long passait aussi. La promotion des entiers était également non standardisée : les compilateurs divergeaient entre les règles "unsigned-preserving" et "value-preserving". Avec la première, -1 < (unsigned short)0 était faux car les opérandes étaient promus en unsigned int ; avec la seconde, c'était vrai car les valeurs de unsigned short tiennent dans signed int. Le comité C89 a choisi value-preserving, malgré la tendance des compilateurs Unix dans l'autre sens.
Les constantes et les littéraux réservaient d'autres surprises. Il n'y avait pas de suffixe U, et en fait aucune constante unsigned. Une constante non décimale tenant dans unsigned int recevait pourtant le type int, si bien que 0x8000 sur un int 16 bits devenait négatif. Les constantes octales acceptaient 8 et 9 comme chiffres, donc 078 et 0100 avaient la même valeur, et cela valait aussi pour les séquences d'échappement octales. Plusieurs séquences d'échappement manquaient : \a, \v et \x n'existaient pas. Une séquence d'échappement invalide n'était pas une erreur ; la barre oblique inverse était silencieusement supprimée, ce qui faisait que \" et \? fonctionnaient déjà en pratique avant d'être standardisés. Les constantes flottantes étaient toujours double ; aucun suffixe f ou l n'existait.
Parce que int était conçu comme le mot machine, les opérations qui aujourd'hui donnent size_t ou ptrdiff_t donnaient int. size_t et ptrdiff_t eux-mêmes sont des ajouts de l'ANSI. Il s'ensuivait qu'aucun type ne pouvait dépasser INT_MAX. const et volatile étaient absents. Les brouillons du C89 incluaient un qualificateur noalias, mais Dennis Ritchie a argumenté qu'il était inutilisable et il fut retiré avant publication. Les premières impressions de la deuxième édition de K&R mentionnaient encore noalias, supprimé dans les impressions suivantes. Les littéraux chaîne étaient définis comme des objets distincts et mutables, ce qui explique pourquoi ils ont encore le type char * aujourd'hui : const n'existait pas, donc l'ajouter aurait cassé le code existant.
Quelques mots-clés réservés mais inutilisés apparaissent. entry était réservé sans jamais apparaître dans la grammaire. Les continuations de ligne par une barre oblique inverse en fin de ligne n'étaient autorisées que dans les constantes caractère, les littéraux chaîne et les directives de préprocesseur ; le comité ANSI les a permises partout parce que c'était plus simple. Les prototypes de fonction avec des listes de paramètres typés étaient une invention de l'ANSI. En K&R C, les déclarateurs de fonction utilisaient des parenthèses vides, et seules les définitions listent les noms de paramètres. Il était donc impossible de déclarer une fonction variadique ; d'où le changement silencieux causé plus tard par l'exigence d'un prototype explicite pour printf. La grammaire des définitions de fonction dans le livre K&R est aussi erronée : elle décrit un déclarateur normal suivi d'une liste de paramètres, ignorant la précédence en spirale habituelle des déclarateurs. La grammaire de #line est également fausse, exigeant un identifiant obligatoire là où un littéral chaîne devrait se trouver, et contredisant le texte qui le présente comme optionnel.
Les fonctions et les pointeurs se comportaient différemment. Les paramètres tableau se décomposaient en pointeurs comme en C moderne, mais les paramètres fonction n'étaient pas convertis en pointeurs ; ils étaient simplement interdits. Les pointeurs de fonction ne pouvaient pas être utilisés directement dans les expressions d'appel ; une déréférencence explicite était nécessaire. L'ajustement implicite des paramètres float vers double ne se produisait que dans la forme K&R, pas dans la forme prototype, et est resté dans la norme jusqu'à ce que C23 supprime la forme K&R. L'opérande gauche d'un décalage était une exception aux promotions arithmétiques habituelles : en C standard, 1 << 1L a le type int, alors qu'en K&R l'opérande gauche était promu à long.
Les structs et unions étaient bien plus limitées. On ne pouvait pas assigner à un valeur-gauche de struct ou d'union, les fonctions ne pouvaient ni les recevoir ni les renvoyer, et leurs initialiseurs devaient être des littéraux composés entre accolades. Ces initialiseurs composés ne pouvaient pas servir pour les structs auto ni pour les unions, donc les structs de pile et les unions ne pouvaient être initialisées que champ par champ. Tomber à la fin de main n'était pas le cas particulier introduit par C99 ; en C89, la valeur de retour était indéfinie, bien que les exemples de K&R impliquent que les implémentations le traitaient comme un succès.
Le préprocesseur et les opérateurs avaient leurs propres anachronismes. Les affectations composées comme += étaient lues comme + et =, deux jetons séparés. L'ancienne forme =+ restait un seul jeton pour garder des expressions comme x = -1 non ambiguës. La concaténation de chaînes n'existait pas, et les opérateurs # et ## étaient absents, rendant beaucoup de macros impossibles ; certaines implémentations contournaient cela en expansant les macros à l'intérieur des littéraux chaîne. Les macros prédéfinies telles que __DATE__, __FILE__, __LINE__, __TIME__ et __STDC__ n'existaient pas, et les identifiants commençant par deux tirets-bas n'étaient pas documentés comme réservés. Les directives #pragma, #error et #elif manquaient ; il n'y avait pas d'opérateur defined, donc on testait l'existence avec #ifdef et #ifndef. Une liste rapide d'autres différences : les flottants étaient promus à double dans toutes les opérations arithmétiques ; l'expression médiane d'un opérateur ternaire ne pouvait pas être une expression virgule ; le plus unaire n'existait pas ; il n'y avait pas de constantes caractère ou de littéraux chaîne larges préfixés par L, ni de wchar_t ; et les enums n'existaient pas du tout.