Looking for Suggestions on Using Slang (Source Code or IR) as Backend for Dotnet CIL Frontend #8609
Replies: 4 comments
-
|
Thanks for sharing about your project; it sounds interesting! A lot of the challenges you are running into are very familiar to me, and you'd probably find people with similar experiences who have worked on RustGPU, the Of course, there's a lot that can be done in a system that does a best-effort translation, and I'll try to give responses to your specific questions: Q1: IR-to-IR Translation?Short answer: emitting Slang IR from your code generator rather than Slang source code would be more direct, but it is difficult to say with certainty whether it would be a more robust approach. Potential benefits include:
Likely downsides include:
Q2: Control Flow TranslationI believe that the kind of situation you are describing can be handled by inserting loops and using labelled To render your example in that style: I believe that Slang does support labelled I believe that using labelled Q3: Pointer EliminationThe short answer: Slang does not include the kind of complicated optimizations that would be necessary to fully eliminate pointer use from code written to use pointers. Related to the comments I made at the start of this post: part of our reason for building the Slang compiler the way we did was specifically to avoid needing to write complicated pointer-elimination and control-flow-restructuring passes. For targets that don't have "full" pointer support, the Slang IR and the Slang language are carefully designed so that we can always compile to those targets without needing to emit code with pointer use. The desire to robustly support targets without full pointer support is part of the motivation (but not the only motivation) for why the Slang front-end language disallows using If you are emitting Slang IR directly, and want to try to implement a pointer-elimination pass in the context of the Slang codebase, that might be one way to approach things. Internal to the Slang IR, things like local variables do have addresses, and function parameters of pointer type can be expressed at the IR level. As you say, many of the transformations you might like to make to eliminate pointers should be reasonably easy to implement (and some might just fall out of the process of emitting Slang IR). The challenge would be making such a pass robust enough to cover all the cases you care about. The main alternative to doing the pointer-elimination work as a pass in the Slang compiler codebase would be to do it in your own code at the CIL/C# level. Depending on what kind of IR you have built to encode the CIL, you might find it easier to implement the optimizations in your own code than to try and get Slang to do them for you. Q4: Merge/Join DefinitionShort answer: always using the immediate post-dominator as the merge point would lead to Slang IR that is "valid" in the sense that it obeys the invariants that the IR requires. Whether that choice is "valid" or not for the semantics of a particular input program is ultimately up to your programming model and what guarantees you want to provide to users. There's a ton of subtlety to all of this. If you don't know the relevant details on why ILs and IRs for GPU programming models often care about merge points, you might like to read this blog post that I wrote years and years ago. For a lot of code merge points can be seen as just an optimization. Merge points start to be semantically significant as soon as you have operations that involve cooperation between threads/invocations: barriers, thread-/work-group-shared memory, warp/wave/subgroup operations, etc. Note that for fragment shaders, even basic things like texture sampling counts as a cooperative operation (because of the implicit computation of partial differences over quad fragments). As soon as code performs any cooperative operations, the choice of where merge points are placed can (in principle) have semantic consequences, such that places them "too late" or "too early" might result in code computing the wrong values or, much worse, going into an infinite loop or other pathological behavior. I'm bringing some of this up because it is extremely relevant to you for a project like you are doing, because where your translator ends up placing merge points (via its translation to Slang, SPIR-V, Slang IR, WGSL, or what-have-you) will impact the semantics of the programming model that you expose to anybody using your system. Unfortunately, that means that the decisions made in the front-end compiler (for C# or whatever) that generates CIL, as well as your control-flow-restructuring pass will also have an impact on what a user's programs mean. The concrete example I always use (which is shown in the blog post I linked), is code that tries to implement something like mutual exclusion via atomics. Consider pseudo-code like the following: The intention of pseduo-code like that is clear:
The problem is that the The user-perceived semantics of this latter chunk of code is quite different from what they assumed the earlier example meant:
(Note that I'm not saying either of these semantic interpretations is definitely going to happen for that kind of input code across all languages, APIs, and hardware...) To a programmer's intuition, there is something semantically different between the two cases, because in one case the The ecosystem that Slang, GLSL, SPIR-V, and WGSL sit in recognizes that the distinction there matters. High-level languages like Slang and GLSL define their semantics in terms of the shape and nesting structure of the AST (see how GLSL defines "uniform control flow"), and an intermediate language like SPIR-V provides a way to encode those semantics as part of a CFG (via merge points). The dxc compiler for HLSL has had modifications made in the past to introduce fake You are ultimately the one who gets to decide what the semantics of your programming model will be. My strongly held opinion (based on my own experience in this domain) is that the a compilation stack should either provide reconvergence/join/merge behavior based on the shape of the input AST in the source language (which tends to match what programmers expect/want the behavior to be), or they should provide maximal reconvergence (which I guess you can request via I haven't worked on the theory of this stuff for a while, but I seem to recall that for structured input code, the "programmer intuition" model and maximal reconvergence are the same, so it is probably okay to treat maximal reconvergence as the correct generalization to arbitrary CFGs. Just note that neither of the two "good" definitions of reconvergence/merge behavior is defined in terms of immediate post-dominators. |
Beta Was this translation helpful? Give feedback.
-
|
Random: looking at the abstract for the paper you linked, I'm very pleased to see it reference the 1973 work from Peterson, Kasami, and Tokura. It is unfortunate that so many people only credit Relooper as the origination of the idea of restructuring an arbitrary CFG, when a working algorithm had been published many decades prior. |
Beta Was this translation helpful? Give feedback.
-
|
Hi @tangent-vector , Thank you so much for your clear and detailed response—it's been incredibly helpful and has sparked several new ideas that I'm excited to explore. These approaches may help address some of the challenges I mentioned earlier. Q2: Control Flow TranslationYour explanation on using labelled break to encode control flow code generation really opened my eyes to new possibilities. I previously thought labeled breaks were primarily a convenience feature for exiting nested loops, but your example demonstrates they're much more powerful than that. The pattern you showed bears a striking resemblance to WebAssembly's break-to-outer-scopes mechanism, which aligns well with my current IR design. I'm now investigating whether labeled breaks can be used to emulate WASM-style This could be encoded using only This raises two questions I'd appreciate your thoughts on:
Q3: Pointer Handling and LegalizationThank you for highlighting that the key challenge is determining whether optimization passes alone are sufficient for legalization in all cases. It definitely sounds unlikely, so your insight has prompted me to consider a more robust alternative approach. Since my requirements are limited to supporting the logical memory model with pointers only from parameters and local variables (a compile-time fixed set), I'm exploring an enum-based encoding scheme. The idea would be:
thus for a function it seems it could be encoded as This is conceptually similar to how interface-based runtime polymorphism can be compiled down into tagged unions. I'm planning to prototype this to see if it actually works. If both of these explorations above yield positive results, it seems current most challenging aspects of source code emission could be resolved. This might make it worthwhile to invest more time in the source-to-source compilation approach before pivoting to IR emission. Q4: Merge Points and Control Flow RecoveryThe concept of different semantics of merge points for GPU programs was new to me—thank you for the thorough explanation and references! Your example helped me realize I may have already encountered a related phenomenon. In some cases where source code has a loop followed by a return statement, my current control flow recovery implementation produces a structure that initially surprised me: it places the post-loop block inside the loop body itself. Original source pattern: Recovered structure: While this looks unusual, it's semantically equivalent from a standard CPU program perspective. What's particularly interesting is that some emissions of this pattern pass the Tint WGSL compiler but fail in Naga, with Naga complaining about the missing return statement in the function. After reading your explanation, I realize this is somewhat the inverse of your example. I initially thought this might be a compiler bug, but your insights suggest there's something deeper here worth understanding. I'm going to diving deeper into this area! Thank you again for taking the time to provide such thoughtful and educational responses. Your help is greatly appreciated! |
Beta Was this translation helpful? Give feedback.
-
|
I'm glad that you found my reply helpful. I certainly enjoy talking about this kind of stuff. Q2: Control Flow Translation2.1: Multi-Level
|
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hello Slang Team,
I'd like to begin by expressing my sincere gratitude for the Slang project, which has been of significant help and brings new possibility to my project - a compiler(transpiler) prototype that transpiles C# to shader languages, or more specifically from dotnet CIL to WGSL. My initial approach was an ad-hoc emitter for WGSL, but I recently found Slang. Its rich language features, reflection capabilities, webgpu/wasm support (and hopefully future CPU SIMD target) are exactly my original motivations for developing such an dotnet CIL to (graphics) shader transpiler.
And fortunately, it provides COM-like API, which could significantly simplify .NET integration.
After a recent refactor, I have a minimal proof-of-concept that emits Slang source code and uses
slangcCLI to generate WGSL. it works for some simple "shader toy"-style programs.While my knowledge of Slang's internals is still very limited, I'm encountering some challenges that lead me to consider that emitting Slang's IR directly might be a more robust/longer term easier approach than generating Slang source code. I would be very grateful for your thoughts and suggestions on this idea.
Here are my primary motivations and questions:
Question 1: Simpler IR-to-IR Transformation
My compiler's frontend is dotnet CIL, which is a linear, stack-based IR. I perform custom analysis to transform it into an SSA-based IR, using block arguments instead of phi-nodes, like Slang IR.
I've implemented a simple version of recover structural control flow algorithm which is somehow related to Relooper based on this papar, to recover structural control flow from control flow graph. The structural control flow is in the sense similar to WASM, thus only allowing jump to proper nest regions, not arbitrary jump.
This is still not same meaning of structured control flow in Slang, but with some post dominance analysis its already close.
However, to emit slang source code from my "structural control flow IR", there is still a recovery process, which involves identifying
continueandbreakstatements from block branches, adds some seemly unnecessary complexity. Thus currently my pipeline is like from SSA IR -> Slang source code -> Slang SSA IR, which seems source code recovery is redundant and brings more challenges.Question: Would emitting Slang IR directly from my SSA-form IR be a more direct and robust approach, bypassing the need for Slang source code recovery?
Before emitting Slang source code, I also tried to emit SPIRV, but I found that although SPIRV is a SSA like linear IR with detailed specification, which seems would be easier to emit from my linear SSA IR, it is actually harder to emit at least for simple shaders as there is much more details to handle, e.g. finding and mapping to right instruction set (with GLSL extension etc), properly handing memory space etc.
Since slang itself has higher level abstraction, I'm assuming emit Slang IR would be easier than SPIRV, is that correct? One concern is that Slang source code is easier to understand and with more documentation, I'm not sure if emitting Slang IR would be suitable to be used as a backend.
Question 2: Handling Control Flow With Reused Blocks
Certain code patterns, like nested ternary expressions, result in a Control Flow Graph (CFG) that doesn't map cleanly to a simple AST structure. For example, consider this C# code:
The corresponding CFG would look something like this:
To represent this in a structured way, the
b3block (theelsecase) would need to be duplicated in the AST, which is not ideal:While a
selectinstruction could handle this specific case, it's not a general solution.Question: Does the Slang IR accept these kinds of CFGs (need to reuse/duplicate block in structural control flow)?
Question 3: Eliminating Unnecessary Pointers
dotnet CIL allows taking the address of arguments and local variables. A direct, 1:1 translation of this logic into Slang source code can produce invalid constructs, particularly for graphics shader targets.
Consider this example:
My SSA IR for this function might look like this, where pointers to
pare passed between blocks:Those passing of pointer for argument
pcomes from the dotnet stack IR passing values on stack between blocks.If I directly emit this IR into Slang source code, it might produce something like following, which is invalid for graphics shader target because it takes the address of a function parameter:
In this case, the pointer
ptris actually unnecessary and could be optimized away by a constant propagation pass, as it always points top, it's just a artifact from stack IR passing value across blocks for reduced instruction count.Question: Is it feasible to emit Slang IR containing such pointer patterns and rely on subsequent legalization or optimization passes within Slang to eliminate them?
Question 4: Definition of Merge/Join Nodes in Slang IR
I've read in the Design of Slang's IR post that a "join" node in Slang's CFG is not strictly required to be a post-dominator. In my current AST recovery logic, I use the immediate post-dominator of a branch as the merge point for blocks/if-else, and use the first post dominator outside loop region for loops, which works for my limited simple test cases, but I am not sure if it's a valid assumption for all cases.
Question: Although a "join" node is not strictly required to be a post-dominator, if I always use the immediate post-dominator as the merge block when constructing Slang IR, will that always result in a valid IR?
Thank you for your time for reading this, looking forward to your guidance and suggestions.
Beta Was this translation helpful? Give feedback.
All reactions