Skip to content

Commit 1f51744

Browse files
Merge pull request #11 from ShaderFrog/typescript-scopes
Refactor to support tracking undeclared functions and types
2 parents 70259f7 + d1c2510 commit 1f51744

23 files changed

+2655
-1509
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ jobs:
2525
- run: npm ci
2626
- name: Run tests
2727
run: npm test
28+
- name: Typecheck
29+
run: npx tsc --noEmit

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ node_modules
22
dist
33
.vscode
44
.DS_Store
5+
tmp
6+
src/parser/parser.js
7+
tsconfig.tsbuildinfo

README.md

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ npm install --save @shaderfrog/glsl-parser
2828
import { parser, generate } from '@shaderfrog/glsl-parser';
2929

3030
// To parse a GLSL program's source code into an AST:
31-
const ast = parser.parse('float a = 1.0;');
31+
const program = parser.parse('float a = 1.0;');
3232

3333
// To turn a parsed AST back into a source program
34-
const program = generate(ast);
34+
const transpiled = generate(program);
3535
```
3636

3737
The parser accepts an optional second `options` argument:
@@ -41,18 +41,24 @@ parser.parse('float a = 1.0;', options);
4141

4242
Where `options` is:
4343

44-
```js
45-
{
44+
```typescript
45+
type ParserOptions = {
4646
// Hide warnings. If set to false or not set, then the parser logs warnings
47-
// like undefined functions and variables
48-
quiet: boolean,
47+
// like undefined functions and variables. If `failOnWarn` is set to true,
48+
// warnings will still cause the parser to raise an error. Defaults to false.
49+
quiet: boolean;
4950
// The origin of the GLSL, for debugging. For example, "main.js", If the
5051
// parser raises an error (specifically a GrammarError), and you call
51-
// error.format([]) on it, the error shows { source: 'main.js', ... }
52-
grammarSource: string,
52+
// error.format([]) on it, the error shows { source: 'main.js', ... }.
53+
// Defaults to null.
54+
grammarSource: string;
5355
// If true, sets location information on each AST node, in the form of
54-
// { column: number, line: number, offset: number }
55-
includeLocation: boolean
56+
// { column: number, line: number, offset: number }. Defaults to false.
57+
includeLocation: boolean;
58+
// If true, causes the parser to raise an error instead of log a warning.
59+
// The parser does limited type checking, and things like undeclared variables
60+
// are treated as warnings. Defaults to false.
61+
failOnWarn: boolean;
5662
}
5763
```
5864
@@ -76,8 +82,8 @@ console.log(preprocess(`
7682

7783
Where `options` is:
7884

79-
```js
80-
{
85+
```typescript
86+
type PreprocessorOptions = {
8187
// Don't strip comments before preprocessing
8288
preserveComments: boolean,
8389
// Macro definitions to use when preprocessing
@@ -109,16 +115,98 @@ import {
109115
const commentsRemoved = preprocessComments(`float a = 1.0;`)
110116

111117
// Parse the source text into an AST
112-
const ast = parser.parse(commentsRemoved);
118+
const program = parser.parse(commentsRemoved);
113119

114120
// Then preproces it, expanding #defines, evaluating #ifs, etc
115-
preprocessAst(ast);
121+
preprocessAst(program);
116122

117123
// Then convert it back into a program string, which can be passed to the
118124
// core glsl parser
119-
const preprocessed = preprocessorGenerate(ast);
125+
const preprocessed = preprocessorGenerate(program);
120126
```
121127

128+
## Scope
129+
130+
`parse()` returns a [`Program`], which has a `scopes` array on it. A scope looks
131+
like:
132+
```typescript
133+
type Scope = {
134+
name: string;
135+
parent?: Scope;
136+
bindings: ScopeIndex;
137+
types: TypeScopeIndex;
138+
functions: FunctionScopeIndex;
139+
location?: LocationObject;
140+
}
141+
```
142+
143+
The `name` of a scope is either `"global"`, the name of the function that
144+
introduced the scope, or in anonymous blocks, `"{"`. In each scope, `bindings` represents variables,
145+
`types` represents user-created types (structs in GLSL), and `functions` represents
146+
functions.
147+
148+
For `bindings` and `types`, the scope index looks like:
149+
```typescript
150+
type ScopeIndex = {
151+
[name: string]: {
152+
declaration?: AstNode;
153+
references: AstNode[];
154+
}
155+
}
156+
```
157+
158+
Where `name` is the name of the variable or type. `declaration` is the AST node
159+
where the variable was declared. In the case the variable is used without being
160+
declared, `declaration` won't be present. If you set the [`failOnWarn` parser
161+
option](#Parsing) to `true`, the parser will throw an error when encountering
162+
an undeclared variable, rather than allow a scope entry without a declaration.
163+
164+
For `functions`, the scope index is slighty different:
165+
```typescript
166+
type FunctionScopeIndex = {
167+
[name: string]: {
168+
[signature: string]: {
169+
returnType: string;
170+
parameterTypes: string[];
171+
declaration?: FunctionNode;
172+
references: AstNode[];
173+
}
174+
}
175+
};
176+
```
177+
178+
Where `name` is the name of the function, and `signature` is a string representing
179+
the function's return and parameter types, in the form of `"returnType: paramType1, paramType2, ..."`
180+
or `"returnType: void"` in the case of no arguments. Each `signature` in this
181+
index represents an "overloaded" function in GLSL, as in:
182+
183+
```glsl
184+
void someFunction(int x) {};
185+
void someFunction(int x, int y) {};
186+
```
187+
188+
With this source code, there will be two entries under `name`, one for each
189+
overload signature. The `references` are the uses of that specific overloaded
190+
version of the function. `references` also contains the function prototypes
191+
for the overloaded function, if present.
192+
193+
In the case there is only one declaration for a function, there will still be
194+
a single entry under `name` with the function's `signature`.
195+
196+
⚠️ Caution! This parser does very limited type checking. This leads to a known
197+
case where a function call can match to the wrong overload in scope:
198+
199+
```glsl
200+
void someFunction(float, float);
201+
void someFunction(bool, bool);
202+
someFunction(true, true); // This will be attributed to the wrong scope entry
203+
```
204+
205+
The parser doesn't know the type of the operands in the function call, so it
206+
matches based on the name and arity of the functions.
207+
208+
See also [#Utility-Functions] for renaming scope references.
209+
122210
## Manipulating and Searching ASTs
123211

124212
### Visitors
@@ -283,7 +371,17 @@ and `#extension` have no effect, and can be fully preserved as part of parsing.
283371

284372
# Local Development
285373

286-
To run the tests (and do other things), you must first build the parser files
287-
using Peggy. Run `./build.sh` to generate these files.
288-
289374
To work on the tests, run `npx jest --watch`.
375+
376+
The GLSL grammar definition lives in `src/parser/glsl-grammar.pegjs`. Peggyjs
377+
supports inlining Javascript code in the `.pegjs` file to define utility
378+
functions, but that means you have to write in vanilla Javascript, which is
379+
terrible. Instead, I've pulled out utility functions into the `grammar.ts`
380+
entrypoint. Some functions need access to Peggy's local variables, like
381+
`location(s)`, so the `makeLocals()` function uses a closure to provide that
382+
access.
383+
384+
To submit a change, please open a pull request. Tests are appreciated!
385+
386+
See [the Github workflow](.github/workflows/main.yml) for the checks run against
387+
each PR.

build.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ mkdir -p dist
88
# Compile the typescript project
99
npx tsc
1010

11-
# Build the parers with peggy. Requires tsc to run first for the subfolders
1211
npx peggy --cache -o dist/parser/parser.js src/parser/glsl-grammar.pegjs
1312
# Manualy copy in the type definitions
14-
cp src/parser/parser.d.ts dist/parser/parser.d.ts
13+
cp src/parser/parser.d.ts dist/parser/
1514

1615
npx peggy --cache -o dist/preprocessor/preprocessor-parser.js src/preprocessor/preprocessor-grammar.pegjs
1716
cp src/preprocessor/preprocessor-parser.d.ts dist/preprocessor/preprocessor-parser.d.ts

jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
2-
testPathIgnorePatterns: ['dist/'],
32
moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'pegjs', 'glsl'],
3+
modulePathIgnorePatterns: ['src/parser/parser.js'],
4+
testPathIgnorePatterns: ['dist', 'src/parser/parser.js'],
45
};

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"engines": {
44
"node": ">=16"
55
},
6-
"version": "1.4.2",
6+
"version": "2.0.0",
77
"description": "A GLSL ES 1.0 and 3.0 parser and preprocessor that can preserve whitespace and comments",
88
"scripts": {
99
"prepare": "npm run build && ./prepublish.sh",
@@ -44,6 +44,6 @@
4444
"jest": "^27.0.2",
4545
"peggy": "^1.2.0",
4646
"prettier": "^2.1.2",
47-
"typescript": "^4.9.3"
47+
"typescript": "^4.9.5"
4848
}
4949
}

0 commit comments

Comments
 (0)