blob: 4d8554ba0d04b9a4b736f9bb33505b5f853e1c15 [file] [log] [blame] [edit]
mergeInto(LibraryManager.library, {
// Removes all C++ '//' and '/* */' comments from the given source string.
// N.b. will also eat comments inside strings.
$remove_cpp_comments_in_shaders: function(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: function(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: ['$jstoi_q', '$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 = defs || {};
defs['defined'] = function(args) { // built-in "#if defined(x)"" macro.
#if ASSERTIONS
assert(args.length == 1);
#endif
return defs[args[0]] ? 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.
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.substr(i, 2);
if (['<=', '>=', '==', '!=', '&&', '||'].includes(op2)) {
out.push(op2);
++i;
} else {
out.push(exprString[i]);
}
}
}
return out;
}
// Expands preprocessing macros on substring str[lineStart...lineEnd]
function expandMacros(str, lineStart, lineEnd) {
if (lineEnd === undefined) 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);
var pp = defs[symbol];
if (pp) {
var expanded = str.substring(lineStart, i);
if (pp.length && str[j] == '(') { // Expanding a macro? (#define FOO(X) ...)
var closeParens = find_closing_parens_index(str, j);
#if ASSERTIONS
assert(str[closeParens] == ')');
#endif
expanded += pp(str.substring(j+1, closeParens).split(',')) + str.substring(closeParens+1, lineEnd);
} else { // Expanding a non-macro (#define FOO BAR)
expanded += pp() + str.substring(j, lineEnd);
}
return expandMacros(expanded, 0);
} else {
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 = (function(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
var 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
if (tokens[i] == ')') throw 'Parsing failure, mismatched parentheses in parsing!' + tokens.toString();
assert(operatorAndPriority == -1);
#endif
var num = jstoi_q(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 #<directive> <expression>
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 '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]; 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 ) <statement of x,y and 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)] = function(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)] = function() {
return value;
};
}
}
break;
case 'undef': if (thisLineIsInActivePreprocessingBlock) delete defs[expression]; break;
default:
if (directive != 'version' && directive != 'pragma' && directive != 'extension') { // 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;
}
});