VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
preprocessor.h
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3#pragma once
4
5#include <algorithm>
6#include <cstdlib>
7#include <cctype>
8#include <optional>
9#include <regex>
10#include <sstream>
11#include <string>
12#include <string_view>
13#include <unordered_map>
14#include <vector>
15
16namespace visutwin::canvas
17{
19 {
21 bool stripDefines = false;
22 std::string sourceName;
23 };
24
26 {
27 public:
28 static std::string stripComments(const std::string& source)
29 {
30 return std::regex_replace(source, std::regex(R"(/\*[\s\S]*?\*/|([^\\:]|^)//.*$)", std::regex_constants::multiline), "$1");
31 }
32
33 static std::string removeEmptyLines(const std::string& source)
34 {
35 std::string output = source;
36 output = std::regex_replace(output, std::regex(R"((\n\n){3,})"), "\n\n");
37 return output;
38 }
39
40 static std::string run(const std::string& source,
41 const std::unordered_map<std::string, std::string>& includes,
42 const bool stripDefines)
43 {
44 PreprocessorOptions options;
45 options.stripDefines = stripDefines;
46 return run(source, includes, options);
47 }
48
49 static std::string run(const std::string& source,
50 const std::unordered_map<std::string, std::string>& includes = {},
51 const PreprocessorOptions& options = {})
52 {
53 std::unordered_map<std::string, std::string> defines;
54 std::unordered_map<std::string, std::string> injectDefines;
55
56 std::string output = stripComments(source);
57 output = trimEndPerLine(output);
58 output = preprocess(output, defines, injectDefines, includes, options.stripDefines);
59 output = stripComments(output);
60 output = stripUnusedColorAttachments(output, options.stripUnusedColorAttachments);
61 output = removeEmptyLines(output);
62 output = processArraySize(output, defines);
63 output = injectDefinesIntoSource(output, injectDefines);
64 return output;
65 }
66
67 private:
68 struct ConditionalFrame
69 {
70 bool parentKeep = true;
71 bool branchTaken = false;
72 bool keep = true;
73 };
74
75 static std::string trim(const std::string& s)
76 {
77 size_t start = 0;
78 while (start < s.size() && std::isspace(static_cast<unsigned char>(s[start]))) {
79 ++start;
80 }
81 size_t end = s.size();
82 while (end > start && std::isspace(static_cast<unsigned char>(s[end - 1]))) {
83 --end;
84 }
85 return s.substr(start, end - start);
86 }
87
88 static std::string trimEndPerLine(const std::string& source)
89 {
90 std::stringstream in(source);
91 std::string line;
92 std::string out;
93 bool first = true;
94 while (std::getline(in, line)) {
95 while (!line.empty() && (line.back() == ' ' || line.back() == '\t' || line.back() == '\r')) {
96 line.pop_back();
97 }
98 if (!first) {
99 out += '\n';
100 }
101 first = false;
102 out += line;
103 }
104 return out;
105 }
106
107 static bool active(const std::vector<ConditionalFrame>& stack)
108 {
109 return stack.empty() ? true : stack.back().keep;
110 }
111
112 static bool parseDefinedExpr(const std::string& expr, const std::unordered_map<std::string, std::string>& defines)
113 {
114 const std::string e = trim(expr);
115 std::smatch match;
116 static const std::regex definedPattern(R"(^(!)?\s*defined\‍(([^)]+)\)\s*$)");
117 if (std::regex_match(e, match, definedPattern)) {
118 const bool negated = match[1].matched;
119 const std::string id = trim(match[2].str());
120 const bool value = defines.contains(id);
121 return negated ? !value : value;
122 }
123 return false;
124 }
125
126 static std::optional<bool> parseComparisonExpr(const std::string& expr, const std::unordered_map<std::string, std::string>& defines)
127 {
128 std::smatch match;
129 static const std::regex comparison(R"(^\s*([A-Za-z_]\w*)\s*(==|!=|<=|>=|<|>)\s*([\w"']+)\s*$)");
130 if (!std::regex_match(expr, match, comparison)) {
131 return std::nullopt;
132 }
133
134 const std::string lhsId = match[1].str();
135 const std::string op = match[2].str();
136 std::string rhs = match[3].str();
137
138 auto it = defines.find(lhsId);
139 const std::string lhsRaw = (it != defines.end()) ? it->second : "0";
140
141 auto stripQuotes = [](std::string s) {
142 if (s.size() >= 2 && ((s.front() == '\'' && s.back() == '\'') || (s.front() == '"' && s.back() == '"'))) {
143 return s.substr(1, s.size() - 2);
144 }
145 return s;
146 };
147
148 const std::string lhs = stripQuotes(lhsRaw);
149 rhs = stripQuotes(rhs);
150
151 auto toDouble = [](const std::string& s) -> std::optional<double> {
152 char* end = nullptr;
153 const double v = std::strtod(s.c_str(), &end);
154 if (end && *end == '\0') {
155 return v;
156 }
157 return std::nullopt;
158 };
159
160 if (const auto ln = toDouble(lhs); ln.has_value()) {
161 if (const auto rn = toDouble(rhs); rn.has_value()) {
162 if (op == "==") return *ln == *rn;
163 if (op == "!=") return *ln != *rn;
164 if (op == "<") return *ln < *rn;
165 if (op == "<=") return *ln <= *rn;
166 if (op == ">") return *ln > *rn;
167 if (op == ">=") return *ln >= *rn;
168 }
169 }
170
171 if (op == "==") return lhs == rhs;
172 if (op == "!=") return lhs != rhs;
173 if (op == "<") return lhs < rhs;
174 if (op == "<=") return lhs <= rhs;
175 if (op == ">") return lhs > rhs;
176 if (op == ">=") return lhs >= rhs;
177 return std::nullopt;
178 }
179
180 static bool evalExpr(const std::string& expr, const std::unordered_map<std::string, std::string>& defines)
181 {
182 const std::string e = trim(expr);
183 if (e.empty()) {
184 return false;
185 }
186
187 const size_t orPos = e.find("||");
188 if (orPos != std::string::npos) {
189 return evalExpr(e.substr(0, orPos), defines) || evalExpr(e.substr(orPos + 2), defines);
190 }
191
192 const size_t andPos = e.find("&&");
193 if (andPos != std::string::npos) {
194 return evalExpr(e.substr(0, andPos), defines) && evalExpr(e.substr(andPos + 2), defines);
195 }
196
197 if (parseDefinedExpr(e, defines)) {
198 return true;
199 }
200
201 static const std::regex negDefinedPattern(R"(^\s*!\s*defined\‍(([^)]+)\)\s*$)");
202 if (std::regex_match(e, negDefinedPattern)) {
203 return parseDefinedExpr(e, defines);
204 }
205
206 if (const auto comparison = parseComparisonExpr(e, defines); comparison.has_value()) {
207 return *comparison;
208 }
209
210 if (defines.contains(e)) {
211 const auto& value = defines.at(e);
212 return !(value == "0" || value == "false" || value.empty());
213 }
214
215 if (e == "true") return true;
216 if (e == "false") return false;
217 return false;
218 }
219
220 static std::string preprocess(const std::string& source,
221 std::unordered_map<std::string, std::string>& defines,
222 std::unordered_map<std::string, std::string>& injectDefines,
223 const std::unordered_map<std::string, std::string>& includes,
224 const bool stripDefines)
225 {
226 std::stringstream in(source);
227 std::string line;
228 std::vector<ConditionalFrame> stack;
229 std::vector<std::string> output;
230
231 static const std::regex includePattern(R"(^\s*#include\s+"([\w-]+)(?:\s*,\s*([\w-]+))?"\s*$)");
232 static const std::regex definePattern(R"(^\s*#define\s+([^\s]+)\s*(.*)$)");
233 static const std::regex undefPattern(R"(^\s*#undef\s+([^\s]+)\s*$)");
234 static const std::regex extensionPattern(R"(^\s*#extension\s+([\w-]+)\s*:\s*(enable|require)\s*$)");
235 static const std::regex ifdefPattern(R"(^\s*#ifdef\s+(.+)$)");
236 static const std::regex ifndefPattern(R"(^\s*#ifndef\s+(.+)$)");
237 static const std::regex ifPattern(R"(^\s*#if\s+(.+)$)");
238 static const std::regex elifPattern(R"(^\s*#elif\s+(.+)$)");
239 static const std::regex elsePattern(R"(^\s*#else\s*$)");
240 static const std::regex endifPattern(R"(^\s*#endif\s*$)");
241
242 while (std::getline(in, line)) {
243 std::smatch match;
244 const bool keep = active(stack);
245
246 if (std::regex_match(line, match, includePattern)) {
247 if (keep) {
248 const auto includeIt = includes.find(match[1].str());
249 if (includeIt != includes.end()) {
250 output.push_back(includeIt->second);
251 }
252 if (match[2].matched) {
253 const auto includeIt2 = includes.find(match[2].str());
254 if (includeIt2 != includes.end()) {
255 output.push_back(includeIt2->second);
256 }
257 }
258 }
259 continue;
260 }
261
262 if (std::regex_match(line, match, definePattern)) {
263 if (keep) {
264 const std::string id = trim(match[1].str());
265 std::string value = trim(match[2].str());
266 if (value.empty()) {
267 value = "true";
268 }
269
270 if (id.size() > 2 && id.front() == '{' && id.back() == '}') {
271 injectDefines[id] = value;
272 } else {
273 defines[id] = value;
274 }
275 }
276
277 if (!stripDefines && keep) {
278 output.push_back(line);
279 }
280 continue;
281 }
282
283 if (std::regex_match(line, match, undefPattern)) {
284 if (keep) {
285 defines.erase(trim(match[1].str()));
286 }
287 if (!stripDefines && keep) {
288 output.push_back(line);
289 }
290 continue;
291 }
292
293 if (std::regex_match(line, match, extensionPattern)) {
294 if (keep) {
295 defines[trim(match[1].str())] = "true";
296 }
297 if (!stripDefines && keep) {
298 output.push_back(line);
299 }
300 continue;
301 }
302
303 if (std::regex_match(line, match, ifdefPattern)) {
304 const bool parentKeep = keep;
305 const bool cond = parentKeep && defines.contains(trim(match[1].str()));
306 stack.push_back({parentKeep, cond, parentKeep && cond});
307 continue;
308 }
309
310 if (std::regex_match(line, match, ifndefPattern)) {
311 const bool parentKeep = keep;
312 const bool cond = parentKeep && !defines.contains(trim(match[1].str()));
313 stack.push_back({parentKeep, cond, parentKeep && cond});
314 continue;
315 }
316
317 if (std::regex_match(line, match, ifPattern)) {
318 const bool parentKeep = keep;
319 const bool cond = parentKeep && evalExpr(match[1].str(), defines);
320 stack.push_back({parentKeep, cond, parentKeep && cond});
321 continue;
322 }
323
324 if (std::regex_match(line, match, elifPattern)) {
325 if (!stack.empty()) {
326 auto& top = stack.back();
327 const bool cond = top.parentKeep && !top.branchTaken && evalExpr(match[1].str(), defines);
328 top.keep = cond;
329 if (cond) {
330 top.branchTaken = true;
331 }
332 }
333 continue;
334 }
335
336 if (std::regex_match(line, elsePattern)) {
337 if (!stack.empty()) {
338 auto& top = stack.back();
339 top.keep = top.parentKeep && !top.branchTaken;
340 top.branchTaken = true;
341 }
342 continue;
343 }
344
345 if (std::regex_match(line, endifPattern)) {
346 if (!stack.empty()) {
347 stack.pop_back();
348 }
349 continue;
350 }
351
352 if (keep) {
353 output.push_back(line);
354 }
355 }
356
357 std::string result;
358 for (size_t i = 0; i < output.size(); ++i) {
359 result += output[i];
360 if (i + 1 < output.size()) {
361 result += '\n';
362 }
363 }
364 return result;
365 }
366
367 static std::string processArraySize(std::string source,
368 const std::unordered_map<std::string, std::string>& defines)
369 {
370 for (const auto& [key, value] : defines) {
371 char* end = nullptr;
372 std::strtol(value.c_str(), &end, 10);
373 if (!end || *end != '\0') {
374 continue;
375 }
376
377 source = std::regex_replace(source, std::regex("\\\\[" + key + "\\\\]"), "[" + value + "]");
378 }
379 return source;
380 }
381
382 static std::string injectDefinesIntoSource(const std::string& source,
383 const std::unordered_map<std::string, std::string>& injectDefines)
384 {
385 if (injectDefines.empty()) {
386 return source;
387 }
388
389 std::stringstream in(source);
390 std::string line;
391 std::string out;
392 bool first = true;
393
394 while (std::getline(in, line)) {
395 if (line.find('#') == std::string::npos) {
396 for (const auto& [key, value] : injectDefines) {
397 line = std::regex_replace(line, std::regex(regexEscape(key)), value);
398 }
399 }
400
401 if (!first) {
402 out += '\n';
403 }
404 first = false;
405 out += line;
406 }
407
408 return out;
409 }
410
411 static std::string regexEscape(const std::string& input)
412 {
413 static const std::regex specials(R"([.^$|()\\‍[\‍]{}*+?])");
414 return std::regex_replace(input, specials, R"(\$&)");
415 }
416
417 static std::string stripUnusedColorAttachments(const std::string& source, const bool enabled)
418 {
419 if (!enabled) {
420 return source;
421 }
422
423 static const std::regex fragColorPattern(R"((pcFragColor[1-8])\b)");
424
425 std::unordered_map<int, int> counts;
426 for (auto it = std::sregex_iterator(source.begin(), source.end(), fragColorPattern);
427 it != std::sregex_iterator(); ++it) {
428 const auto& match = *it;
429 const std::string token = match[1].str();
430 const int index = token.back() - '0';
431 counts[index]++;
432 }
433
434 bool anySingleUse = false;
435 for (const auto& [_, c] : counts) {
436 if (c == 1) {
437 anySingleUse = true;
438 break;
439 }
440 }
441
442 if (!anySingleUse) {
443 return source;
444 }
445
446 std::stringstream in(source);
447 std::string line;
448 std::vector<std::string> keep;
449
450 while (std::getline(in, line)) {
451 std::smatch m;
452 if (std::regex_search(line, m, fragColorPattern)) {
453 const int index = m[1].str().back() - '0';
454 if (index > 0 && counts[index] == 1) {
455 continue;
456 }
457 }
458 keep.push_back(line);
459 }
460
461 std::string out;
462 for (size_t i = 0; i < keep.size(); ++i) {
463 out += keep[i];
464 if (i + 1 < keep.size()) {
465 out += '\n';
466 }
467 }
468 return out;
469 }
470 };
471}
static std::string run(const std::string &source, const std::unordered_map< std::string, std::string > &includes, const bool stripDefines)
static std::string removeEmptyLines(const std::string &source)
static std::string run(const std::string &source, const std::unordered_map< std::string, std::string > &includes={}, const PreprocessorOptions &options={})
static std::string stripComments(const std::string &source)