playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
444 lines (443 loc) • 14.3 kB
JavaScript
const TRACEID = "Preprocessor";
const KEYWORD = /[ \t]*#(ifn?def|if|endif|else|elif|define|undef|extension|include)/g;
const DEFINE = /define[ \t]+([^\n]+)\r?(?:\n|$)/g;
const EXTENSION = /extension[ \t]+([\w-]+)[ \t]*:[ \t]*(enable|require)/g;
const UNDEF = /undef[ \t]+([^\n]+)\r?(?:\n|$)/g;
const IF = /(ifdef|ifndef|if)[ \t]*([^\r\n]+)\r?\n/g;
const ENDIF = /(endif|else|elif)(?:[ \t]+([^\r\n]*))?\r?\n?/g;
const IDENTIFIER = /\{?[\w-]+\}?/;
const DEFINED = /(!|\s)?defined\(([\w-]+)\)/;
const DEFINED_PARENS = /!?defined\s*\([^)]*\)/g;
const DEFINED_BEFORE_PAREN = /!?defined\s*$/;
const COMPARISON = /([a-z_]\w*)\s*(==|!=|<|<=|>|>=)\s*([\w"']+)/i;
const INVALID = /[+\-]/g;
const INCLUDE = /include[ \t]+"([\w-]+)(?:\s*,\s*([\w-]+))?"/g;
const LOOP_INDEX = /\{i\}/g;
const FRAGCOLOR = /(pcFragColor[1-8])\b/g;
const NUMERIC_LITERAL = /^\d+(?:\.\d+)?$/;
class Preprocessor {
static sourceName;
static run(source, includes = /* @__PURE__ */ new Map(), options = {}) {
Preprocessor.sourceName = options.sourceName;
source = this.stripComments(source);
source = source.split(/\r?\n/).map((line) => line.trimEnd()).join("\n");
const defines = /* @__PURE__ */ new Map();
const injectDefines = /* @__PURE__ */ new Map();
source = this._preprocess(source, defines, injectDefines, includes, options.stripDefines);
if (source === null) return null;
const intDefines = /* @__PURE__ */ new Map();
defines.forEach((value, key) => {
if (Number.isInteger(parseFloat(value)) && !value.includes(".")) {
intDefines.set(key, value);
}
});
source = this.stripComments(source);
source = this.stripUnusedColorAttachments(source, options);
source = this.RemoveEmptyLines(source);
source = this.processArraySize(source, intDefines);
source = this.injectDefines(source, injectDefines);
return source;
}
static stripUnusedColorAttachments(source, options) {
if (options.stripUnusedColorAttachments) {
const counts = /* @__PURE__ */ new Map();
const matches = source.match(FRAGCOLOR);
matches?.forEach((match) => {
const index = parseInt(match.charAt(match.length - 1), 10);
counts.set(index, (counts.get(index) ?? 0) + 1);
});
const anySingleUse = Array.from(counts.values()).some((count) => count === 1);
if (anySingleUse) {
const lines = source.split("\n");
const keepLines = [];
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(FRAGCOLOR);
if (match) {
const index = parseInt(match[0].charAt(match[0].length - 1), 10);
if (index > 0 && counts.get(index) === 1) {
continue;
}
}
keepLines.push(lines[i]);
}
source = keepLines.join("\n");
}
}
return source;
}
static stripComments(source) {
return source.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
}
static processArraySize(source, intDefines) {
if (source !== null) {
intDefines.forEach((value, key) => {
source = source.replace(new RegExp(`\\[${key}\\]`, "g"), `[${value}]`);
});
}
return source;
}
static injectDefines(source, injectDefines) {
if (source !== null && injectDefines.size > 0) {
const lines = source.split("\n");
injectDefines.forEach((value, key) => {
const regex = new RegExp(key, "g");
for (let i = 0; i < lines.length; i++) {
if (!lines[i].includes("#")) {
lines[i] = lines[i].replace(regex, value);
}
}
});
source = lines.join("\n");
}
return source;
}
static RemoveEmptyLines(source) {
if (source !== null) {
source = source.split(/\r?\n/).map((line) => line.trim() === "" ? "" : line).join("\n");
source = source.replace(/(\n\n){3,}/g, "\n\n");
}
return source;
}
static _preprocess(source, defines = /* @__PURE__ */ new Map(), injectDefines, includes, stripDefines) {
const originalSource = source;
const stack = [];
let error = false;
let match;
while ((match = KEYWORD.exec(source)) !== null && !error) {
const keyword = match[1];
switch (keyword) {
case "define": {
DEFINE.lastIndex = match.index;
const define = DEFINE.exec(source);
error || (error = define === null);
const expression = define[1];
IDENTIFIER.lastIndex = define.index;
const identifierValue = IDENTIFIER.exec(expression);
const identifier = identifierValue[0];
let value = expression.substring(identifier.length).trim();
if (value === "") value = "true";
const keep = Preprocessor._keep(stack);
let stripThisDefine = stripDefines;
if (keep) {
const replacementDefine = identifier.startsWith("{") && identifier.endsWith("}");
if (replacementDefine) {
stripThisDefine = true;
}
if (replacementDefine) {
injectDefines.set(identifier, value);
} else {
defines.set(identifier, value);
}
if (stripThisDefine) {
source = source.substring(0, define.index - 1) + source.substring(DEFINE.lastIndex);
KEYWORD.lastIndex = define.index - 1;
}
}
if (!stripThisDefine) {
KEYWORD.lastIndex = define.index + define[0].length;
}
break;
}
case "undef": {
UNDEF.lastIndex = match.index;
const undef = UNDEF.exec(source);
const identifier = undef[1].trim();
const keep = Preprocessor._keep(stack);
if (keep) {
defines.delete(identifier);
if (stripDefines) {
source = source.substring(0, undef.index - 1) + source.substring(UNDEF.lastIndex);
KEYWORD.lastIndex = undef.index - 1;
}
}
if (!stripDefines) {
KEYWORD.lastIndex = undef.index + undef[0].length;
}
break;
}
case "extension": {
EXTENSION.lastIndex = match.index;
const extension = EXTENSION.exec(source);
error || (error = extension === null);
if (extension) {
const identifier = extension[1];
const keep = Preprocessor._keep(stack);
if (keep) {
defines.set(identifier, "true");
}
}
KEYWORD.lastIndex = extension.index + extension[0].length;
break;
}
case "ifdef":
case "ifndef":
case "if": {
IF.lastIndex = match.index;
const iff = IF.exec(source);
const expression = iff[2];
const evaluated = Preprocessor.evaluate(expression, defines);
error || (error = evaluated.error);
let result = evaluated.result;
if (keyword === "ifndef") {
result = !result;
}
stack.push({
anyKeep: result,
// true if any branch was already accepted
keep: result,
// true if this branch is being taken
start: match.index,
// start index if IF line
end: IF.lastIndex
// end index of IF line
});
KEYWORD.lastIndex = iff.index + iff[0].length;
break;
}
case "endif":
case "else":
case "elif": {
ENDIF.lastIndex = match.index;
const endif = ENDIF.exec(source);
const blockInfo = stack.pop();
if (!blockInfo) {
console.error(`Shader preprocessing encountered "#${endif[1]}" without a preceding #if #ifdef #ifndef while preprocessing ${Preprocessor.sourceName} on line:
${source.substring(match.index, match.index + 100)}...`, { source: originalSource });
error = true;
continue;
}
const blockCode = blockInfo.keep ? source.substring(blockInfo.end, match.index) : "";
source = source.substring(0, blockInfo.start) + blockCode + source.substring(ENDIF.lastIndex);
KEYWORD.lastIndex = blockInfo.start + blockCode.length;
const endifCommand = endif[1];
if (endifCommand === "else" || endifCommand === "elif") {
let result = false;
if (!blockInfo.anyKeep) {
if (endifCommand === "else") {
result = !blockInfo.keep;
} else {
const evaluated = Preprocessor.evaluate(endif[2], defines);
result = evaluated.result;
error || (error = evaluated.error);
}
}
stack.push({
anyKeep: blockInfo.anyKeep || result,
keep: result,
start: KEYWORD.lastIndex,
end: KEYWORD.lastIndex
});
}
break;
}
case "include": {
INCLUDE.lastIndex = match.index;
const include = INCLUDE.exec(source);
error || (error = include === null);
if (!include) {
error = true;
continue;
}
const identifier = include[1].trim();
const countIdentifier = include[2]?.trim();
const keep = Preprocessor._keep(stack);
if (keep) {
let includeSource = includes?.get(identifier);
if (includeSource !== void 0) {
includeSource = this.stripComments(includeSource);
if (countIdentifier) {
const countString = defines.get(countIdentifier);
const count = parseFloat(countString);
if (Number.isInteger(count)) {
let result = "";
for (let i = 0; i < count; i++) {
result += includeSource.replace(LOOP_INDEX, String(i));
}
includeSource = result;
} else {
console.error(`Include Count identifier "${countIdentifier}" not resolved while preprocessing ${Preprocessor.sourceName} on line:
${source.substring(match.index, match.index + 100)}...`, { originalSource, source });
error = true;
}
}
source = source.substring(0, include.index - 1) + includeSource + source.substring(INCLUDE.lastIndex);
KEYWORD.lastIndex = include.index - 1;
} else {
console.error(`Include "${identifier}" not resolved while preprocessing ${Preprocessor.sourceName}`, { originalSource, source });
error = true;
continue;
}
}
break;
}
}
}
if (stack.length > 0) {
console.error(`Shader preprocessing reached the end of the file without encountering the necessary #endif to close a preceding #if, #ifdef, or #ifndef block. ${Preprocessor.sourceName}`);
error = true;
}
if (error) {
console.error("Failed to preprocess shader: ", { source: originalSource });
return null;
}
return source;
}
// function returns true if the evaluation is inside keep branches
static _keep(stack) {
for (let i = 0; i < stack.length; i++) {
if (!stack[i].keep) {
return false;
}
}
return true;
}
static evaluateAtomicExpression(expr, defines) {
let error = false;
expr = expr.trim();
let invert = false;
if (expr === "true") {
return { result: true, error };
}
if (expr === "false") {
return { result: false, error };
}
if (NUMERIC_LITERAL.test(expr)) {
return { result: parseFloat(expr) !== 0, error };
}
const definedMatch = DEFINED.exec(expr);
if (definedMatch) {
invert = definedMatch[1] === "!";
expr = definedMatch[2].trim();
const exists = defines.has(expr);
return { result: invert ? !exists : exists, error };
}
const comparisonMatch = COMPARISON.exec(expr);
if (comparisonMatch) {
const left = defines.get(comparisonMatch[1].trim()) ?? comparisonMatch[1].trim();
const right = defines.get(comparisonMatch[3].trim()) ?? comparisonMatch[3].trim();
const operator = comparisonMatch[2].trim();
let result2 = false;
switch (operator) {
case "==":
result2 = left === right;
break;
case "!=":
result2 = left !== right;
break;
case "<":
result2 = left < right;
break;
case "<=":
result2 = left <= right;
break;
case ">":
result2 = left > right;
break;
case ">=":
result2 = left >= right;
break;
default:
error = true;
}
return { result: result2, error };
}
const result = defines.has(expr);
return { result, error };
}
static processParentheses(expression, defines) {
let error = false;
let processed = expression.trim();
while (processed.startsWith("(") && processed.endsWith(")")) {
let depth = 0;
let wrapsEntire = true;
for (let i = 0; i < processed.length - 1; i++) {
if (processed[i] === "(") depth++;
else if (processed[i] === ")") {
depth--;
if (depth === 0) {
wrapsEntire = false;
break;
}
}
}
if (wrapsEntire) {
processed = processed.slice(1, -1).trim();
} else {
break;
}
}
while (true) {
let foundParen = false;
let depth = 0;
let maxDepth = 0;
let deepestStart = -1;
let deepestEnd = -1;
let inDefinedParen = 0;
for (let i = 0; i < processed.length; i++) {
if (processed[i] === "(") {
const beforeParen = processed.substring(0, i);
if (DEFINED_BEFORE_PAREN.test(beforeParen)) {
inDefinedParen++;
} else if (inDefinedParen === 0) {
depth++;
if (depth > maxDepth) {
maxDepth = depth;
deepestStart = i;
}
foundParen = true;
}
} else if (processed[i] === ")") {
if (inDefinedParen > 0) {
inDefinedParen--;
} else if (depth > 0) {
if (depth === maxDepth && deepestStart !== -1) {
deepestEnd = i;
}
depth--;
}
}
}
if (!foundParen || deepestStart === -1 || deepestEnd === -1) {
break;
}
const subExpr = processed.substring(deepestStart + 1, deepestEnd);
const { result, error: subError } = Preprocessor.evaluate(subExpr, defines);
error = error || subError;
processed = processed.substring(0, deepestStart) + (result ? "true" : "false") + processed.substring(deepestEnd + 1);
}
return { expression: processed, error };
}
static evaluate(expression, defines) {
const correct = INVALID.exec(expression) === null;
let processedExpr = expression;
let parenError = false;
const withoutDefined = expression.replace(DEFINED_PARENS, "");
if (withoutDefined.indexOf("(") !== -1) {
const processed = Preprocessor.processParentheses(expression, defines);
processedExpr = processed.expression;
parenError = processed.error;
}
if (parenError) {
return { result: false, error: true };
}
const orSegments = processedExpr.split("||");
for (const orSegment of orSegments) {
const andSegments = orSegment.split("&&");
let andResult = true;
for (const andSegment of andSegments) {
const { result, error } = Preprocessor.evaluateAtomicExpression(andSegment.trim(), defines);
if (!result || error) {
andResult = false;
break;
}
}
if (andResult) {
return { result: true, error: !correct };
}
}
return { result: false, error: !correct };
}
}
export {
Preprocessor
};