But really in my experience the best way to get better at compilers (I can't claim to be a wizard) is to just build a goddamn compiler. Start by writing a parser (you can crib from Crafting Interpreters), writing the simplest possible typechecker for arithmetic (crib from Hindley Milner), then a code generator to whatever target you want. Code generation is tricky, but if you're generating arithmetic, it's really not that bad.
If at any point you get confused or have no clue what to do, take a deep breath and guess! Yep, just guess how to do it. I've done this quite a few times and later learned that my guess was really a dumbed down version of idk, solving the AST typing problem, or lambda lifting, or whatever. If that's too hard, scroll through other compilers and figure out what they do.
Once you get something working, celebrate! It's pretty cool seeing even arithmetic end up as code generated and run.
Then start adding other features! Add local variables. Add nested functions. Add whatever.
The secret is to treat a compiler as a challenging software development project. Because that's what it is. Start writing code and figure it out.
Granted this is not great advice if you want a usable compiler at the end. I'm just trying to learn, not make something for people to use.
Yeah I would call this the engineering approach (matrices) vs the mathematical approach (algebra).
I took like 3-4 courses in the US involving the engineering approach, starting in high school and continuing through the college as a CS major. That was all that was required.
But I also like algebra, so I happened to take a 400-level course that only math majors take my senior of college. And then I got the group theory / vector space view on it. I don't think 95% of CS majors got that.
I don't think one is better than the other, but they should have tried to balance it out more. It helps to understand both viewpoints. (If you haven't seen the latter, then picture a 300-page text on linear algebra that doesn't mention matrices at all. It's all linear transformations and spaces.)
What country were you taught in? Wild guess: France?
Sounds like a bank, a government agency, or any business over a certain size.
I tell people you only call it politics when you are losing. More accurately, it's a layer of literal stupidity above the competent to shield the money side of the company from the leverage that operations people would have if they had any information about how the money side worked.
Instead of a hierarchy, rethink a company as a hub and spoke model with concentric rings. The main differences are the implication in a hierarchy that there is "gravity," keeping people down and that they need energy and leverage to climb "up," which further implies there is a place to "fall," and that there is only one way "up," instead of many possible paths to the centre from all directions. There is no gravity, only gates and barriers, and even these are just information. Politics is how a middle manager runs interference and creates distractions to make sure you can't see over, around, or through them, and that the people behind them closer to the money can't see you. Tech is usually outside the main perimeter, mediated by contracting companies or middle managers whose job is to compartmentalize the value people create, and be sure it is replaceable.
Viewed this way, of course this demented political farce is how Apple works, because it's how everything seems to work when you have internalized the precise and specific mental model someone uses to take advantage of you.
Sorry if you can't unsee it now, but hopefully it will be funny and we can get good, competent people who value tangible skills into positions of power.
You can turn an interpreter into a compiler by replacing all code that actually does something by code that prints out the code that does it in the target language. It takes a bit to wrap your head around it, and you won't get an optimizing compiler, but a compiler it will be.
So your values are no longer values in the interpreter's language, but descriptions in the target language for getting that value. To compile an expression, you first handle the sub-expressions, as in an interpreter, which prints the code to compute them. Additionally, you get such a value description for the return value of each expression.
Then you can use those descriptions to print out code to get them into known locations (e.g. registers). Then you can print out code to perform your operation on the values in those locations and put the result in another ___location (e.g. on the stack). The return value of this compilation step is the description of that ___location.
It's interesting to watch the swings and roundabouts in the VM engineering space. I remember many years ago being in a Google engineering all hands where Android was first announced (to the firm) and the technical architecture was explained. I and quite a few others were very surprised to hear that they planned to take slow, limited mobile devices and run a Java bytecode interpreter on them. The stated rationale was also quite surprising: it was done to save memory. I remember being very dubious about the idea of running a GCd interpreted language on a mobile phone (I had J2ME experience!).
In the years since I've seen Android go from interpreter, to interpreter+JIT, to AOT, to JIT+AOT, to interpreted + JIT then AOT at night. V8 has gone from only JIT (compile on first use) to multiple JITs to now, interpreter+single JIT. MS CLR still doesn't have any interpreter and is fully AOT or JIT depending on mode. HotSpot started interpreted, then gained a parallel fast JIT, then gained a parallel optimising JIT, then went to a tiered mechanism where code can be interpreted, compiled and recompiled multiple times before the system stabilises at peak performance.
Looking back, it's apparent that Android's and indeed Java's initial design was really quite insightful. A tight bytecode interpreter isn't quite as awful as it sounds, especially given how far CPU core execution speed has raced ahead of memory and cache availability. If you can fit an interpreter almost entirely in icache, and pack a ton of logic into dense bytecode, and if you can use spare cores that would otherwise be idle (in desktop/mobile scenarios) to do profile guided optimisation, you can end up utilising machine resources more effectively than it might otherwise appear.
* Lambda calculus evaluator
* Hindley Milner typechecker for lambda calculus
* Stack based calculator (extend with variables)
* Play around with macros
* Work through Crafting Interpreters
But really in my experience the best way to get better at compilers (I can't claim to be a wizard) is to just build a goddamn compiler. Start by writing a parser (you can crib from Crafting Interpreters), writing the simplest possible typechecker for arithmetic (crib from Hindley Milner), then a code generator to whatever target you want. Code generation is tricky, but if you're generating arithmetic, it's really not that bad.
If at any point you get confused or have no clue what to do, take a deep breath and guess! Yep, just guess how to do it. I've done this quite a few times and later learned that my guess was really a dumbed down version of idk, solving the AST typing problem, or lambda lifting, or whatever. If that's too hard, scroll through other compilers and figure out what they do.
Once you get something working, celebrate! It's pretty cool seeing even arithmetic end up as code generated and run.
Then start adding other features! Add local variables. Add nested functions. Add whatever.
The secret is to treat a compiler as a challenging software development project. Because that's what it is. Start writing code and figure it out.
Granted this is not great advice if you want a usable compiler at the end. I'm just trying to learn, not make something for people to use.