UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

514 lines (513 loc) 20.6 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug } from "./debug.js"; 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+)?$/; const _Preprocessor = class _Preprocessor { /** * Run c-like preprocessor on the source code, and resolves the code based on the defines and ifdefs * * @param {string} source - The source code to work on. * @param {Map<string, string>} [includes] - A map containing key-value pairs of include names * and their content. These are used for resolving #include directives in the source. * @param {object} [options] - Optional parameters. * @param {boolean} [options.stripUnusedColorAttachments] - If true, strips unused color attachments. * @param {boolean} [options.stripDefines] - If true, strips all defines from the source. * @param {string} [options.sourceName] - The name of the source file. * @returns {string|null} Returns preprocessed source code, or null in case of error. */ 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; } /** * Process source code, and resolves the code based on the defines and ifdefs. * * @param {string} source - The source code to work on. * @param {Map<string, string>} defines - Supplied defines which are used in addition to those * defined in the source code. Maps a define name to its value. Note that the map is modified * by the function. * @param {Map<string, string>} injectDefines - An object to collect defines that are to be * replaced with their values. * @param {Map<string, string>} [includes] - An object containing key-value pairs of include names and their * content. * @param {boolean} [stripDefines] - If true, strips all defines from the source. * @returns {string|null} Returns preprocessed source code, or null if failed. */ 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); Debug.assert(define, `Invalid [${keyword}]: ${source.substring(match.index, match.index + 100)}...`); 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; } } Debug.trace(TRACEID, `${keyword}: [${identifier}] ${value} ${keep ? "" : "IGNORED"}`); 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; } } Debug.trace(TRACEID, `${keyword}: [${identifier}] ${keep ? "" : "IGNORED"}`); if (!stripDefines) { KEYWORD.lastIndex = undef.index + undef[0].length; } break; } case "extension": { EXTENSION.lastIndex = match.index; const extension = EXTENSION.exec(source); Debug.assert(extension, `Invalid [${keyword}]: ${source.substring(match.index, match.index + 100)}...`); error || (error = extension === null); if (extension) { const identifier = extension[1]; const keep = _Preprocessor._keep(stack); if (keep) { defines.set(identifier, "true"); } Debug.trace(TRACEID, `${keyword}: [${identifier}] ${keep ? "" : "IGNORED"}`); } 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 }); Debug.trace(TRACEID, `${keyword}: [${expression}] => ${result}`); 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) : ""; Debug.trace(TRACEID, `${keyword}: [previous block] => ${blockCode !== ""}`); 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 }); Debug.trace(TRACEID, `${keyword}: [${endif[2]}] => ${result}`); } break; } case "include": { INCLUDE.lastIndex = match.index; const include = INCLUDE.exec(source); error || (error = include === null); if (!include) { Debug.assert(include, `Invalid [${keyword}] while preprocessing ${_Preprocessor.sourceName}: ${source.substring(match.index, match.index + 100)}...`); 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; } } Debug.trace(TRACEID, `${keyword}: [${identifier}] ${keep ? "" : "IGNORED"}`); 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; } /** * Evaluates a single atomic expression, which can be: * - `defined(EXPRESSION)` or `!defined(EXPRESSION)` * - Comparisons such as `A == B`, `A != B`, `A > B`, etc. * - Simple checks for the existence of a define. * * @param {string} expr - The atomic expression to evaluate. * @param {Map<string, string>} defines - A map containing key-value pairs of defines. * @returns {object} Returns an object containing the result of the evaluation and an error flag. */ 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 }; } /** * Processes parentheses in an expression by recursively evaluating subexpressions. * Ignores parentheses that are part of defined() calls. * * @param {string} expression - The expression to process. * @param {Map<string, string>} defines - A map containing key-value pairs of defines. * @returns {object} Returns an object containing the processed expression and an error flag. */ 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 }; } /** * Evaluates a complex expression with support for `defined`, `!defined`, comparisons, `&&`, * `||`, and parentheses for precedence. * * @param {string} expression - The expression to evaluate. * @param {Map<string, string>} defines - A map containing key-value pairs of defines. * @returns {object} Returns an object containing the result of the evaluation and an error flag. */ static evaluate(expression, defines) { const correct = INVALID.exec(expression) === null; Debug.assert(correct, `Resolving expression like this is not supported: ${expression}`); 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) { Debug.log(`Parenthesis parsing error in expression: "${expression}"`); 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 }; } }; __publicField(_Preprocessor, "sourceName"); let Preprocessor = _Preprocessor; export { Preprocessor };