In my C-like (C3), I retained an #if-like construct, which while semantic (unlike C’s) still meant dealing with sections of conditional code that had to be resolved before semantic checking could commence. To simplify I removed the ability to conditionally include struct members etc.
Doing more research I found D’s `version` interesting. In particular the non-block variant where the version is attached to a declaration.
I ended up creating a more flexible (and therefore actually less good) version of it as an attribute (`@if(cond)`) which then may be attached to any declaration. The advantage here is that at most this leads to some symbols having two (or more) different declarations (resolvable using the `@if`), which allows a lot of analysis even if the attribute hasn’t had its argument resolved.
I think this might be the way forward at the top level.
Inside of functions / macros, $if and friends are fine as they often do not matter for the overall analysis.