The Shaderfrog GLSL compiler is an open source GLSL 1.00 and 3.00 parser and preprocessor that compiles back to GLSL. Both the parser and preprocessor can preserve comments and whitespace.
The parser uses PEG grammar via the Peggy Javascript library. The PEG grammars for both the preprocessor and main parser are in the source code on Github.
This library has limited Typescript support.
See the state of this library for limitations and goals of this compiler.
npm install --save @shaderfrog/glsl-parserimport { parser, generate } from '@shaderfrog/glsl-parser';
// To parse a GLSL program's source code into an AST:
const ast = parser.parse('float a = 1.0;');
// To turn a parsed AST back into a source program
const program = generate(ast);The parser accepts an optional second options argument:
parser.parse('float a = 1.0;', options);Where options is:
{
// Hide warnings. If set to false or not set, then the parser logs warnings
// like undefined functions and variables
quiet: boolean,
// The origin of the GLSL, for debugging. For example, "main.js", If the
// parser raises an error (specifically a GrammarError), and you call
// error.format([]) on it, the error shows { source: 'main.js', ... }
grammarSource: string | object,
// If true, sets location information on each AST node, in the form of
// { column: number, line: number, offset: number }
includeLocation: boolean
}See the GLSL Langauge Spec to learn more about GLSL preprocessing. Some notable
differences from the C++ parser are no "stringize" operator (#), no #include
operator, and #if expressions can only operate on integer constants, not other
types of data. The Shaderfrog GLSL preprocessor can't be used as a C/C++
preprocessor without modification.
import preprocess from '@shaderfrog/glsl-parser/preprocessor';
// Preprocess a program
console.log(preprocess(`
#define a 1
float b = a;
`, options));Where options is:
{
// Don't strip comments before preprocessing
preserveComments: boolean,
// Macro definitions to use when preprocessing
defines: {
SOME_MACRO_NAME: 'macro body'
},
// A list of callbacks evaluted for each node type, and returns whether or not
// this AST node is subject to preprocessing
preserve: {
ast_node_name: (path) => boolean
}
}A preprocessed program string can be handed off to the main GLSL parser.
If you want more control over preprocessing, the preprocess function above is
a convenience method for approximately the following:
import {
preprocessAst,
preprocessComments,
generate,
parser,
} from '@shaderfrog/glsl-parser/preprocessor';
// Remove comments before preprocessing
const commentsRemoved = preprocessComments(`float a = 1.0;`)
// Parse the source text into an AST
const ast = parser.parse(commentsRemoved);
// Then preproces it, expanding #defines, evaluating #ifs, etc
preprocessAst(ast);
// Then convert it back into a program string, which can be passed to the
// core glsl parser
const preprocessed = preprocessorGenerate(ast);The Shaderfrog parser provides a AST visitor function for manipulating and searching an AST. The visitor API loosely follows the Babel visitor API. A visitor object looks like:
const visitors = {
function_call: {
enter: (path) => {},
exit: (path) => {},
}
}Where every key in the object is a node type, and every value is an object
with optional enter and exit functions. What's passed to each function
is not the AST node itself, instead it's a "path" object, which gives you
information about the node's parents, methods to manipulate the node, and the
node itself. The path object:
{
// Properties:
// The node itself
node: AstNode;
// The parent of this node
parent: AstNode | null;
// The parent path of this path
parentPath: Path | null;
// The key of this node in the parent object, if node parent is an object
key: string | null;
// The index of this node in the parent array, if node parent is an array
index: number | null;
// Methods:
// Don't visit any children of this node
skip: () => void;
// Remove this node from the AST
remove: () => void;
// Replace this node with another AST node
replaceWith: (replacer: any) => void;
// Search for parents of this node's parent using a test function
findParent: (test: (p: Path) => boolean) => Path | null;
}Visit an AST by calling the visit method with an AST and visitors:
import { visit } from '@shaderfrog/glsl-parser/ast';
visit(ast, visitors);The visit function doesn't return a value. If you want to collect data from the AST, use a variable in the outer scope to collect data. For example:
let numberOfFunctionCalls = 0;
visit(ast, {
function_call: {
enter: (path) => {
numberOfFunctionCalls += 1;
},
}
});
console.log('There are ', numberOfFunctionCalls, 'function calls');Rename all the variables in a program:
import { renameBindings, renameFunctions, renameTypes } from '@shaderfrog/glsl-parser/utils';
// ... parse an ast...
// Suffix top level variables with _x
renameBindings(ast.scopes[0], (name, node) => `${name}_x`);
// Suffix function names with _x
renameFunctions(ast.scopes[0], (name, node) => `${name}_x`);
// Suffix struct names and usages (including constructors) with _x
renameTypes(ast.scopes[0], (name, node) => `${name}_x`);In general, a parser is a computer program that analyzes source code and turn it into a data structure called an "abstract syntax tree" (AST). The AST is a tree representation of the source program, which can be analyzed or manipulated. A use of this GLSL parser could be to parse a program into an AST, find all variable names in the AST, rename them, and generate new GLSL source code with renamed variables.
GLSL supports "preprocessing," a compiler text manipulation step. GLSL's preprocessor is based on the C++ preprocessor. This library supports limited preprocessing.
Parsing, preprocesing, and code generation, are all phases of a compiler. This library is technically a source code > source code compiler, also known as a "transpiler." The input and output source code are both GLSL.
The Shaderfrog compiler has tests for the more complex parts of the GLSL ES 3.00 grammar. This library is definitively the most complete GLSL compiler written in Javascript.
This library is used by the experimental Shaderfrog 2.0 shader composer. The compiler has wide expoure to different GLSL programs.
This library also exposed:
This library doesn't support full "semantic analysis" required by the Khronos
GLSL specification. For example, some tokens are only valid in GLSL 1.00 vs
3.00, like texture() vs texture2D(). This parser considers both valid as
they're both part of the grammar. However if you send compiled source code off
to a native compiler like ANGLE with the wrong texture function, it will fail
to compile.
This library is mainly for manipulating ASTs before handing off a generated program to a downstream compilers like as ANGLE.
The preprocessor supports full macro evaluations and expansions, with the
exceptions of __LINE__. Additional control lines like #error and #pragma
and #extension have no effect, and can be fully preserved as part of parsing.
- There's probably some bugs in the preprocessor logic. I haven't yet verified
all of the evaluations of "binary" expressions in
preprocessor.ts preprocessor.tshas lots of yucky typecasting
- Compilers are supposed to raise an error if a switch body ends in a case or default label.
- Currently no semantic analysis of vertex vs fragment shaders
selection_statementis renamed toif_statement- The grammar specifies
declarationitself ends with a semicolon. I moved the semicolon into thedeclaration_statementrule. - The grammar has a left paren "(" in the function_call production. Due to how I de-left-recursed the function_call -> postfix_expression loop, I moved the left paren into the function_identifier production.
- Function calls in the grammar are TYPE_NAME LEFT_PAREN, in my grammar they're IDENTIFIER LEFT_PAREN, because TYPE_NAME is only used for structs, and function names are stored in their own separate place in the scope.
To run the tests (and do other things), you must first build the parser files
using Peggy. Run ./build.sh to generate these files.
To work on the tests, run npx jest --watch.