The first programs were written in binary, to be executed on computers with a very limited amount of things they could do. Let’s imagine a basic computer; having an example will help. We’ll call it Basic Computer, or BC for short.
Here’s an image I’m going to use, even though it’s actually more advanced than our hypothetical Basic Computer. See how it has those three ports where an input signal can come in, and very obvious lines drawn to microchips, which modify the signal, and then draw more lines to other chips, eventually culminating in two outputs? BC is like that, except it can take a bit more input and output, and instead of microchips, it has really basic physical circuits, and a lot more of them.
BC can do, let’s say, six things: PRINT, ADD, MULTIPLY, DIVIDE, STORE, READ. BC is a calculator! (Subtraction is just negative adding.) BC also doesn’t handle input or output in ways you’d understand. It’s a basic machine – it has places where wires can be soldered on, and those wires will either have current or not. For the input wires, we determine the input by either running or not running current to specific wires. For the output, we detect if the output wires have current or not.
Current, or no current. On, or off. 1 or 0. Binary. We can now represent all our inputs and outputs in binary, with each digit corresponding to a wire.
When BC gets its inputs, it sends them on to a series of logic gates. The first one it sends it to reads the first input, and sends the remaining inputs to one of six other series of circuits. Each of these circuits – the PRINT, ADD, MULTIPLY, DIVIDE, STORE, and READ sections – can only do that one thing. They take inputs, the inputs run through a series of logic gates, which makes the correct output happen. They do not “think” about what they’re doing. They do the only thing they can do. These functions are hard-wired (or, as we more commonly say today, hard-coded) into the machine.
An instruction to BC might therefore look like this: 010001001100. Which, split into 3-bit chunks, reads 010 001 001 100, or translating into decimal, 2 1 1 4. When those inputs are fed into BC, it will read the first 3 bits, see a “2”, strip those 3 bits off, and send the remaining bits to the circuit it’s told to send inputs to when it sees a “2”. Which, in this case, happens to be the ADD function. It will take those bits and store the result of 1+1 in memory location 4. I can write as many instructions in sequence as I like. This is a program. I have written it in machine language – 0s and 1s that represent the actual voltage sequences to be sent to the physical machine.
Now, you may be saying, that’s not much of a language at all. How did we get from there to programming languages?
Well, for that, poor BC isn’t enough. Let’s imagine a more Advanced Computer, called AC. AC has more functions than BC. It can store data larger than 3 bits. But it still only takes machine language. Which isn’t great, especially since now there’s more functions and more bits to write out! But AC can do some logic and replace text. Remember my BC example? “2 1 1 4” and “010001001100” are exactly the same string, just one is formatted so it’s easy for humans to read and one is formatted so it’s easy for a computer to read. In that example, I did the replacements myself. Using machine language, is it possible to make a program that reads an input consisting solely of repeated numbers in decimal format, and outputs their binary equivalent without the spaces?
As it turns out, yes. It is possible. Which means that I can take that program, give it my decimals as an input, and then take the output and feed it directly back into AC, which will recognize it as a valid program and execute it. Which means I have just invented a compiler. A compiler is a program written in a lower level language which is capable of taking programs written in a higher level language and converting them to its language. What I’ve written so far isn’t much of a higher level language; it’s just decimals.
But compilers can stack. Now that I have my decimal compiler, can I write a program in decimal to recognize command words and translate them to their decimal equivalents? I absolutely can. In fact, I can do more than that. I can have my compiler expect to see those command words in certain formatting. Now I can have something like “ADD 1 1 = 4”. I’ll compile that, which will result in “2 1 1 4”. I can then take that output, feed it to the decimal compiler, get “010001001100”, and feed that to the computer, and have it execute successfully. And I can then take that, and go further! What if I want to use the plus sign to indicate the ADD command, with the two numbers to be added on either side of it? I can do that! And, y’know, for short programs it’s okay, but for longer ones it’s gonna be pretty hard for everyone to keep track of what’s being stored in memory location 4. Can I tell my compiler that I’m going to give memory locations a nickname, and whenever it sees that, to replace it with the location? I sure can.
Now I’ve got something like this:
Which will compile to “ADD 1 1 4”, which will compile to “2 1 1 4”, which will compile to “010001001100”, which will execute.
And so programming languages are born.