Skip to content

Commit

Permalink
Merge pull request #30 from ShaderFrog/rename-beta
Browse files Browse the repository at this point in the history
5.0.0: Rename utility functions breaking API change
  • Loading branch information
AndrewRayCode authored Jul 28, 2024
2 parents 2397a8d + e61bd62 commit 62017d8
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 137 deletions.
82 changes: 76 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,18 @@ type ParserOptions = {
// like undefined functions and variables. If `failOnWarn` is set to true,
// warnings will still cause the parser to raise an error. Defaults to false.
quiet: boolean;

// An optional string representing the origin of the GLSL, for debugging and
// error messages. For example, "main.js". If the parser raises an error, the
// grammarSource shows up in the error.source field. If you format the error
// (see the errors section), the grammarSource shows up in the formatted error
// string. Defaults to undefined.
grammarSource: string;

// If true, sets location information on each AST node, in the form of
// { column: number, line: number, offset: number }. Defaults to false.
includeLocation: boolean;

// If true, causes the parser to raise an error instead of log a warning.
// The parser does limited type checking, and things like undeclared variables
// are treated as warnings. Defaults to false.
Expand Down Expand Up @@ -427,19 +430,86 @@ visitors.
### Utility Functions
Rename all the variables in a program:
#### Rename variables / identifiers in a program
You can rename bindings (aka variables), functions, and types (aka structs) with `renameBindings`, `renameFunctions`, and `renameTypes` respectively.
The signature for these methods:
```ts
const renameBindings = (
// The scope to rename the bindings in. ast.scopes[0] is the global scope.
// Passing this ast.scopes[0] renames all global variables
bindings: ScopeIndex,

// The rename function. This is called once per scope entry with the original
// name in the scope, to generate the renamed variable.
mangle: (name: string) => string
): ScopeIndex
```
These scope renaming functions, `renameBindings`, `renameFunctions`, and `renameTypes`, do two things:
1. Each function *mutates* the AST to rename identifiers in place.
2. They *return* an *immutable* new ScopeIndex where the scope references
themselves are renamed.
If you want your ast.scopes array to stay in sync with your AST, you need to
re-assign it to the output of the functions! Examples:
```typescript
import { renameBindings, renameFunctions, renameTypes } from '@shaderfrog/glsl-parser/utils';

// ... parse an ast...
// Suffix top level variables with _x, and update the scope
ast.scopes[0].bindings = renameBindings(ast.scopes[0].bindings, (name) => `${name}_x`);

// 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`);
ast.scopes[0].functions = renameFunctions(ast.scopes[0].functions, (name) => `${name}_x`);

// Suffix struct names and usages (including constructors) with _x
renameTypes(ast.scopes[0], (name, node) => `${name}_x`);
ast.scopes[0].types = renameTypes(ast.scopes[0].types, (name) => `${name}_x`);
```
There are also functions to rename only one variable/identifier in a given
scope. Use these if you know specifically which variable you want to rename.
```typescript
import { renameBinding, renameFunction, renameType } from '@shaderfrog/glsl-parser/utils';

// Replace all instances of "oldVar" with "newVar" in the global scope, and
// creates a new global scope entry named "newVar"
ast.scopes[0].bindings.newVar = renameBinding(
ast.scopes[0].bindings.oldVar,
'newVar',
);
// You need to manually delete the old scope entry if you want the scope to stay
// in sync with your program AST
delete ast.scopes[0].bindings.oldVar;

// Rename a specific function
ast.scopes[0].functions.newFn = renameFunction(
ast.scopes[0].functions.oldFn,
'newFn',
);
delete ast.scopes[0].functions.oldFn;

// Rename a specific type/struct
ast.scopes[0].functions.newType = renametype(
ast.scopes[0].functions.oldType,
'newType',
);
delete ast.scopes[0].functions.oldType;
```
#### Debugging utility functions
The parser also exports a debugging function, useful for logging information
about the AST.
```ts
import { debugScopes } from '@shaderfrog/glsl-parser/parser/utils';

// Print a condensed representation of the AST scopes to the console
debugScopes(ast);
```
## What are "parsing" and "preprocessing"?
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"engines": {
"node": ">=16"
},
"version": "4.1.1",
"version": "5.0.0",
"type": "module",
"description": "A GLSL ES 1.0 and 3.0 parser and preprocessor that can preserve whitespace and comments",
"scripts": {
Expand Down Expand Up @@ -49,6 +49,6 @@
"prettier": "^2.1.2",
"ts-jest": "^29.1.2",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.3.3"
"typescript": "^5.5.3"
}
}
31 changes: 22 additions & 9 deletions src/parser/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ float fn() {
`);

