resolveIncludes function

String resolveIncludes(
  1. String source,
  2. GlslSources sources, {
  3. required String from,
  4. Set<String>? seen,
  5. List<String>? stack,
})

Resolves #include <path> against sources.

Depth-first, and a file already pulled in is skipped rather than repeated — the headers include each other (material_maps → surface → color), so without that the second inclusion redeclares every uniform and the compile fails on a duplicate rather than on anything real.

A cycle is an error and not a silent stop: the include graph here is a tree by design, and a cycle means somebody changed that without noticing.

Implementation

String resolveIncludes(
  String source,
  GlslSources sources, {
  required String from,
  Set<String>? seen,
  List<String>? stack,
}) {
  final included = seen ?? <String>{};
  final path = stack ?? <String>[from];
  final out = StringBuffer();

  for (final line in source.split('\n')) {
    final match = _include.firstMatch(line);
    if (match == null) {
      out.writeln(line);
      continue;
    }
    final target = match.group(1)!;
    if (path.contains(target)) {
      throw GlslTranslateException(
        'include cycle: ${<String>[...path, target].join(' -> ')}',
      );
    }
    if (!included.add(target)) continue;
    final body = sources[target];
    if (body == null) {
      throw GlslTranslateException(
        '$from includes "$target", which is not here',
      );
    }
    // The included text is translated by the same rules, so a header's own
    // includes and its version line are handled once rather than per includer.
    out.writeln('// --- $target ---');
    out.write(
      resolveIncludes(
        body,
        sources,
        from: target,
        seen: included,
        stack: <String>[...path, target],
      ),
    );
  }
  return out.toString();
}