Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5.0.0: Rename utility functions breaking API change #30

Merged
merged 6 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) so much nicer


// 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
Loading