mixinc

We should consider mixinC. mixinC takes a string of C code that is converted to a D ast - like importC can do now - but also takes and returns an argument of immutable preprocessor state.

Consider the following code:

// at top level, mixinC always returns a template mixin
mixin mixinC(__initial_pps, `
#define _WIN32_LEAN_AND_MEAN
#include<windows.h>
`) c_windows;

// the declarations are mixed in like normal. It includes all the C entities just like we get from importC, but there's also a magic member in there `pps` - preprocessor state.

// pps is immutable. it has the macro definitions in it and can be passed to later mixinC statements which will return a copy of it with modifications made:

enum pps2 = mixinC(pps, `#undef _WIN32_LEAN_AND_MEAN`).pps;
static assert("_WIN32_LEAN_AND_MEAN" !in pps2.macros); // removed here
static assert("_WIN32_LEAN_AND_MEAN" in c_windows.pps.macros); // but this unmodified

void main() {
	// mixinC expressions and statements work like a standard string mixin
	// but still take a preprocessor state. However they no longer return
	// one for practical reasons; there's only the one return value.
	//
	// This will run the preprocessor over the given string given the
	// macros from pps, then pass it through the C compiler components
	// to make your ast node to graft on to the D tree.
	auto value = mixinC(pps2, `can_use_macro(x)`);
}

The pps object need not be opaque; it should expose the C macros for D processing, including CTFE reflection. But it is strictly immutable because of order-of-evaluation in the declaration level. By making you pass the immutable object to any place it is used, you give a linear dependency chain to force an order of evaluation without breaking D's normal (lack of) order of evaluation of declarations.

The __initial_pps is a magic variable to get the correct state out of the system preprocessor. It defines compiler-provided constants, etc. Then from here you build up more with your own defines and includes.

BTW things like the struct tag namespace could also work through this template mixin object perhaps; things not expressed directly in D may perhaps be wrapped up in other aggregates D can use.

Arguments to the c preprocessor would probably need to be forwarded through dmd. So like dmd -P-Icpp/path kind of thing. You can dmd -P-Dsomething to define things too - which could be forwarded to D.

Opening the cpp can of worms here can infect D in other places, though at least the mixinC keyword would keep it limited, it can still explicitly forward that info back to the rest of D. Worth noting that cpp integration kinda renders -J obsolete since you can #include things separately and pass it down. Maybe not exactly but it warrants some thought.