@shaderfrog/glsl-parser
Version:
A GLSL ES 1.0 and 3.0 parser and preprocessor that can preserve whitespace and comments
475 lines (474 loc) • 18.9 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
import { visit } from '../ast/visit.js';
var without = function (obj) {
var keys = [];
for (var _i = 1; _i < arguments.length; _i++) {
keys[_i - 1] = arguments[_i];
}
return Object.entries(obj).reduce(function (acc, _a) {
var _b;
var key = _a[0], value = _a[1];
return (__assign(__assign({}, acc), (!keys.includes(key) && (_b = {}, _b[key] = value, _b))));
}, {});
};
// Scan for the use of a function-like macro, balancing parentheses until
// encountering a final closing ")" marking the end of the macro use
var scanFunctionArgs = function (src) {
var char;
var parens = 0;
var args = [];
var arg = '';
for (var i = 0; i < src.length; i++) {
char = src.charAt(i);
if (char === '(') {
parens++;
}
if (char === ')') {
parens--;
}
if (parens === -1) {
// In the case of "()", we don't want to add the argument of empty string,
// but we do in case of "(,)" and "(asdf)". When we hit the closing paren,
// only capture the arg of empty string if there was a previous comma,
// which we can infer from there being a previous arg
if (arg !== '' || args.length) {
args.push(arg);
}
return { args: args, length: i };
}
if (char === ',' && parens === 0) {
args.push(arg);
arg = '';
}
else {
arg += char;
}
}
return null;
};
// From glsl2s https://github.com/cimaron/glsl2js/blob/4046611ac4f129a9985d74704159c41a402564d0/preprocessor/comments.js
var preprocessComments = function (src) {
var i;
var chr;
var la;
var out = '';
var line = 1;
var in_single = 0;
var in_multi = 0;
for (i = 0; i < src.length; i++) {
chr = src.substring(i, i + 1);
la = src.substring(i + 1, i + 2);
// Enter single line comment
if (chr == '/' && la == '/' && !in_single && !in_multi) {
in_single = line;
i++;
continue;
}
// Exit single line comment
if (chr == '\n' && in_single) {
in_single = 0;
}
// Enter multi line comment
if (chr == '/' && la == '*' && !in_multi && !in_single) {
in_multi = line;
i++;
continue;
}
// Exit multi line comment
if (chr == '*' && la == '/' && in_multi) {
// Treat single line multi-comment as space
if (in_multi == line) {
out += ' ';
}
in_multi = 0;
i++;
continue;
}
// Newlines are preserved
if ((!in_multi && !in_single) || chr == '\n') {
out += chr;
line++;
}
}
return out;
};
var tokenPaste = function (str) { return str.replace(/\s+##\s+/g, ''); };
var evaluate = function (ast, evaluators) {
var visit = function (node) {
var evaluator = evaluators[node.type];
if (!evaluator) {
throw new Error("No evaluate() evaluator for ".concat(node.type));
}
// I can't figure out why evalutor has node type never here
// @ts-ignore
return evaluator(node, visit);
};
return visit(ast);
};
var expandFunctionMacro = function (macros, macroName, macro, text) {
var pattern = "\\b".concat(macroName, "\\s*\\(");
var startRegex = new RegExp(pattern, 'm');
var expanded = '';
var current = text;
var startMatch;
var _loop_1 = function () {
var result = scanFunctionArgs(current.substring(startMatch.index + startMatch[0].length));
if (result === null) {
throw new Error("".concat(current.match(startRegex), " unterminated macro invocation"));
}
var macroArgs = (macro.args || []).filter(function (arg) { return arg.literal !== ','; });
var args = result.args, argLength = result.length;
// The total length of the raw text to replace is the macro name in the
// text (startMatch), plus the length of the arguments, plus one to
// encompass the closing paren that the scan fn skips
var matchLength = startMatch[0].length + argLength + 1;
if (args.length > macroArgs.length) {
throw new Error("'".concat(macroName, "': Too many arguments for macro"));
}
if (args.length < macroArgs.length) {
throw new Error("'".concat(macroName, "': Not enough arguments for macro"));
}
// Collect the macro identifiers and build a replacement map from those to
// the user defined replacements
var argIdentifiers = macroArgs.map(function (a) { return a.identifier; });
var argKeys = argIdentifiers.reduce(function (acc, identifier, index) {
var _a;
return (__assign(__assign({}, acc), (_a = {}, _a[identifier] = args[index].trim(), _a)));
}, {});
var replacedBody = tokenPaste(macro.body.replace(
// Replace all instances of macro arguments in the macro definition
// (the arg separated by word boundaries) with its user defined
// replacement. This one-pass strategy ensures that we won't clobber
// previous replacements when the user supplied args have the same names
// as the macro arguments
new RegExp('(' + argIdentifiers.map(function (a) { return "\\b".concat(a, "\\b"); }).join("|") + ')', 'g'), function (match) { return (match in argKeys ? argKeys[match] : match); }));
// Any text expanded is then scanned again for more replacements. The
// self-reference rule means that a macro that references itself won't be
// expanded again, so remove it from the search
var expandedReplace = expandMacros(replacedBody, without(macros, macroName));
// We want to break this string at where we finished expanding the macro
var endOfReplace = startMatch.index + expandedReplace.length;
// Replace the use of the macro with the expansion
var processed = current.replace(current.substring(startMatch.index, startMatch.index + matchLength), expandedReplace);
// Add text up to the end of the expanded macro to what we've procssed
expanded += processed.substring(0, endOfReplace);
// Only work on the rest of the text, not what we already expanded. This is
// to avoid a nested macro #define foo() foo() where we'll try to expand foo
// forever. With this strategy, we expand foo() to foo() and move on
current = processed.substring(endOfReplace);
};
while ((startMatch = startRegex.exec(current))) {
_loop_1();
}
return expanded + current;
};
var expandObjectMacro = function (macros, macroName, macro, text) {
var regex = new RegExp("\\b".concat(macroName, "\\b"), 'g');
var expanded = text;
if (regex.test(text)) {
// Macro definitions like
// #define MACRO
// Have null for the body. Make it empty string if null to avoid 'null' expanded
var replacement = macro.body || '';
var firstPass = tokenPaste(text.replace(new RegExp("\\b".concat(macroName, "\\b"), 'g'), replacement));
// Scan expanded text for more expansions. Ignore the expanded macro because
// of the self-reference rule
expanded = expandMacros(firstPass, without(macros, macroName));
}
return expanded;
};
var expandMacros = function (text, macros) {
return Object.entries(macros).reduce(function (result, _a) {
var macroName = _a[0], macro = _a[1];
return macro.args
? expandFunctionMacro(macros, macroName, macro, result)
: expandObjectMacro(macros, macroName, macro, result);
}, text);
};
var isTruthy = function (x) { return !!x; };
// Given an expression AST node, visit it to expand the macro macros to in the
// right places
var expandInExpressions = function (macros) {
var expressions = [];
for (var _i = 1; _i < arguments.length; _i++) {
expressions[_i - 1] = arguments[_i];
}
expressions.forEach(function (expression) {
visitPreprocessedAst(expression, {
unary_defined: {
enter: function (path) {
path.skip();
},
},
identifier: {
enter: function (path) {
path.node.identifier = expandMacros(path.node.identifier, macros);
},
},
});
});
};
var evaluateIfPart = function (macros, ifPart) {
if (ifPart.type === 'if') {
return evaluteExpression(ifPart.expression, macros);
}
else if (ifPart.type === 'ifdef') {
return ifPart.identifier.identifier in macros;
}
else if (ifPart.type === 'ifndef') {
return !(ifPart.identifier.identifier in macros);
}
};
// TODO: Are all of these operators equivalent between javascript and GLSL?
var evaluteExpression = function (node, macros) {
return evaluate(node, {
// TODO: Handle non-base-10 numbers. Should these be parsed in the peg grammar?
int_constant: function (node) { return parseInt(node.token, 10); },
unary_defined: function (node) { return node.identifier.identifier in macros; },
identifier: function (node) { return node.identifier; },
group: function (node, visit) { return visit(node.expression); },
binary: function (_a, visit) {
var left = _a.left, right = _a.right, literal = _a.operator.literal;
switch (literal) {
// multiplicative
case '*': {
return visit(left) * visit(right);
}
// division
case '/': {
return visit(left) / visit(right);
}
// modulo
case '%': {
return visit(left) % visit(right);
}
// addition
case '+': {
return visit(left) + visit(right);
}
// subtraction
case '-': {
return visit(left) - visit(right);
}
// bit-wise shift
case '<<': {
return visit(left) << visit(right);
}
// bit-wise shift
case '>>': {
return visit(left) >> visit(right);
}
case '<': {
return visit(left) < visit(right);
}
case '>': {
return visit(left) > visit(right);
}
case '<=': {
return visit(left) <= visit(right);
}
case '>=': {
return visit(left) >= visit(right);
}
case '==': {
return visit(left) == visit(right);
}
case '!=': {
return visit(left) != visit(right);
}
// bit-wise and
case '&': {
return visit(left) & visit(right);
}
// bit-wise exclusive or
case '^': {
return visit(left) ^ visit(right);
}
// bit-wise inclusive or
case '|': {
return visit(left) | visit(right);
}
case '&&': {
return visit(left) && visit(right);
}
case '||': {
return visit(left) || visit(right);
}
default: {
throw new Error("Preprocessing error: Unknown binary operator ".concat(literal));
}
}
},
unary: function (node, visit) {
switch (node.operator.literal) {
case '+': {
return visit(node.expression);
}
case '-': {
return -1 * visit(node.expression);
}
case '!': {
return !visit(node.expression);
}
case '~': {
return ~visit(node.expression);
}
default: {
throw new Error("Preprocessing error: Unknown unary operator ".concat(node.operator.literal));
}
}
},
});
};
var shouldPreserve = function (preserve) {
if (preserve === void 0) { preserve = {}; }
return function (path) {
var test = preserve === null || preserve === void 0 ? void 0 : preserve[path.node.type];
return typeof test === 'function' ? test(path) : test;
};
};
// @ts-ignore
export var visitPreprocessedAst = visit;
var convertPath = function (p) {
return p;
};
var preprocessAst = function (program, options) {
if (options === void 0) { options = {}; }
var macros = Object.entries(options.defines || {}).reduce(function (defines, _a) {
var _b;
var name = _a[0], body = _a[1];
return (__assign(__assign({}, defines), (_b = {}, _b[name] = { body: body }, _b)));
}, {});
var preserve = options.preserve;
var preserveNode = shouldPreserve(preserve);
visitPreprocessedAst(program, {
conditional: {
enter: function (initialPath) {
var path = convertPath(initialPath);
var node = path.node;
// TODO: Determining if we need to handle edge case conditionals here
if (preserveNode(path)) {
return;
}
// Expand macros in if/else *expressions* only. Macros are expanded in:
// #if X + 1
// #elif Y + 2
// But *not* in
// # ifdef X
// Because X should not be expanded in the ifdef. Note that
// # if defined(X)
// does have an expression, but the skip() in unary_defined prevents
// macro expansion in there. Checking for .expression and filtering out
// any conditionals without expressions is how ifdef is avoided.
// It's not great that ifdef is skipped differentaly than defined().
expandInExpressions.apply(void 0, __spreadArray([macros], __spreadArray([
node.ifPart.expression
], node.elseIfParts.map(function (elif) { return elif.expression; }), true).filter(isTruthy), false));
if (evaluateIfPart(macros, node.ifPart)) {
path.replaceWith(node.ifPart.body);
}
else {
var elseBranchHit = node.elseIfParts.reduce(function (res, elif) {
return res ||
(evaluteExpression(elif.expression, macros) &&
// path/visit hack to remove type error
(path.replaceWith(elif.body), true));
}, false);
if (!elseBranchHit) {
if (node.elsePart) {
path.replaceWith(node.elsePart.body);
}
else {
path.remove();
}
}
}
},
},
text: {
enter: function (initialPath) {
var path = convertPath(initialPath);
path.node.text = expandMacros(path.node.text, macros);
},
},
define_arguments: {
enter: function (initialPath) {
var path = convertPath(initialPath);
var _a = path.node, identifier = _a.identifier.identifier, body = _a.body, args = _a.args;
macros[identifier] = { args: args, body: body };
!preserveNode(path) && path.remove();
},
},
define: {
enter: function (initialPath) {
var path = convertPath(initialPath);
var _a = path.node, identifier = _a.identifier.identifier, body = _a.body;
macros[identifier] = { body: body };
!preserveNode(path) && path.remove();
},
},
undef: {
enter: function (initialPath) {
var path = convertPath(initialPath);
delete macros[path.node.identifier.identifier];
!preserveNode(path) && path.remove();
},
},
error: {
enter: function (initialPath) {
var path = convertPath(initialPath);
if (options.stopOnError) {
throw new Error(path.node.message);
}
!preserveNode(path) && path.remove();
},
},
pragma: {
enter: function (initialPath) {
var path = convertPath(initialPath);
!preserveNode(path) && path.remove();
},
},
version: {
enter: function (initialPath) {
var path = convertPath(initialPath);
!preserveNode(path) && path.remove();
},
},
extension: {
enter: function (initialPath) {
var path = convertPath(initialPath);
!preserveNode(path) && path.remove();
},
},
// TODO: Causes a failure
line: {
enter: function (initialPath) {
var path = convertPath(initialPath);
!preserveNode(path) && path.remove();
},
},
});
// Even though it mutates, useful for passing around functions
return program;
};
export { preprocessAst, preprocessComments };