Skip to content

Evaluating expanded macros in conditional expressions#58

Merged
AndrewRayCode merged 5 commits intomainfrom
token-evaluation
Mar 6, 2026
Merged

Evaluating expanded macros in conditional expressions#58
AndrewRayCode merged 5 commits intomainfrom
token-evaluation

Conversation

@AndrewRayCode
Copy link
Collaborator

@AndrewRayCode AndrewRayCode commented Aug 22, 2025

Fix for bug where #if expressions with macros inside aren't evaluated properly

In the preprocessor grammar spec, there is a grammar spec for if statements

if-line:
 #if constant-expression
 #ifdef identifier
 #ifndef identifier

If there are no macros to expand, parsing a program can be done in one pass then evaluated.

If there is a define like

#define A 1 +
#if A 2 == 3
true
#end

Then in the current version of the parser, there is a bug, because while it does properly expand "A 2" to "1 + 2", it does not properly re-create the AST here, so when the AST is evaluated, it does not evaluate "1 + 2", it does something weird, and produces the wrong output.

The fix is to parse the expressions in the grammar as strings, perform macro expansion on that string first, then parse them on the

@06wj
Copy link
Contributor

06wj commented Aug 22, 2025

Thanks!

@AndrewRayCode AndrewRayCode force-pushed the token-evaluation branch 2 times, most recently from 9f7856b to 20b8b6f Compare March 6, 2026 02:10
"watch-test": "jest --watch",
"test": "jest --colors"
"watch-test": "vitest --watch",
"test": "vitest"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This PR includes switching to vitest which is much faster than jest

'npx peggy --cache --format es --allowed-start-rules program,constant_expression -o src/preprocessor/preprocessor-parser.js src/preprocessor/preprocessor-grammar.pegjs'
);
const { parse, parser } = require('../preprocessor');
const { parse, parser } = await import('../preprocessor/index.js');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ths might be introducing a bug where changing the grammar does not update in the tests

properly

In the preprocessor grammar spec, there is a grammar spec for if statements

````
if-line:
 #if constant-expression
 #ifdef identifier
 #ifndef identifier
````

If there are no macros to expand, parsing a program can be done in one pass then evaluated.

If there is a define like

```
true
```

Then in the current version of the parser, there is a bug, because while
it does properly expand "A 2" to "1 + 2", it does not properly re-create
the AST here, so when the AST is evaluated, it does not evaluate "1 +
2", it does something weird, and produces the wrong output.

The fix is to parse the expressions in the grammar as strings, perform
macro expansion on that string first, then parse them on the fly using
the peggy parser starting from a specific entry point
…s compiled TypeScript — including *.test.js files — into the root-level preprocessor/, parser/, and ast/

directories. Vitest found and ran these compiled test files alongside the TypeScript ones. The compiled preprocessor/preprocessor.test.js imported GlslSyntaxError from ../error.js
(root-level compiled), but buildPreprocessorParser dynamically imported preprocessor/index.js which was resolved by Vite's TypeScript-aware resolver to src/preprocessor/index.ts →
src/error.ts. Two different class instances → instanceof failed.

Two fixes applied:
1. vite.config.ts: Added include: ['src/**/*.test.ts'] so Vitest only runs the canonical TypeScript test files in src/, ignoring compiled JS copies elsewhere.
2. prepublish.sh: Added cleanup to delete *.test.js and *.test.d.ts from published directories — test files don't belong in the npm package and cause this discovery problem.
/ token:IF expression:constant_expression? {
// Intentionally defer parsing of the expression, macro expansion is first
// done on this string, then the expanded expression lexeme is parsed again
/ token:IF expression:token_string? {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the first step in the core change - defer expression parsing until later - treat it as a string for now

);

const expanded = expandMacros(defined, macros);
return evaluteExpression(expressionParser(expanded.trim()), macros);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the primary change - evaluate the expression string after the macros are expanded

)
.replace(/defined\s+([A-Za-z_][A-Za-z_0-9]*)/g, (_, id) =>
id in macros ? '1' : '0'
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

AI wrote this code. It works. It would be nice if there weren't a new mini grammar nested in here. Another way to do this is to re-generate the expression into a string then re-parse it, to avoid having to evaluate defines this way. But this still works for now.

@AndrewRayCode AndrewRayCode merged commit 6ca6d48 into main Mar 6, 2026
1 check passed
@AndrewRayCode AndrewRayCode deleted the token-evaluation branch March 6, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants