Speaking as someone who loves writing compilers, I’m intrigued by this idea, and would like to probe it. Please, let me invite you to my mental model.
A language is an assignment of semantics to syntax. A compiler is a transformation from syntax in one language to syntax in another (maybe the same) language that preserves the semantics.
With this framing, a bunch of common tools are seen to be special cases of compilers:
- An optimizer takes one program and spits out a new one with the same behavior (semantics), but that generally runs faster than the original program.
- An obfuscator is a compiler whose output is harder to understand than its input.
- A minifier is a compiler whose output is smaller than its input.
- A prettifier is a compiler whose output is more consistently formatted than its input, and maybe more aesthetically pleasing.
- An assembler is simply a compiler whose output language is called “assembly”, and likewise for a disassembler.
- A decompiler is just a compiler.
But note that I haven’t said anything about programming languages. As
formulated, this applies to natural languages, too, and it applies to
languages that aren’t programming languages or natural languages. The
GraphQL Mirror module has a method extract
that transforms a subset of
a SQL database into a JavaScript object graph, preserving the associated
semantics at each end. The TensorBoard data_compat
module exposes a
function that transforms “v1-style summaries” to “v2-style summaries”
while preserving their semantics. These are compilers, and this
generalization is critical.
A while ago, on the topic of whether various interactions constitute transactions, @decentralion said:
This is an astute point, and I’d like to refine it a bit. Let’s move up a level. A definition is useful if you can reason about generic instances of the term and apply that reasoning to specific instances. An instance of the definition is useful if the high-level reasoning transfers faithfully and provides insight.
For instance, the above definition of compiler is useful because we can make the general statement that “compilers compose”: if you have a correct compiler from A to B and a correct compiler from B to C, then you also have a correct compiler from A to C, and you know exactly what it does. The classifications of, say, obfuscators and optimizers as compilers are both useful because we now know what happens if you take a program, run it through an optimizer, and then run that through an obfuscator: you have a semantically equivalent program that likely runs faster and is harder to read, because compilers compose.
Personally, I derive great value from this broad definition of compiler. When I want to write a compiler, I know how to structure its internals, how to test its correctness, what it even means for it to be correct, how it should interface with outside systems. In fact, I find this conceptual framing so useful that whenever I’m working, I’m generally running a low-priority background thread called “find the compiler”, in which I try to identify how the problem that I’m trying to solve can be expressed in this framework, after which it becomes basically a “known problem” whose solution I just have to implement.
So: Is SourceCred an incentive compiler? That depends. What language is it compiling from, and what’s it compiling to? What semantics is it preserving, and what non-semantic transformations is it making, if any? And how does this help us reason about what it is or should be?
Some references for further reading:
- Rich Programmer Food (Steve Yegge, 2007); although I don’t with everything in this post, it has some valuable insights
- A recent Stack Overflow post (Jörg W Mittag, 2017) that summed up some of my thoughts quite well
- The Galois connection between syntax and semantics (Peter Smith, 2010), an introductory formal treatment of some related concepts (h/t Chas Leichner for the reference)
(Exercise for the interested reader: In the context of programming, what are “compiled languages” and “interpreted languages”?)