Skip to content

Instantly share code, notes, and snippets.

@jimblandy
Last active February 13, 2025 00:10
Show Gist options
  • Save jimblandy/dbe44fd4133057ab760a74cf1944790e to your computer and use it in GitHub Desktop.
Save jimblandy/dbe44fd4133057ab760a74cf1944790e to your computer and use it in GitHub Desktop.
ChatGPT's advice on Naga IR design

I work on a Rust crate called Naga that translates GPU shaders from one language (say, the WebGPU shading language, also called WGSL) to another (say, Vulkan's SPIR-V). Naga is divided into front ends that parse shader modules into Naga's own IR, a middle end that does validation and analysis in language-independent code, and back ends that generate shader modules from IR.

At first, we had a principle that the IR should only represent concepts needed to preserve the original shader code's meaning, and concerns that were specific to some front end should be isolated in that front end. For example, WGSL promises that expressions containing only compile-time constants are fully evaluated at compile time, so that users can write general expressions for array sizes, alignment attributes on structure members, and other values that need to be known at compile time. At first, Naga's IR did not permit general expressions to occur in such places, requiring the front end to determine their final value itself before building IR. This made validation simpler, and the back ends could be somewhat simpler because they didn't need to consider general expressions in those places.

But now we feel this might be false economy. By forcing the front end to fully evaluate constant expressions, we have effectively forced constant evaluation to run before the middle end validation. It would be much nicer if constant evaluation could assume that all the expressions it needed to evaluation had already been type checked, and the only work needed was to determine their values.

But allowing constant evaluation to run after validation would mean that the IR would need to be flexible enough to permit general expressions in places like array lengths and alignment attributes. Validation would type check them, constant evaluation could assume expressions were fully type-checked and replace them with simplified values, and then back ends could assume that, while these places did contain general expressions, that they were always fully simplified.

So this seems like a case where a reasonable requirement - isolate language-specific features to front ends, let the IR be responsible only for conveying the semantics necessary for the back ends to generate correct code - actually resulted in a poor design.

Are there any general principles of IR design that we could have considered in advance to avoid making this mistake?

Yes, there are some general principles of intermediate representation (IR) design that could have helped anticipate this issue. Here are a few key ideas that might be useful for thinking through IR design trade-offs:


1. The Phase Order Problem in Compiler Design

Your issue is a classic example of "phase ordering" in compilers: the constraint that constant evaluation must happen before validation means that you don’t get the benefits of validated inputs when evaluating constants. A better IR design might have avoided forcing this ordering.

Principle: IR should allow reordering of independent passes

  • If possible, the IR should not overly constrain the order in which analyses and transformations must happen.
  • Allowing constant expressions in more places enables validation before evaluation, which leads to a more modular and reliable compiler structure.

2. The "High-Level Enough" Principle

Your initial design was based on the idea that the IR should capture only the essential semantics for code generation while leaving language-specific features in the front end. However, this approach ignored a key counterpoint:

Principle: An IR should be high-level enough to capture necessary program structure for all later stages

  • If a concept (like compile-time expressions) is meaningful and common across multiple front ends, it often belongs in the IR.
  • The fact that multiple shading languages (WGSL, GLSL, HLSL) all allow constant expressions in places like array sizes suggests that this isn't just a WGSL-specific quirk, but a general need.
  • By forcing lowering too early, the IR design made validation and later passes more difficult than necessary.

This is a common pitfall: overly desugaring the IR too soon can prematurely remove structure that would have been useful for later optimizations and analysis.


3. "Don’t Let Back Ends Dictate IR Structure"

Your original idea—that the IR should make life easier for back ends—seems reasonable, but it inadvertently caused trouble in earlier phases. This aligns with a common IR design mistake:

Principle: IR should serve analysis and transformation first, and code generation second

  • It’s tempting to design an IR around how easy it is to generate code from it.
  • But a good IR is primarily for analysis and optimization—back ends can deal with lowering later.
  • If simplifying the IR for back ends makes validation and transformations harder, it’s probably the wrong trade-off.

4. Flexibility in IR Representations

A key mistake was assuming that an IR should avoid constructs just because they are front-end specific. However, an IR often needs to be flexible enough to express incomplete or partially resolved states.

Principle: IR should support progressive lowering rather than forcing early desugaring

  • Allowing compile-time expressions in more places lets you separate validation from evaluation.
  • The IR should allow some expressions to remain unevaluated until later passes, rather than forcing front ends to collapse them too early.
  • Many compilers use deferred constant folding, where constants are only fully resolved when needed.

5. "Lowering Happens, But Not Too Early"

A common theme in IR design is the idea that IR should support multiple stages of transformation without forcing early decisions.

Principle: Keep IR expressive enough to allow deferred decisions

  • Instead of forcing front ends to resolve constants, let constant expressions flow through validation first.
  • Constant evaluation can then work on already-validated input, ensuring correctness and simplifying its logic.

This is similar to how LLVM IR allows certain high-level constructs (like getelementptr expressions) to remain in the IR rather than eagerly flattening them.


Summary: What You Could Have Considered

  1. Reorderability: If a pass depends on another for correctness, your IR should enable reordering.
  2. High-Level Enough: If multiple languages share a concept, it's not "just a front-end concern."
  3. IR First, Codegen Second: Making life easy for back ends shouldn't make earlier passes harder.
  4. Progressive Lowering: Don't collapse structure too soon—preserve high-level meaning where useful.
  5. Deferred Decisions: Let transformations happen when they have enough information, rather than forcing early resolution.

Would it make sense to adjust Naga's IR to allow general expressions in these cases, relying on validation to guarantee they are safe? That seems like a natural fix that aligns with these principles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment