Why do negative array indices make sense?
The array indexing operation a[i] gains its meaning from the following features of C


The syntax a[i] is equivalent to *(a + i).  Thus it is valid to say 5[a] to get at the 5th element of a.
Pointer-arithmetic says that given a pointer p and an integer i, p + i  the pointer p advanced by i * sizeof(*p) bytes
The name of an array a very quickly devolves to a pointer to the 0th element of a


In effect, array-indexing is a special case of pointer-indexing.  Since a pointer can point to any place inside an array, any arbitrary expression that looks like p[-1] is not wrong by examination, and so compilers don't (can't) consider all such expressions as errors.

Your example a[-1] where a is actually the name of an array is actually invalid.  IIRC, it is undefined if there's a meaningful pointer value as the result of the expression a - 1 where a is know to be a pointer to the 0th element of an array.  So, a clever compiler could detect this and flag it as an error.  Other compilers can still be compliant while allowing you to shoot yourself in the foot by giving you a pointer to a random stack slot.

The computer science answer is:


In C, the [] operator is defined on pointers, not arrays.  In particular, it's defined in terms of pointer arithmetic and pointer dereference.
In C, a pointer is abstractly a tuple (start, length, offset) with the condition that 0 &lt;= offset &lt;= length.  Pointer arithmetic is essentially lifted arithmetic on the offset, with the caveat that if the result of the operation violates the pointer condition, it is an undefined value.  De-referencing a pointer adds an additional constraint that offset &lt; length.
C has a notion of undefined behaviour which allows a compiler to concretely represent that tuple as a single number, and not have to detect any violations of the pointer condition.  Any program that satisfies the abstract semantics will be safe with the concrete (lossy) semantics.  Anything that violates the abstract semantics can be, without comment, accepted by the compiler and it can do anything it wants to do with it.