Path: blob/main/src/lib/libc_preprocessor.js
6171 views
addToLibrary({1// Removes all C++ '//' and '/* */' comments from the given source string.2// N.b. will also eat comments inside strings.3$remove_cpp_comments_in_shaders: (code) => {4var i = 0, out = '', ch, next, len = code.length;5for (; i < len; ++i) {6ch = code[i];7if (ch == '/') {8next = code[i+1];9if (next == '/') {10while (i < len && code[i+1] != '\n') ++i;11} else if (next == '*') {12while (i < len && (code[i-1] != '*' || code[i] != '/')) ++i;13} else {14out += ch;15}16} else {17out += ch;18}19}20return out;21},2223// Finds the index of closing parens from the opening parens at arr[i].24// Used polymorphically for strings ("foo") and token arrays (['(', 'foo', ')']) as input.25$find_closing_parens_index: (arr, i, opening='(', closing=')') => {26for (var nesting = 0; i < arr.length; ++i) {27if (arr[i] == opening) ++nesting;28if (arr[i] == closing && --nesting == 0) {29return i;30}31}32},3334// Runs C preprocessor algorithm on the given string 'code'.35// Supported preprocessor directives: #if, #ifdef, #ifndef, #else, #elif, #endif, #define and #undef.36// predefs: Specifies a dictionary of { 'key1': function(arg0, arg1) {...}, 'key2': ... } of predefined preprocessing variables37$preprocess_c_code__deps: ['$find_closing_parens_index'],38$preprocess_c_code: function(code, defs = {}) {39var i = 0, // iterator over the input string40len = code.length, // cache input length41out = '', // generates the preprocessed output string42stack = [1]; // preprocessing stack (state of active/inactive #ifdef/#else blocks we are currently inside)43// a mapping 'symbolname' -> function(args) which evaluates the given cpp macro, e.g. #define FOO(x) x+10.44defs['defined'] = (args) => { // built-in "#if defined(x)"" macro.45#if ASSERTIONS46assert(args.length == 1);47assert(/^[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)48#endif49return defs[args[0].trim()] ? 1 : 0;50};5152// Returns true if str[i] is whitespace.53function isWhitespace(str, i) {54return !(str.charCodeAt(i) > 32); // Compare as negation to treat end-of-string undefined as whitespace55}5657// Returns index to the next whitespace character starting at str[i].58function nextWhitespace(str, i) {59while (!isWhitespace(str, i)) ++i;60return i;61}6263// Returns an integer ID classification of the character at str[idx], used for tokenization purposes.64function classifyChar(str, idx) {65var cc = str.charCodeAt(idx);66#if ASSERTIONS67assert(!(cc > 127), "Only 7-bit ASCII can be used in preprocessor #if/#ifdef/#define statements!");68#endif69if (cc > 32) {70if (cc < 48) return 1; // an operator symbol, any of !"#$%&'()*+,-./71if (cc < 58) return 2; // a number 012345678972if (cc < 65) return 1; // an operator symbol, any of :;<=>?@73if (cc < 91 || cc == 95/*_*/) return 3; // a character, any of A-Z or _74if (cc < 97) return 1; // an operator symbol, any of [\]^`75if (cc < 123) return 3; // a character, any of a-z76return 1; // an operator symbol, any of {|}~77}78return cc < 33 ? 0 : 4; // 0=whitespace, 4=end-of-string79}8081// Returns a tokenized array of the given string expression, i.e. "FOO > BAR && BAZ" -> ["FOO", ">", "BAR", "&&", "BAZ"]82// Optionally keeps whitespace as tokens to be able to reconstruct the original input string.83/**84* @param {string} exprString85* @param {(number|boolean)=} keepWhitespace Optional, can be omitted. Defaults to false.86*/87function tokenize(exprString, keepWhitespace) {88var out = [], len = exprString.length;89for (var i = 0; i <= len; ++i) {90var kind = classifyChar(exprString, i);91if (kind == 2/*0-9*/ || kind == 3/*a-z*/) { // a character or a number92for (var j = i+1; j <= len; ++j) {93var kind2 = classifyChar(exprString, j);94if (kind2 != kind && (kind2 != 2/*0-9*/ || kind != 3/*a-z*/)) { // parse number sequence "423410", and identifier sequence "FOO32BAR"95out.push(exprString.substring(i, j));96i = j-1;97break;98}99}100} else if (kind == 1/*operator symbol*/) {101// Lookahead for two-character operators.102var op2 = exprString.slice(i, i + 2);103if (['<=', '>=', '==', '!=', '&&', '||'].includes(op2)) {104out.push(op2);105++i;106} else {107out.push(exprString[i]);108}109}110}111return out;112}113114// Expands preprocessing macros on substring str[lineStart...lineEnd]115/**116* @param {string} str117* @param {number} lineStart118* @param {number=} lineEnd Optional, may be omitted.119*/120function expandMacros(str, lineStart, lineEnd=str.length) {121var len = str.length;122var out = '';123for (var i = lineStart; i < lineEnd; ++i) {124var kind = classifyChar(str, i);125if (kind == 3/*a-z*/) {126for (var j = i + 1; j <= lineEnd; ++j) {127var kind2 = classifyChar(str, j);128if (kind2 != 2/*0-9*/ && kind2 != 3/*a-z*/) {129var symbol = str.substring(i, j);130if (Object.hasOwn(defs, symbol)) {131var pp = defs[symbol], expanded;132if (typeof pp == 'function') { // definition is a function?133if (pp.length) { // Expanding a macro? (#define FOO(X) ...)134while (str[j] && isWhitespace(str, j)) ++j;135if (str[j] == '(') {136var closeParens = find_closing_parens_index(str, j);137// N.b. this has a limitation that multiparameter macros cannot nest with other multiparameter macros138// e.g. FOO(a, BAR(b, c)) is not supported.139expanded = pp(str.substring(j+1, closeParens).split(','));140if (expanded === !!expanded) expanded = expanded|0; // Convert boolean true/false to int 1/0141j = closeParens+1;142} else {143var start = j;144j = nextWhitespace(str, j);145expanded = pp([str.substring(start, j)]);146}147} else { // A zero-arg function macro (#define FOO() BAR)?148expanded = pp();149}150} else { // Definition is either a boolean, an integer or a string.. in any case, not a macro.151// Expand boolean args from defs, e.g. 'FOO': true as integer 1,152// so that further preprocessing won't attempt to search for153// a preprocessing macro 'true' as being defined.154expanded = (pp === !!pp ? pp|0 : pp);155}156return expandMacros(str.substring(lineStart, i) + expanded + str.substring(j, lineEnd), 0);157}158out += symbol;159i = j-1;160break;161}162}163} else {164out += str[i];165}166}167return out;168}169170// Given a token list e.g. ['2', '>', '1'], returns a function that evaluates that token list.171function buildExprTree(tokens) {172// Consume tokens array into a function tree until the tokens array is exhausted173// to a single root node that evaluates it.174while (tokens.length > 1 || typeof tokens[0] != 'function') {175tokens = ((tokens) => {176// Find the index 'i' of the operator we should evaluate next:177var i, j, p, operatorAndPriority = -2;178for (j = 0; j < tokens.length; ++j) {179if ((p = ['*', '/', '+', '-', '!', '<', '<=', '>', '>=', '==', '!=', '&&', '||', '('].indexOf(tokens[j])) > operatorAndPriority) {180i = j;181operatorAndPriority = p;182}183}184185if (operatorAndPriority == 13 /* parens '(' */) {186// Find the closing parens position187j = find_closing_parens_index(tokens, i);188if (j) {189tokens.splice(i, j+1-i, buildExprTree(tokens.slice(i+1, j)));190return tokens;191}192}193194if (operatorAndPriority == 4 /* unary ! */) {195// Special case: the unary operator ! needs to evaluate right-to-left.196i = tokens.lastIndexOf('!');197var innerExpr = buildExprTree(tokens.slice(i+1, i+2));198tokens.splice(i, 2, function() { return !innerExpr(); })199return tokens;200}201202// A binary operator:203if (operatorAndPriority >= 0) {204var left = buildExprTree(tokens.slice(0, i));205var right = buildExprTree(tokens.slice(i+1));206switch(tokens[i]) {207case '&&': return [function() { return left() && right(); }];208case '||': return [function() { return left() || right(); }];209case '==': return [function() { return left() == right(); }];210case '!=': return [function() { return left() != right(); }];211case '<' : return [function() { return left() < right(); }];212case '<=': return [function() { return left() <= right(); }];213case '>' : return [function() { return left() > right(); }];214case '>=': return [function() { return left() >= right(); }];215case '+': return [function() { return left() + right(); }];216case '-': return [function() { return left() - right(); }];217case '*': return [function() { return left() * right(); }];218case '/': return [function() { return Math.floor(left() / right()); }];219}220}221// else a number:222#if ASSERTIONS223assert(tokens[i] !== ')', 'Parsing failure, mismatched parentheses in parsing!' + tokens.toString());224assert(operatorAndPriority == -1);225#endif226var num = Number(tokens[i]);227return [function() { return num; }]228})(tokens);229}230return tokens[0];231}232233// Preprocess the input one line at a time.234for (; i < len; ++i) {235// Find the start of the current line.236var lineStart = i;237238// Seek iterator to end of current line.239i = code.indexOf('\n', i);240if (i < 0) i = len;241242// Find the first non-whitespace character on the line.243for (var j = lineStart; j < i && isWhitespace(code, j);) ++j;244245// Is this a non-preprocessor directive line?246var thisLineIsInActivePreprocessingBlock = stack[stack.length-1];247if (code[j] != '#') { // non-preprocessor line?248if (thisLineIsInActivePreprocessingBlock) {249out += expandMacros(code, lineStart, i) + '\n';250}251continue;252}253// This is a preprocessor directive line, e.g. #ifdef or #define.254255// Parse the line as #<directive> <expression>256var space = nextWhitespace(code, j);257var directive = code.substring(j+1, space);258var expression = code.substring(space, i).trim();259switch(directive) {260case 'if':261var tokens = tokenize(expandMacros(expression, 0));262var exprTree = buildExprTree(tokens);263var evaluated = exprTree();264stack.push(!!evaluated * stack[stack.length-1]);265break;266case 'elif':267var tokens = tokenize(expandMacros(expression, 0));268var exprTree = buildExprTree(tokens);269var evaluated = exprTree();270// If the previous #if / #elif block was executed, output NaN so that all further #elif and #else blocks will271// short to false.272stack[stack.length-1] = !!evaluated * (stack[stack.length-1] ? NaN : 1-stack[stack.length-1]);273break;274case 'ifdef': stack.push(!!defs[expression] * stack[stack.length-1]); break;275case 'ifndef': stack.push(!defs[expression] * stack[stack.length-1]); break;276case 'else': stack[stack.length-1] = (1-stack[stack.length-1]) * stack[stack.length-2]; break;277case 'endif': stack.pop(); break;278case 'define':279if (thisLineIsInActivePreprocessingBlock) {280// This could either be a macro with input args (#define MACRO(x,y) x+y), or a direct expansion #define FOO 2,281// figure out which.282var macroStart = expression.indexOf('(');283var firstWs = nextWhitespace(expression, 0);284if (firstWs < macroStart) macroStart = 0;285if (macroStart > 0) { // #define MACRO( x , y , z ) <statement of x,y and z>286var macroEnd = expression.indexOf(')', macroStart);287let params = expression.substring(macroStart+1, macroEnd).split(',').map(x => x.trim());288let value = tokenize(expression.substring(macroEnd+1).trim())289defs[expression.substring(0, macroStart)] = (args) => {290var ret = '';291value.forEach((x) => {292var argIndex = params.indexOf(x);293ret += (argIndex >= 0) ? args[argIndex] : x;294});295return ret;296};297} else { // #define FOO (x + y + z)298let value = expandMacros(expression.substring(firstWs+1).trim(), 0);299defs[expression.substring(0, firstWs)] = () => value;300}301}302break;303case 'undef': if (thisLineIsInActivePreprocessingBlock) delete defs[expression]; break;304default:305if (directive != 'version' && directive != 'pragma' && directive != 'extension' && directive != 'line') { // GLSL shader compiler specific #directives.306#if ASSERTIONS307err('Unrecognized preprocessor directive #' + directive + '!');308#endif309}310311// Unknown preprocessor macro, just pass through the line to output.312out += expandMacros(code, lineStart, i) + '\n';313}314}315return out;316}317});318319320