playcanvas
Version:
PlayCanvas WebGL game engine
393 lines (390 loc) • 19.1 kB
JavaScript
import { Debug } from './debug.js';
// id for debug tracing
var TRACEID = 'Preprocessor';
// accepted keywords
var KEYWORD = /[ \t]*#(ifn?def|if|endif|else|elif|define|undef|extension|include)/g;
// #define EXPRESSION
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/optimal-quantifier-concatenation
var DEFINE = /define[ \t]+([^\n]+)\r?(?:\n|$)/g;
// #extension IDENTIFIER : enabled
var EXTENSION = /extension[ \t]+([\w-]+)[ \t]*:[ \t]*(enable|require)/g;
// #undef EXPRESSION
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/optimal-quantifier-concatenation
var UNDEF = /undef[ \t]+([^\n]+)\r?(?:\n|$)/g;
// #ifdef/#ifndef SOMEDEFINE, #if EXPRESSION
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/no-unused-capturing-group
var IF = /(ifdef|ifndef|if)[ \t]*([^\r\n]+)\r?\n/g;
// #endif/#else or #elif EXPRESSION
var ENDIF = /(endif|else|elif)([ \t][^\r\n]+)?\r?(?:\n|$)/g;
// identifier
var IDENTIFIER = /([\w-]+)/;
// [!]defined(EXPRESSION)
var DEFINED = /(!|\s)?defined\(([\w-]+)\)/;
// Matches comparison operators like ==, !=, <, <=, >, >=
var COMPARISON = /([a-z_]\w*)\s*(==|!=|<|<=|>|>=)\s*([\w"']+)/i;
// currently unsupported characters in the expression: | & < > = + -
var INVALID = /[|&+-]/g;
// #include "identifier"
var INCLUDE = /include[ \t]+"([\w-]+)"\r?(?:\n|$)/g;
/**
* Pure static class implementing subset of C-style preprocessor.
* inspired by: https://github.com/dcodeIO/Preprocessor.js
*/ 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.
* @returns {string|null} Returns preprocessed source code, or null in case of error.
*/ static run(source, includes, options) {
if (includes === undefined) includes = new Map();
if (options === undefined) options = {};
// strips comments, handles // and many cases of /*
source = this.stripComments(source);
// right trim each line
source = source.split(/\r?\n/).map((line)=>line.trimEnd()).join('\n');
// generate defines to remove unused color attachments
var defines = new Map();
if (options.stripUnusedColorAttachments) {
// find out how many times pcFragColorX is used (see gles3.js)
var counts = new Map();
var regex = /(pcFragColor[1-8])\b/g;
var matches = source.match(regex);
matches == null ? undefined : matches.forEach((match)=>{
var index = parseInt(match.charAt(match.length - 1), 10);
var _counts_get;
counts.set(index, ((_counts_get = counts.get(index)) != null ? _counts_get : 0) + 1);
});
// if pcFragColorX is used only once, remove it
counts.forEach((count, index)=>{
if (count === 1) {
defines.set("REMOVE_COLOR_ATTACHMENT_" + index, '');
}
});
}
// preprocess defines / ifdefs ..
source = this._preprocess(source, defines, includes, options.stripDefines);
// extract defines that evaluate to an integer number
var intDefines = new Map();
defines.forEach((value, key)=>{
if (Number.isInteger(parseFloat(value)) && !value.includes('.')) {
intDefines.set(key, value);
}
});
// strip comments again after the includes have been resolved
source = this.stripComments(source);
// remove empty lines
source = this.RemoveEmptyLines(source);
// process array sizes
source = this.processArraySize(source, intDefines);
return source;
}
static stripComments(source) {
return source.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');
}
static processArraySize(source, intDefines) {
if (source !== null) {
// replace lines containing "[intDefine]" with their values, so that we know the array size for WebGPU uniform buffer
// example: weight[SAMPLES] => float weight[11] in case there was a "define SAMPLES 11" in the source code
intDefines.forEach((value, key)=>{
source = source.replace(new RegExp("\\[" + key + "\\]", 'g'), "[" + value + "]");
});
}
return source;
}
static RemoveEmptyLines(source) {
if (source !== null) {
source = source.split(/\r?\n/)// convert lines with only white space into empty string
.map((line)=>line.trim() === '' ? '' : line).join('\n');
// remove more than 1 consecutive empty lines
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>} [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} Returns preprocessed source code.
*/ static _preprocess(source, defines, includes, stripDefines) {
if (defines === undefined) defines = new Map();
var originalSource = source;
// stack, storing info about ifdef blocks
var stack = [];
// true if the function encounter a problem
var error = false;
var match;
while((match = KEYWORD.exec(source)) !== null){
var keyword = match[1];
switch(keyword){
case 'define':
{
// read the rest of the define line
DEFINE.lastIndex = match.index;
var define = DEFINE.exec(source);
Debug.assert(define, "Invalid [" + keyword + "]: " + source.substring(match.index, match.index + 100) + "...");
error || (error = define === null);
var expression = define[1];
// split it to identifier name and a value
IDENTIFIER.lastIndex = define.index;
var identifierValue = IDENTIFIER.exec(expression);
var identifier = identifierValue[1];
var value = expression.substring(identifier.length).trim();
if (value === '') value = 'true';
// are we inside if-blocks that are accepted
var keep = Preprocessor._keep(stack);
if (keep) {
defines.set(identifier, value);
if (stripDefines) {
// cut out the define line
source = source.substring(0, define.index - 1) + source.substring(DEFINE.lastIndex);
// continue processing on the next symbol
KEYWORD.lastIndex = define.index;
}
}
Debug.trace(TRACEID, keyword + ": [" + identifier + "] " + value + " " + (keep ? '' : 'IGNORED'));
// continue on the next line
if (!stripDefines) {
KEYWORD.lastIndex = define.index + define[0].length;
}
break;
}
case 'undef':
{
// read the rest of the define line
UNDEF.lastIndex = match.index;
var undef = UNDEF.exec(source);
var identifier1 = undef[1].trim();
// are we inside if-blocks that are accepted
var keep1 = Preprocessor._keep(stack);
// remove it from defines
if (keep1) {
defines.delete(identifier1);
if (stripDefines) {
// cut out the undef line
source = source.substring(0, undef.index - 1) + source.substring(UNDEF.lastIndex);
// continue processing on the next symbol
KEYWORD.lastIndex = undef.index;
}
}
Debug.trace(TRACEID, keyword + ": [" + identifier1 + "] " + (keep1 ? '' : 'IGNORED'));
// continue on the next line
if (!stripDefines) {
KEYWORD.lastIndex = undef.index + undef[0].length;
}
break;
}
case 'extension':
{
EXTENSION.lastIndex = match.index;
var extension = EXTENSION.exec(source);
Debug.assert(extension, "Invalid [" + keyword + "]: " + source.substring(match.index, match.index + 100) + "...");
error || (error = extension === null);
if (extension) {
var identifier2 = extension[1];
// are we inside if-blocks that are accepted
var keep2 = Preprocessor._keep(stack);
if (keep2) {
defines.set(identifier2, 'true');
}
Debug.trace(TRACEID, keyword + ": [" + identifier2 + "] " + (keep2 ? '' : 'IGNORED'));
}
// continue on the next line
KEYWORD.lastIndex = extension.index + extension[0].length;
break;
}
case 'ifdef':
case 'ifndef':
case 'if':
{
// read the if line
IF.lastIndex = match.index;
var iff = IF.exec(source);
var expression1 = iff[2];
// evaluate expression
var evaluated = Preprocessor.evaluate(expression1, defines);
error || (error = evaluated.error);
var result = evaluated.result;
if (keyword === 'ifndef') {
result = !result;
}
// add info to the stack (to be handled later)
stack.push({
anyKeep: result,
keep: result,
start: match.index,
end: IF.lastIndex // end index of IF line
});
Debug.trace(TRACEID, keyword + ": [" + expression1 + "] => " + result);
// continue on the next line
KEYWORD.lastIndex = iff.index + iff[0].length;
break;
}
case 'endif':
case 'else':
case 'elif':
{
// match the endif
ENDIF.lastIndex = match.index;
var endif = ENDIF.exec(source);
var blockInfo = stack.pop();
// code between if and endif
var blockCode = blockInfo.keep ? source.substring(blockInfo.end, match.index) : '';
Debug.trace(TRACEID, keyword + ": [previous block] => " + (blockCode !== ''));
// cut out the IF and ENDIF lines, leave block if required
source = source.substring(0, blockInfo.start) + blockCode + source.substring(ENDIF.lastIndex);
KEYWORD.lastIndex = blockInfo.start + blockCode.length;
// handle else if
var endifCommand = endif[1];
if (endifCommand === 'else' || endifCommand === 'elif') {
// if any branch was already accepted, all else branches need to fail regardless of the result
var result1 = false;
if (!blockInfo.anyKeep) {
if (endifCommand === 'else') {
result1 = !blockInfo.keep;
} else {
var evaluated1 = Preprocessor.evaluate(endif[2], defines);
result1 = evaluated1.result;
error || (error = evaluated1.error);
}
}
// add back to stack
stack.push({
anyKeep: blockInfo.anyKeep || result1,
keep: result1,
start: KEYWORD.lastIndex,
end: KEYWORD.lastIndex
});
Debug.trace(TRACEID, keyword + ": [" + endif[2] + "] => " + result1);
}
break;
}
case 'include':
{
// match the include
INCLUDE.lastIndex = match.index;
var include = INCLUDE.exec(source);
error || (error = include === null);
Debug.assert(include, "Invalid [" + keyword + "]: " + source.substring(match.index, match.index + 100) + "...");
var identifier3 = include[1].trim();
// are we inside if-blocks that are accepted
var keep3 = Preprocessor._keep(stack);
if (keep3) {
// cut out the include line and replace it with the included string
var includeSource = includes == null ? undefined : includes.get(identifier3);
if (includeSource !== undefined) {
source = source.substring(0, include.index - 1) + includeSource + source.substring(INCLUDE.lastIndex);
// process the just included test
KEYWORD.lastIndex = include.index;
} else {
console.error('Include "' + identifier3 + '" not resolved while preprocessing a shader', {
source: originalSource
});
error = true;
}
}
Debug.trace(TRACEID, keyword + ": [" + identifier3 + "] " + (keep3 ? '' : 'IGNORED'));
break;
}
}
}
if (error) {
console.warn('Failed to preprocess shader: ', {
source: originalSource
});
return originalSource;
}
return source;
}
// function returns true if the evaluation is inside keep branches
static _keep(stack) {
for(var i = 0; i < stack.length; i++){
if (!stack[i].keep) {
return false;
}
}
return true;
}
/**
* Very simple expression evaluation, handles cases:
*
* - expression
* - defined(expression)
* - !defined(expression)
* - simple comparisons like "XX == 3" or "XX != test"
*
* But does not handle more complex cases, which would require more complex system:
*
* - defined(A) || defined(B)
*
* @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) {
var correct = INVALID.exec(expression) === null;
Debug.assert(correct, "Resolving expression like this is not supported: " + expression);
// if the format is 'defined(expression)', extract expression
var invert = false;
var defined = DEFINED.exec(expression);
if (defined) {
invert = defined[1] === '!';
expression = defined[2];
}
// if the expression is a comparison, evaluate it
var comparison = COMPARISON.exec(expression);
if (comparison) {
var _defines_get;
var left = (_defines_get = defines.get(comparison[1])) != null ? _defines_get : comparison[1];
var _defines_get1;
var right = (_defines_get1 = defines.get(comparison[3])) != null ? _defines_get1 : comparison[3];
var operator = comparison[2];
var result = false;
switch(operator){
case '==':
result = left === right;
break;
case '!=':
result = left !== right;
break;
case '<':
result = left < right;
break;
case '<=':
result = left <= right;
break;
case '>':
result = left > right;
break;
case '>=':
result = left >= right;
break;
}
return {
result,
error: !correct
};
}
// test if expression define exists
expression = expression.trim();
var exists = defines.has(expression);
// handle inversion
if (invert) {
exists = !exists;
}
return {
result: exists,
error: !correct
};
}
}
export { Preprocessor };