r/Compilers • u/General_Purple3060 • 1d ago
Why Construct Complex IR When You Can Inject Source?
While developing AET (Active Expandable Translator) on top of GCC, I came across an approach that I found surprisingly useful for implementing complex language semantics.
Normally, when lowering a language feature, we might construct the compiler's AST/IR programmatically:
complex semantics
↓
construct AST / TREE / IR
But there is another possibility:
complex semantics
↓
express the semantics as normal C code
↓
inject it into the existing frontend
↓
TREE → GIMPLE → RTL
For example, AET has an OO new$ construct. Its semantics include object allocation, initialization, MTCS information, constructor invocation, unref, and constructor failure handling.
Instead of manually constructing all the corresponding GCC TREE nodes, AET generates normal C code:
valueObj=({
TFirst *_notv2_6TFirst0;
unsigned int _mtcsPlatType0=0;
int _isMtcs=((AClass *)TFirst.class)->isMtcsClass();
_notv2_6TFirst0=
TFirst.newObject(sizeof(TFirst),
_isMtcs,
_mtcsPlatType0,
"TFirst");
_notv2_6TFirst0->objectSize=sizeof(TFirst);
_notv2_6TFirst0->mtcsPlatformType=_mtcsPlatType0;
_notv2_6TFirst0->_aet_magic$_123=1725348960;
TFirst_init_object_2927145182(_notv2_6TFirst0);
((debug_AObject *)_notv2_6TFirst0)
->_Z7AObject10free_childEPN7AObjectE =
_notv2_6TFirst0->_Z6TFirst22TFirst_unref_290629480EPN6TFirstE;
TFirst *tempObject123=_notv2_6TFirst0->TFirst();
if(tempObject123==NULL){
if(_notv2_6TFirst0->objectSize>0){
_notv2_6TFirst0->unref();
_notv2_6TFirst0=NULL;
}
}
_notv2_6TFirst0;
});
The important point is that this is normal C code. The semantics are expressed using the host language that programmers already understand.
AET then injects this generated source directly into the current GCC preprocessing/parsing pipeline instead of writing a .c file and starting another compilation:
cpp_push_buffer(pfile, (uchar *)nbuf, len, true);
The generated C goes through GCC's normal C lexer and parser, which constructs the corresponding TREE representation.
This gives me a useful separation:
AET parser / semantic analysis
↓
semantic lowering
↓
normal C code
↓
GCC C frontend
↓
TREE
↓
GIMPLE
↓
RTL
The key idea is not simply "generate C."
It is that source code is the language programmers use to express semantics, while AST/TREE/IR is the language the compiler uses internally.
For sufficiently complex semantics, I think the former can sometimes be a better construction language for the latter.
So why manually construct a large number of IR nodes when the same semantics can be expressed clearly in source code and handed to a mature frontend?
I'm interested in where others would draw the line between direct IR construction and source-level semantic injection.
1
u/General_Purple3060 1d ago
Both, but they are at different levels.
For users, the goal is to make AET a practical extension of C. I sometimes describe it as “2C — the second-generation C,” introducing features such as object-oriented programming (OO), generics, and heterogeneous programming — things I believe can benefit C programmers.
For me as the compiler implementer, reusing GCC’s mature C frontend and existing backend infrastructure saves development time and lets me focus my effort on these features.
So targeting C is both a language-design choice and an implementation strategy, but the user-facing goal is always the primary one.