ещё скажу, что на «построение компиляторов» я забил большой и тяжёлый; внутри компилятор состоит из огромной кучи специализированых AST-нод. причём на первом этапе текст парзится в ноды типа `NodeId`, `NodeDot`, без уточнений. и только когда всё отпарзилось — по всему дереву запускается `Node.resolve()`. резолвер возвращает новую ноду (или самого себя, если так подходит). те же `NodeId` превращаются в `NodeGlobalVar` или `NodeLocalVar`, точечки — в `NodeField`, и так далее. и на третьем этапе у нас уже гарантировано валидное дерево, где у каждой ноды указан правильный тип, так что спокойно делаем `Node.emitLIR()`.

конечно, это значительно медленней, чем простой однопроходный компилятор. зато независимые части чётко отделены друг от дружки, и в том же парзере не путается под ногами код для построения и проверки таблиц символов, например. равно как и `emitLIR()` никакими символами уже не интересуется: там все адреса и смещения уже решены.

`emitLIR()` тут легко заменяется на что угодно другое, хоть на `emitSSA()` (SSA потом, правда, придётся канонизировать, но это мелочи). и некоторые оптимизации можно делать прямо по дереву (хоть и не очень удобно).

собственно, и CP2 надо было бы переписать таким образом, но: «работает — не трогай!»

одно из достоинств/недостатков подобного подхода — никакие forward declarations больше не нужны. я пока не решил, энфорсить правило «всё должно быть объявлено заранее» или нет. с одной стороны мне правило нравится, а с другой — это рудимент однопроходности же.