expect(ast.scopes[0].functions.noise);
renameFunctions(ast.scopes[0], (name) => `${name}_FUNCTION`);
ast.scopes[0].functions = renameFunctions(
ast.scopes[0].functions,
(name) => `${name}_FUNCTION`
);
expect(generate(ast)).toBe(`
float noise_FUNCTION() {}
float fn_FUNCTION() {
Expand Down Expand Up @@ -233,8 +236,14 @@ vec4 linearToOutputTexel( vec4 value ) { return LinearToLinear( value ); }
{ quiet: true }
);

renameBindings(ast.scopes[0], (name) => `${name}_VARIABLE`);
renameFunctions(ast.scopes[0], (name) => `${name}_FUNCTION`);
ast.scopes[0].bindings = renameBindings(
ast.scopes[0].bindings,
(name) => `${name}_VARIABLE`
);
ast.scopes[0].functions = renameFunctions(
ast.scopes[0].functions,
(name) => `${name}_FUNCTION`
);

expect(generate(ast)).toBe(`
float selfref_VARIABLE, b_VARIABLE = 1.0, c_VARIABLE = selfref_VARIABLE;
Expand Down Expand Up @@ -306,7 +315,8 @@ StructName main(in StructName x, StructName[3] y) {
float a2 = 1.0 + StructName(1.0).color.x;
}
`);
renameTypes(ast.scopes[0], (name) => `${name}_x`);
ast.scopes[0].types = renameTypes(ast.scopes[0].types, (name) => `${name}_x`);

expect(generate(ast)).toBe(`
struct StructName_x {
vec3 color;
Expand Down Expand Up @@ -342,10 +352,10 @@ StructName_x main(in StructName_x x, StructName_x[3] y) {
]);
expect(Object.keys(ast.scopes[0].bindings)).toEqual(['reflectedLight']);
expect(Object.keys(ast.scopes[0].types)).toEqual([
'StructName',
'OtherStruct',
'StructName_x',
'OtherStruct_x',
]);
expect(ast.scopes[0].types.StructName.references).toHaveLength(16);
expect(ast.scopes[0].types.StructName_x.references).toHaveLength(16);

// Inner struct definition should be found in inner fn scope
expect(Object.keys(ast.scopes[2].types)).toEqual(['StructName']);
Expand All @@ -357,7 +367,7 @@ attribute vec3 position;
vec3 func(vec3 position) {
return position;
}`);
renameBindings(ast.scopes[0], (name) =>
ast.scopes[0].bindings = renameBindings(ast.scopes[0].bindings, (name) =>
name === 'position' ? 'renamed' : name
);
// The func arg "position" shadows the global binding, it should be untouched
Expand All @@ -378,7 +388,10 @@ uniform vec2 vProp;
};`);

// This shouldn't crash - see the comment block in renameBindings()
renameBindings(ast.scopes[0], (name) => `${name}_x`);
ast.scopes[0].bindings = renameBindings(
ast.scopes[0].bindings,
(name) => `${name}_x`
);
expect(generate(ast)).toBe(`
layout(std140,column_major) uniform;
float a_x;
Expand Down
1 change: 0 additions & 1 deletion src/parser/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export const functionDeclarationSignature = (
const quantifiers = specifier.quantifier || [];

const parameterTypes = proto?.parameters?.map(({ specifier, quantifier }) => {
// todo: saving place on putting quantifiers here
const quantifiers =
// vec4[1][2] param
specifier.quantifier ||
Expand Down
31 changes: 0 additions & 31 deletions src/parser/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,37 +67,6 @@ export const buildParser = () => {
// }
// };

export const debugEntry = (bindings: ScopeIndex) => {
return Object.entries(bindings).map(
([k, v]) =>
`${k}: (${v.references.length} references, ${
v.declaration ? '' : 'un'
}declared): ${v.references.map((r) => r.type).join(', ')}`
);
};
export const debugFunctionEntry = (bindings: FunctionScopeIndex) =>
Object.entries(bindings).flatMap(([name, overloads]) =>
Object.entries(overloads).map(
([signature, overload]) =>
`${name} (${signature}): (${overload.references.length} references, ${
overload.declaration ? '' : 'un'
}declared): ${overload.references.map((r) => r.type).join(', ')}`
)
);

export const debugScopes = (astOrScopes: Program | Scope[]) =>
console.log(
'Scopes:',
'scopes' in astOrScopes
? astOrScopes.scopes
: astOrScopes.map((s) => ({
name: s.name,
types: debugEntry(s.types),
bindings: debugEntry(s.bindings),
functions: debugFunctionEntry(s.functions),
}))
);

const middle = /\/\* start \*\/((.|[\r\n])+)(\/\* end \*\/)?/m;

type ParseSrc = (src: string, options?: ParserOptions) => Program;
Expand Down
Loading

0 comments on commit 62017d8

Please sign in to comment.