addToLibrary({ // Removes all C++ '//' and '/* */' comments from the given source string. // N.b. will also eat comments inside strings. $remove_cpp_comments_in_shaders: (code) => { var i = 0, out = '', ch, next, len = code.length; for (; i < len; ++i) { ch = code[i]; if (ch == '/') { next = code[i+1]; if (next == '/') { while (i < len && code[i+1] != '\n') ++i; } else if (next == '*') { while (i < len && (code[i-1] != '*' || code[i] != '/')) ++i; } else { out += ch; } } else { out += ch; } } return out; }, // Finds the index of closing parens from the opening parens at arr[i]. // Used polymorphically for strings ("foo") and token arrays (['(', 'foo', ')']) as input. $find_closing_parens_index: (arr, i, opening='(', closing=')') => { for (var nesting = 0; i < arr.length; ++i) { if (arr[i] == opening) ++nesting; if (arr[i] == closing && --nesting == 0) { return i; } } }, // Runs C preprocessor algorithm on the given string 'code'. // Supported preprocessor directives: #if, #ifdef, #ifndef, #else, #elif, #endif, #define and #undef. // predefs: Specifies a dictionary of { 'key1': function(arg0, arg1) {...}, 'key2': ... } of predefined preprocessing variables $preprocess_c_code__deps: ['$find_closing_parens_index'], $preprocess_c_code: function(code, defs = {}) { var i = 0, // iterator over the input string len = code.length, // cache input length out = '', // generates the preprocessed output string stack = [1]; // preprocessing stack (state of active/inactive #ifdef/#else blocks we are currently inside) // a mapping 'symbolname' -> function(args) which evaluates the given cpp macro, e.g. #define FOO(x) x+10. defs['defined'] = (args) => { // built-in "#if defined(x)"" macro. #if ASSERTIONS assert(args.length == 1); assert(/^[A-Za-z0-9_$]+$/.test(args[0].trim())); // Test that a C preprocessor identifier contains only valid characters (we likely parsed wrong if this fails) #endif return defs[args[0].trim()] ? 1 : 0; }; // Returns true if str[i] is whitespace. function isWhitespace(str, i) { return !(str.charCodeAt(i) > 32); // Compare as negation to treat end-of-string undefined as whitespace } // Returns index to the next whitespace character starting at str[i]. function nextWhitespace(str, i) { while (!isWhitespace(str, i)) ++i; return i; } // Returns an integer ID classification of the character at str[idx], used for tokenization purposes. function classifyChar(str, idx) { var cc = str.charCodeAt(idx); #if ASSERTIONS assert(!(cc > 127), "only 7-bit ASCII can be used in preprocessor #if/#ifdef/#define statements"); #endif if (cc > 32) { if (cc < 48) return 1; // an operator symbol, any of !"#$%&'()*+,-./ if (cc < 58) return 2; // a number 0123456789 if (cc < 65) return 1; // an operator symbol, any of :;<=>?@ if (cc < 91 || cc == 95/*_*/) return 3; // a character, any of A-Z or _ if (cc < 97) return 1; // an operator symbol, any of [\]^` if (cc < 123) return 3; // a character, any of a-z return 1; // an operator symbol, any of {|}~ } return cc < 33 ? 0 : 4; // 0=whitespace, 4=end-of-string } // Returns a tokenized array of the given string expression, i.e. "FOO > BAR && BAZ" -> ["FOO", ">", "BAR", "&&", "BAZ"] // Optionally keeps whitespace as tokens to be able to reconstruct the original input string. /** * @param {string} exprString * @param {(number|boolean)=} keepWhitespace Optional, can be omitted. Defaults to false. */ function tokenize(exprString, keepWhitespace) { var out = [], len = exprString.length; for (var i = 0; i <= len; ++i) { var kind = classifyChar(exprString, i); if (kind == 2/*0-9*/ || kind == 3/*a-z*/) { // a character or a number for (var j = i+1; j <= len; ++j) { var kind2 = classifyChar(exprString, j); if (kind2 != kind && (kind2 != 2/*0-9*/ || kind != 3/*a-z*/)) { // parse number sequence "423410", and identifier sequence "FOO32BAR" out.push(exprString.substring(i, j)); i = j-1; break; } } } else if (kind == 1/*operator symbol*/) { // Lookahead for two-character operators. var op2 = exprString.slice(i, i + 2); if (['<=', '>=', '==', '!=', '&&', '||'].includes(op2)) { out.push(op2); ++i; } else { out.push(exprString[i]); } } } return out; } // Expands preprocessing macros on substring str[lineStart...lineEnd] /** * @param {string} str * @param {number} lineStart * @param {number=} lineEnd Optional, may be omitted. */ function expandMacros(str, lineStart, lineEnd=str.length) { var len = str.length; var out = ''; for (var i = lineStart; i < lineEnd; ++i) { var kind = classifyChar(str, i); if (kind == 3/*a-z*/) { for (var j = i + 1; j <= lineEnd; ++j) { var kind2 = classifyChar(str, j); if (kind2 != 2/*0-9*/ && kind2 != 3/*a-z*/) { var symbol = str.substring(i, j); #if MIN_FIREFOX_VERSION < 92 // Firefox only introduced Object.hasOwn() in Firefox 92. if (defs.hasOwnProperty(symbol)) { #else if (Object.hasOwn(defs, symbol)) { #endif var pp = defs[symbol], expanded; if (typeof pp == 'function') { // definition is a function? if (pp.length) { // Expanding a macro? (#define FOO(X) ...) while (str[j] && isWhitespace(str, j)) ++j; if (str[j] == '(') { var closeParens = find_closing_parens_index(str, j); // N.b. this has a limitation that multiparameter macros cannot nest with other multiparameter macros // e.g. FOO(a, BAR(b, c)) is not supported. expanded = pp(str.substring(j+1, closeParens).split(',')); if (expanded === !!expanded) expanded = expanded|0; // Convert boolean true/false to int 1/0 j = closeParens+1; } else { var start = j; j = nextWhitespace(str, j); expanded = pp([str.substring(start, j)]); } } else { // A zero-arg function macro (#define FOO() BAR)? expanded = pp(); } } else { // Definition is either a boolean, an integer or a string.. in any case, not a macro. // Expand boolean args from defs, e.g. 'FOO': true as integer 1, // so that further preprocessing won't attempt to search for // a preprocessing macro 'true' as being defined. expanded = (pp === !!pp ? pp|0 : pp); } return expandMacros(str.substring(lineStart, i) + expanded + str.substring(j, lineEnd), 0); } out += symbol; i = j-1; break; } } } else { out += str[i]; } } return out; } // Given a token list e.g. ['2', '>', '1'], returns a function that evaluates that token list. function buildExprTree(tokens) { // Consume tokens array into a function tree until the tokens array is exhausted // to a single root node that evaluates it. while (tokens.length > 1 || typeof tokens[0] != 'function') { tokens = ((tokens) => { // Find the index 'i' of the operator we should evaluate next: var i, j, p, operatorAndPriority = -2; for (j = 0; j < tokens.length; ++j) { if ((p = ['*', '/', '+', '-', '!', '<', '<=', '>', '>=', '==', '!=', '&&', '||', '('].indexOf(tokens[j])) > operatorAndPriority) { i = j; operatorAndPriority = p; } } if (operatorAndPriority == 13 /* parens '(' */) { // Find the closing parens position j = find_closing_parens_index(tokens, i); if (j) { tokens.splice(i, j+1-i, buildExprTree(tokens.slice(i+1, j))); return tokens; } } if (operatorAndPriority == 4 /* unary ! */) { // Special case: the unary operator ! needs to evaluate right-to-left. i = tokens.lastIndexOf('!'); var innerExpr = buildExprTree(tokens.slice(i+1, i+2)); tokens.splice(i, 2, function() { return !innerExpr(); }) return tokens; } // A binary operator: if (operatorAndPriority >= 0) { var left = buildExprTree(tokens.slice(0, i)); var right = buildExprTree(tokens.slice(i+1)); switch(tokens[i]) { case '&&': return [function() { return left() && right(); }]; case '||': return [function() { return left() || right(); }]; case '==': return [function() { return left() == right(); }]; case '!=': return [function() { return left() != right(); }]; case '<' : return [function() { return left() < right(); }]; case '<=': return [function() { return left() <= right(); }]; case '>' : return [function() { return left() > right(); }]; case '>=': return [function() { return left() >= right(); }]; case '+': return [function() { return left() + right(); }]; case '-': return [function() { return left() - right(); }]; case '*': return [function() { return left() * right(); }]; case '/': return [function() { return Math.floor(left() / right()); }]; } } // else a number: #if ASSERTIONS assert(tokens[i] !== ')', 'parse failure, mismatched parentheses in parsing' + tokens.toString()); assert(operatorAndPriority == -1); #endif var num = Number(tokens[i]); return [function() { return num; }] })(tokens); } return tokens[0]; } // Preprocess the input one line at a time. for (; i < len; ++i) { // Find the start of the current line. var lineStart = i; // Seek iterator to end of current line. i = code.indexOf('\n', i); if (i < 0) i = len; // Find the first non-whitespace character on the line. for (var j = lineStart; j < i && isWhitespace(code, j);) ++j; // Is this a non-preprocessor directive line? var thisLineIsInActivePreprocessingBlock = stack[stack.length-1]; if (code[j] != '#') { // non-preprocessor line? if (thisLineIsInActivePreprocessingBlock) { out += expandMacros(code, lineStart, i) + '\n'; } continue; } // This is a preprocessor directive line, e.g. #ifdef or #define. // Parse the line as # var space = nextWhitespace(code, j); var directive = code.substring(j+1, space); var expression = code.substring(space, i).trim(); switch(directive) { case 'if': var tokens = tokenize(expandMacros(expression, 0)); var exprTree = buildExprTree(tokens); var evaluated = exprTree(); stack.push(!!evaluated * stack[stack.length-1]); break; case 'elif': var tokens = tokenize(expandMacros(expression, 0)); var exprTree = buildExprTree(tokens); var evaluated = exprTree(); // If the previous #if / #elif block was executed, output NaN so that all further #elif and #else blocks will // short to false. stack[stack.length-1] = !!evaluated * (stack[stack.length-1] ? NaN : 1-stack[stack.length-1]); break; case 'ifdef': stack.push(!!defs[expression] * stack[stack.length-1]); break; case 'ifndef': stack.push(!defs[expression] * stack[stack.length-1]); break; case 'else': stack[stack.length-1] = (1-stack[stack.length-1]) * stack[stack.length-2]; break; case 'endif': stack.pop(); break; case 'define': if (thisLineIsInActivePreprocessingBlock) { // This could either be a macro with input args (#define MACRO(x,y) x+y), or a direct expansion #define FOO 2, // figure out which. var macroStart = expression.indexOf('('); var firstWs = nextWhitespace(expression, 0); if (firstWs < macroStart) macroStart = 0; if (macroStart > 0) { // #define MACRO( x , y , z ) var macroEnd = expression.indexOf(')', macroStart); let params = expression.substring(macroStart+1, macroEnd).split(',').map(x => x.trim()); let value = tokenize(expression.substring(macroEnd+1).trim()) defs[expression.substring(0, macroStart)] = (args) => { var ret = ''; value.forEach((x) => { var argIndex = params.indexOf(x); ret += (argIndex >= 0) ? args[argIndex] : x; }); return ret; }; } else { // #define FOO (x + y + z) let value = expandMacros(expression.substring(firstWs+1).trim(), 0); defs[expression.substring(0, firstWs)] = () => value; } } break; case 'undef': if (thisLineIsInActivePreprocessingBlock) delete defs[expression]; break; default: if (directive != 'version' && directive != 'pragma' && directive != 'extension' && directive != 'line') { // GLSL shader compiler specific #directives. #if ASSERTIONS err('Unrecognized preprocessor directive #' + directive + '!'); #endif } // Unknown preprocessor macro, just pass through the line to output. out += expandMacros(code, lineStart, i) + '\n'; } } return out; } });