Originally Written: January 20th, 2013
This week was eventful. “Gödel, Escher, Bach” (GEB) arrived and I’ve started reading it, and I’ve made my first practical foray into low level programming using the Common Intermediate Language (CIL) by being able to create a patch for an application without having the source code.
I promise there is a relation. 😅
Gödel, Escher, Bach
I’m still in the lengthy 20th anniversary preface written by Hofstadter and it’s already very thought provoking. He begins discussing some basic tenets of symbology.
Symbols don’t have inherent meaning, and only in context can a meaning be assigned or interpreted from them. The easiest example of this is language. Before the Rosetta Stone was found, Egyptian Hieroglyphs had no meaning to anyone alive at the time.
The best we could possibly do, is attempt to use the context of “being a human on earth” to determine their meanings, and treat all the hieroglyphs as pictograms.
Once the Rosetta Stone was discovered and used to translate the hieroglyphics, we had a solid context to give the symbols meaning. A pictograph of an eye that once meant “eye” could now mean “modify the previous symbol to be associated with sight”1.
Thus without context, a symbol has no meaning.
Not all symbols are pictographic in nature. A symbol can be in any form, and can even exist in an abstract form completely. In it’s most basic form, a symbol is just information, and information can take many forms. Pictographs rely on light information, verbal language relies on sound waves, and computers store information using electricity.
Symbols can reference other symbols. The letters you’re reading on the screen right now are symbols referencing the data stored in the computer system serving this page. Patterns can emerge in these sets of symbols, and those patterns themselves are symbols.
Hofstadter speaks of consciousness as a strange loop. It’s a set of symbols, that have formed a pattern of interpreting and referencing other symbols.
The concept/symbol of “I”, the entity that is you when you refer to yourself, is a symbol interpretation feedback loop, when the pattern of symbols attempts to interpret itself in a self-referential loop.
This is the Ego or Software of our Mind.
That’s my attempt to summarize what I’ve read/understood so far in GEB. And I’m only 9 pages into the preface of this 792 page book. This will be an amazing read.
Relation to my adventures with the Common Intermediate Lanaguage
So how does all that apply to my activities with CIL?
The ability to understand and conceptualize the same type of abstract symbols is required. Software development is filled with them. Layers of abstractions on top of even more layers of abstractions requires being able to navigate the labyrinth of symbols to create something with a meaningful purpose.
High level and low level languages are descriptions used to tell you (in unspecific terms) how far removed you are from the underlying system. The more layers of abstraction between you and the hardware, the higher level the language is.
High Level Example (C#)
In a high level language, you might write something like this:
// Count to 10
for (int i = 1; i <= 10; i++)
Console.WriteLine(i.ToString());
Low Level Example (CIL)
This is the same code in CIL when the C# version is compiled by Visual Studio. I added comments to make this more readable. nop will not have comments though, since it is the “no operation” opcode, and does nothing. These are generated by compilers for various reasons.2
.locals init (
[0] int32 num, // Create a local scope variable integer. This is "int i" from above code.
[1] bool flag) // Create a local scope variable boolean.
L_0000: nop
L_0001: ldc.i4.1 // Push the value 1 onto the stack3.
L_0002: stloc.0 // Pop the value on top of the stack, and store it in the local variable at index 0. This and the above line are "i = 0" from the above code.
L_0003: br.s L_0018 // Go to instruction L_0018.
L_0005: nop
L_0006: ldloca.s num // Push a pointer to variable named "num" onto the stack.
L_0008: call instance string [mscorlib]System.Int32::ToString() // Pop the pointer off the stack, and call the method ToString for the object instance the pointer points to, and pushes resulting string onto the stack. This and the above line are "i.ToString()"
L_000d: call void [mscorlib]System.Console::WriteLine(string) // Pops the string off the stack and passes it as the parameter to WriteLine. This and the above two line are "Console.WriteLine(i.ToString());
L_0012: nop
L_0013: nop
L_0014: ldloc.0 // Pushes the value of the local variable at index 0 onto the stack.
L_0015: ldc.i4.1 // Pushes the value 1 on to the stack.
L_0016: add // Pops the first two values off the stack, adds them together, and pushes the result back onto the stack.
L_0017: stloc.0 // Pops the value on top of the stack, and store it in the local variable at index 0. This and the above 3 lines are "i++" from the code above.
L_0018: ldloc.0 // Pushes the value of the local variable at index 0 onto the stack.
L_0019: ldc.i4.s 10 // Pushes the value 10 onto the stack.
L_001b: cgt // Pops the first two valuess off the stack. If the first value is greater than the second value, push 1 onto the stack, otherwise push 0 onto the stack.
L_001d: ldc.i4.0 // Push the value 0 on to the stack.
L_001e: ceq // Pops the first two values off the stack. If they are equal, push 1 onto the stack, otherwise push 0 on to the stack.
L_0020: stloc.1 // Pop the value pushed by L_001e off the stacked and store the value in the local variable at index 1.
L_0021: ldloc.1 // Push the value of local variable at index 1 onto the stack.
L_0022: brtrue.s L_0005 // Pop the value on top of the stack, and if it's not zero, go to L_0005. Otherwise continue to next line. This and the above 7 lines are "i <= 10" from the above code.
L_0024: ret // End of function
As you can see, the high level C# is much easier for a human to read. This is a great benefit to software development, and the prime reason high level languages exist. Development in a high level language trades power over the system for increased speed of development.
The reason CIL exists though, is because a computer’s cpu is a very primitive symbol processor4. Our brain is capable understanding far more abstract patterns of symbols than a computer is.
Our ability to process and dereference complex symbols to what they contextually symbolize, is the key to our intelligence. This is what allows us to read and understand the high level C# code, whereas a computer needs it to be translated (compiled) into the extremely limited set of symbols it understands (CIL).
Even this is just a small part of the picture. CIL is an intermediate language. It’s architecturally agnostic. Common Language Infrastructure (CLI) which makes use of CIL was designed to be another abstraction layer from the underlying computer hardware, allowing executable code to run the same on different systems.
Each of those systems however still needs to have a translation engine called the Common Language Runtime (CLR) that converts the CIL into the machine language (set of symbols) that control the hardware. The .NET framework (on Windows), and the Mono framework (Linux/Windows/OS X/iOS/Android) are the system specific implementations of the CLR.
Conclusion
All these sets of symbols, out of context and alone, are meaningless. When these symbols are viewed in context, they create a system that is a large foundation of computer technology, and they obtain meaning/significance when they begin to reference each other.
When reading reviews and summaries of “Gödel, Escher, Bach”, I found out the book has many references to self-reference, recursion and paradoxes.
Meaningless symbols obtaining meaning by referencing each other with functional systems emerging as a result is extremely fascinating.
Notes
[1] http://www.freemaninstitute.com/rosetta.htm
[2] http://stackoverflow.com/questions/234906/whats-the-purpose-of-the-nop-opcode
[3] The stack is where temporary variables that will be used soon are stored. Think of it as a stack of papers, each paper being a variable. There is a rule though, you can only use the paper at the top of the stack, and you can only put other papers on top, not in arbitrary locations. You push Paper A onto the stack, then push paper B onto the stack and you have:
B
A
Then when something that uses the stack for variable storage needs it, it pops the top one off (B) leaving just A. Next thing that needs something from the stack will pop A off the stack and use it. This is an example of the First In, Last Out (FILO) process.
[4] http://en.wikipedia.org/wiki/Turing_machine