From 0974463daf0982626cb2b8c8bb6f494f3e6c9e52 Mon Sep 17 00:00:00 2001 From: Anders Leino Date: Mon, 10 Jun 2024 15:15:02 +0300 Subject: Fix typos in the docs (#4322) --- docs/design/capabilities.md | 16 ++++++++-------- docs/design/casting.md | 2 +- docs/design/coding-conventions.md | 2 +- docs/design/decl-refs.md | 4 ++-- docs/design/existential-types.md | 18 +++++++++--------- docs/design/interfaces.md | 2 +- docs/design/ir.md | 20 ++++++++++---------- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/design/capabilities.md b/docs/design/capabilities.md index 89fef3091..74c08fca9 100644 --- a/docs/design/capabilities.md +++ b/docs/design/capabilities.md @@ -77,7 +77,7 @@ capability opengl : khronos; Here we are saying that `sm_5_1` supports everything `sm_5_0` supports, and potentially more. We are saying that `d3d12` supports `sm_6_0` but maybe not, e.g., `sm_6_3`. We are expressing that fact that having a `glsl_*` capability means you are on some Khronos API target, but that it doesn't specify which one. -(The extact details of these declarations obviously aren't the point; getting a good hierarchy of capabilites will take time) +(The extact details of these declarations obviously aren't the point; getting a good hierarchy of capabilites will take time.) Capability Composition ---------------------- @@ -119,7 +119,7 @@ void myFunc(); ``` This function should be equivalent to one with just a single `[availableFor((vulkan & fragment) | (d3d12 & fragment))]` which is equivalent to `[availableFor((vulkan | d3d12) & fragment)]`. -Simplification should generally push toward "disjunctive normal form," though, rather than puruse simplifications like that. +Simplification should generally push toward "disjunctive normal form," though, rather than pursue simplifications like that. Note that we do *not* include negation, so that capabilities are not general Boolean expressions. Validation @@ -130,7 +130,7 @@ For a given function definition `F`, the front end will scan its body and see wh If `F` doesn't have an `[availableFor(...)]` attribute, then we can derive its *effective* `[availableFor(...)]` capability as `R` (this probably needs to be expressed as an iterative dataflow problem over the call graph, to handle cycles). If `F` *does* have one or more `[availabelFor(...)]` clauses that amount to a declared capability `C` (again in disjunctive normal form), then we can check that `C` implies `R` and error out if it is not the case. -A reasonable implementation would track which calls introduced with requirements, and be able to explain *why* `C` does not capture the stated requirements. +A reasonable implementation would track which calls introduced which requirements, and be able to explain *why* `C` does not capture the stated requirements. For a shader entry point, we should check it as if it had an `[availableFor(...)]` that is the OR of all the specified target profiles (e.g., `sm_5_0 | glsl_450 | ...`) ANDed with the specified stage (e.g., `fragment`). Any error here should be reported to the user. @@ -151,7 +151,7 @@ It should be possible to define multiple versions of a function, having differen [availableFor(d3d12)] void myFunc() { ... } ``` -For front-end checking, these should be treated as if they were a single definition of `myFunc` with a ORed capability (e.g., `vulkan | d3d12`). +For front-end checking, these should be treated as if they were a single definition of `myFunc` with an ORed capability (e.g., `vulkan | d3d12`). Overload resoultion will pick the "best" candidate at a call site based *only* on the signatures of the function (note that this differs greatly from how profile-specific function overloading works in Cg). The front-end will then generate initial IR code for each definition of `myFunc`. @@ -177,7 +177,7 @@ So far I've talked about capabilities on functions, but they should also be allo We should also provide a way to specify that a `register` or other layout modifier is only applicable for specific targets/stages. Such a capability nominally exists in HLSL today, but it would be much more useful if it could be applied to specify target-API-specific bindings. -Only functions should support overloading based on capability. in all other cases there can only be one definition of an entity, and capabilities just decide when it is available. +Only functions should support overloading based on capability. In all other cases there can only be one definition of an entity, and capabilities just decide when it is available. API Extensions as Capabilities ------------------------------ @@ -192,14 +192,14 @@ capability KHR_secret_sauce : vulkan; void improveShadows(); ``` -When generating code for Vulkan, we should be able to tell the user that the `improveShadows()` function requires the given extension. The user should be able to expression compositions of capabilities in their `-profile` option (and similarly for the API): +When generating code for Vulkan, we should be able to tell the user that the `improveShadows()` function requires the given extension. The user should be able to express compositions of capabilities in their `-profile` option (and similarly for the API): ``` slangc code.slang -profile vulkan+KHR_secret_sauce ``` (Note that for the command line, it is beneficial to use `+` instead of `&` to avoid conflicts with shell interpreters) -And important question is whether the compiler should automatically infer required extensions without them being specified, so that it produces SPIR-V that requires extensions the user didn't ask for. +An important question is whether the compiler should automatically infer required extensions without them being specified, so that it produces SPIR-V that requires extensions the user didn't ask for. The argument against such inference is that users should opt in to non-standard capabilities they are using, but it would be unfortunate if this in turn requires verbose command lines when invoking the compiler. It should be possible to indicate the capabilities that a module or entry point should be compiled to use without command-line complications. @@ -268,4 +268,4 @@ Conclusion ---------- Overall, the hope is that in many cases developers will be able to use capability-based partitioning and overloading of APIs to build code that only has to pass through the Slang front-end once, but that can then go through back-end code generation for each target. -In cases where this can't be achieved, the way that capability-based overloading is built into the Slang ir design means that we should be able to merge multiple target-specific definitions into one IR module, so that a library can employ target-specific specializations while still presenting a single API to consumers. +In cases where this can't be achieved, the way that capability-based overloading is built into the Slang IR design means that we should be able to merge multiple target-specific definitions into one IR module, so that a library can employ target-specific specializations while still presenting a single API to consumers. diff --git a/docs/design/casting.md b/docs/design/casting.md index 6c4b119eb..80c1f149f 100644 --- a/docs/design/casting.md +++ b/docs/design/casting.md @@ -25,7 +25,7 @@ These functions will also work with types that do not have Vtbl - like IRInst de Both 'as' and 'dynamicCast' handle the case if the pointer is a nullptr, by returning a nullptr. If the cast succeeds the cast pointer is returned otherwise nullptr is returned. If a cast is performed with a free function it always returns a raw pointer. -So why have 'as' and 'dynamicCast' - they seem sort of similar? The primary difference is dynamicCast *must* always return a pointer to the same object, whilst 'as' *can* return a pointer to a different object if that is the desired 'normal' casting behavior for the type. This is the case for Type* when using 'as' it may return a different object - the 'canonical type' for the Type*. For a concrete example take 'NamedExpressionType', it's canonical type is the type the name relates to. If you use 'as' on it - it will produce a pointer to a different object, an object that will not be castable back into a NamedExpressionType. +So why have 'as' and 'dynamicCast' - they seem sort of similar? The primary difference is dynamicCast *must* always return a pointer to the same object, whilst 'as' *can* return a pointer to a different object if that is the desired 'normal' casting behavior for the type. This is the case for Type* when using 'as' it may return a different object - the 'canonical type' for the Type*. For a concrete example take 'NamedExpressionType', its canonical type is the type the name relates to. If you use 'as' on it - it will produce a pointer to a different object, an object that will not be castable back into a NamedExpressionType. Also keep in mind that 'as' behavior is based on the pointer type being cast from. For any pointer to a type derived from Type it will cast the canonical type. **BUT** if the pointer is pointing to a Type derived *object*, but the pointer type is *not* derived from Type (like say RefObject*), then 'as' will behave like dynamicCast. diff --git a/docs/design/coding-conventions.md b/docs/design/coding-conventions.md index f730a25d5..95d2a42ae 100644 --- a/docs/design/coding-conventions.md +++ b/docs/design/coding-conventions.md @@ -31,7 +31,7 @@ As a general rule, be skeptical of "modern C++" ideas unless they are clearly be We are not quite in the realm of "Orthodox C++", but some of the same guidelines apply: * Don't use exceptions for non-fatal errors (and even then support a build flag to opt out of exceptions) -* Don't the built-in C++ RTTI system (home-grown is okay) +* Don't use the built-in C++ RTTI system (home-grown is okay) * Don't use the C++ variants of C headers (e.g., `` instead of ``) * Don't use the STL containers * Don't use iostreams diff --git a/docs/design/decl-refs.md b/docs/design/decl-refs.md index 8e863aa8c..34b74a6f4 100644 --- a/docs/design/decl-refs.md +++ b/docs/design/decl-refs.md @@ -34,7 +34,7 @@ Cell a = { 3 }; int b = a.value + 4; ``` -In this case, the expression node for `a.value` can directly reference the declaration of the field `Cell::value`, and from that we can conclude that the type of the field (and hence the expression) is `int. +In this case, the expression node for `a.value` can directly reference the declaration of the field `Cell::value`, and from that we can conclude that the type of the field (and hence the expression) is `int`. In contrast, things get more complicated as soon as we have a language with generics: @@ -158,7 +158,7 @@ There are many queries like "what is the return type of this function?" that typ The `syntax.h` file defines helpers for most of the existing declaration AST nodes for querying properties that should represent substitutions (the type of a variable, the return type of a function, etc.). If you are writing code that is working with a `DeclRef`, try to use these accessors and avoid being tempted to extract the bare declaration and start querying it. -Some things like `Modifier`s aren't (currently) affected by substitutions, so it can make sense to query them on a bare declaration instead of a `DeclRef. +Some things like `Modifier`s aren't (currently) affected by substitutions, so it can make sense to query them on a bare declaration instead of a `DeclRef`. Conclusion ---------- diff --git a/docs/design/existential-types.md b/docs/design/existential-types.md index e5bae0b7d..06e2613e3 100644 --- a/docs/design/existential-types.md +++ b/docs/design/existential-types.md @@ -83,7 +83,7 @@ A C++ class or COM component can implement an existential type, with the constra Many modern languages (e.g., Go) support adapting existing types to new interfaces, so that a "pointer" of interface type is actually a fat pointer: one for the object, and one for the interface dispatch table. Our examples so far have assumed that the type `T` needs to be passed around separately from the witness table `W`, but that isn't strictly required in some implementations. -In type theory, the most important operation you can do with an existential type is to "open" it, which means to have a limited scope in which you can refer to the constinuent pieces of a "bundled up" value of a type like `IImage`. +In type theory, the most important operation you can do with an existential type is to "open" it, which means to have a limited scope in which you can refer to the constituent pieces of a "bundled up" value of a type like `IImage`. We could imagine "opening" an existential as something like: ``` @@ -97,7 +97,7 @@ void myFunc(IImage img) // and `obj` is a value of type `T`. // doSomethingCool(obj); - } + } } ``` @@ -125,18 +125,18 @@ Knowing the implementation strategy outline above, we can re-phrase this questio For simple interfaces this is sometimes possible, but in the general case there are other desirable language features that get in the way: -* When an interface has associated types, there is no type that can be chosen as the associated type for the interface's existential type. The "obvious" approach of using the constraints on the associatd type can lead to unsound logic when interface methods take associated types as parameters. +* When an interface has associated types, there is no type that can be chosen as the associated type for the interface's existential type. The "obvious" approach of using the constraints on the associated type can lead to unsound logic when interface methods take associated types as parameters. -* When an interface uses the "this type" (e.g., an `IComparable` interface with a `compareTo(ThisType other)` method), it isn't correct to simplify the this type to the interface type (just because you have to `IComarable` values doesn't mean you can compare them - they have to be of the same concrete type!) +* When an interface uses the "this type" (e.g., an `IComparable` interface with a `compareTo(ThisType other)` method), it isn't correct to simplify the this type to the interface type (just because you have two `IComarable` values doesn't mean you can compare them - they have to be of the same concrete type!) -* If we allow for `static` method on interfaces, then what implementation would we use for these methods on the interface existential type? +* If we allow for `static` method on interfaces, then what implementation would we use for these methods on the interface's existential type? Encoding Existentials in the IR ------------------------------- Existentials are encoded in the Slang IR quite simply. We have an operation `makeExistential(T, obj, W)` that takes a type `T`, a value `obj` that must have type `T`, and a witness table `W` that shows how `T` conforms to some interface `I`. The result of the `makeExistential` operation is then a value of the type `I`. -Rather than include an IR operation to "open" an existential, we can instead just provide accessors for the pieces of information in an existential: one to extract the type field, one to extract the value, and one to extract the witness table. These would idomatically be used like: +Rather than include an IR operation to "open" an existential, we can instead just provide accessors for the pieces of information in an existential: one to extract the type field, one to extract the value, and one to extract the witness table. These would idiomatically be used like: ``` let e : ISomeInterface = /* some existential */ @@ -172,7 +172,7 @@ We require further transformation passes to allow specialization in more general * Function specialization, is needed so that a function with existential parameters is specialized based on the actual types used at call sites Transformations just like these are already required when working with resource types (textures/samplers) on targets that don't support first-class computation on resources, so it is possible to share some of the same logic. -Similarly, any effort we put into validation (to ensure that code is written in a way that *can* be simplified) can hopefully be shared between existentials and reources. +Similarly, any effort we put into validation (to ensure that code is written in a way that *can* be simplified) can hopefully be shared between existentials and resources. Compositions ------------ @@ -186,7 +186,7 @@ The hardest part of supporting composition of interfaces is actually in how to l Why are we passing along the type? ---------------------------------- -I'm glossing over something pretty significant here, which is why anybody would pass around the type as part of the existential value, when none of our examples so far have made us of it. +I'm glossing over something pretty significant here, which is why anybody would pass around the type as part of the existential value, when none of our examples so far have made use of it. This sort of thing isn't very important for languages where interface polymorphism is limited to heap-allocated "reference" types (or values that have been "boxed" into reference types), because the dynamic type of an object can almost always be read out of the object itself. When dealing with a value type, though, we have to deal with things like making *copies*: @@ -235,7 +235,7 @@ This is the reason for passing through the type `T` as part of an existential va If we only wanted to deal with reference types, this would all be greatly simplified, because the `sizeInBytes` and the copy/move semantics would be fixed: everything is a single pointer. -All of the same issues arise if we making copies of existential values: +All of the same issues arise if we're making copies of existential values: ``` IWritable copyAndClobberExistential(IWritable obj) diff --git a/docs/design/interfaces.md b/docs/design/interfaces.md index c60b01009..ce6e0be28 100644 --- a/docs/design/interfaces.md +++ b/docs/design/interfaces.md @@ -226,7 +226,7 @@ Bot the use of `&` and `where` are advanced features that we might cut due to im ### Value Parameters -Because HLSL has generics like `vector` that already take non-type parameters, the language will need *some* degree of support for generic parameters that arent' types (at least integers need to be supported). +Because HLSL has generics like `vector` that already take non-type parameters, the language will need *some* degree of support for generic parameters that aren't types (at least integers need to be supported). We need syntax for this that doesn't bloat the common case. In this case, I think that what I've used in the current Slang implementation is reasonable, where a value parameter needs a `let` prefix: diff --git a/docs/design/ir.md b/docs/design/ir.md index 97c13c6ee..c7f4ffeb2 100644 --- a/docs/design/ir.md +++ b/docs/design/ir.md @@ -64,7 +64,7 @@ The Slang IR strives for an extremely high degree of uniformity, so almost every This uniformity greatly simplifies the task of supporting generics, and also means that operations that need to work over all instructions, such as cloning and serialization, can work with a single uniform representation and avoid special-casing particular opcodes. -The decision to use an extremely uniform deisgn, even going as far to treat types as "ordinary" instructions, is similar to SPIR-V, although we do not enforce many of the constraints SPIR-V does on how type and value instructions can be mixed. +The decision to use an extremely uniform design, even going as far to treat types as "ordinary" instructions, is similar to SPIR-V, although we do not enforce many of the constraints SPIR-V does on how type and value instructions can be mixed. ### Instructions Have a Uniform Structure @@ -95,14 +95,14 @@ There are some subtleties to how the opcodes are ordered to deal with the fact t ### A Simpler Encoding of SSA -The traditional encoding of SSA form involves placing "phi" instructions at the start of blocks that represent control-flow join points where a variable will take on different values depend on the incoming edge that is taken. +The traditional encoding of SSA form involves placing "phi" instructions at the start of blocks that represent control-flow join points where a variable will take on different values depending on the incoming edge that is taken. There are of course benefits to sticking with tradition, but phi instructions also have a few downsides: - The operands to phi instructions are the one case where the "def dominates use" constraint of SSA appears to be violated. I say "appears" because officially the action of a phi occurs on the incoming edge (not in the target block) and that edge will of course be dominated by the predecessor block. It still creates a special case that programmers need to be careful about. This also complicates serialization in that there is no order in which the blocks/instructions of a function can be emitted that guarantees that every instruction always precedes all of its uses in the stream. - All of the phi instructions at the start of the block must effectively operate in parallel, so that they all "read" from the correct operand before "writing" to the target variable. Like the above special case, this is only a problem for a phi related to a loop back-edge. It is of course possible to always remember the special interpretation of phi instructions (that they don't actually execute sequentially like every other instruction in a block), but its another special case. -- The order of operands to a phi instruction needs to be related back to the predecessor blocks, so that one can determine which value is to be used for which incoming edge. Any transformation that modifies the CFG of a function needs to be careful to rewrite phi instructions to match the order in which predecessors are list, or else the compiler must maintain a side data structure that remembers the mapping (and update it instead). +- The order of operands to a phi instruction needs to be related back to the predecessor blocks, so that one can determine which value is to be used for which incoming edge. Any transformation that modifies the CFG of a function needs to be careful to rewrite phi instructions to match the order in which predecessors are listed, or else the compiler must maintain a side data structure that remembers the mapping (and update it instead). - Directly interpreting/executing code in an SSA IR with phi instructions is made more difficult because when branching to a block we need to immediately execute any phi instructions based on the block from which we just came. The above issues around phis needing to be executed in parallel, and needing to track how phi operands relate to predecessor blocks also add complexity to an interpreter. @@ -115,13 +115,13 @@ The first N instructions in a Slang basic block are its parameters, each of whic A block that would have had N phi instrutions now has N parameters, but the parameters do not have operands. Instead, a branch instruction that targets that block will have N *arguments* to match the parameters, representing the values to be assigned to the parameters when this control-flow edge is taken. -This encoding is equivalent in what it represents to traditional phi instructions, but nicely solves the problems outlines above: +This encoding is equivalent in what it represents to traditional phi instructions, but nicely solves the problems outlined above: - The phi operands in the successor block are now arguments in the *predecessor* block, so that the "def dominates use" property can be enforced without any special cases. - The "assignment" of the argument values to parameters is now encoded with a single instruction, so that the simultaneity of all the assignments is more clear. We still need to be careful when leaving SSA form to obey those semantics, but there are no tricky issues when looking at the IR itself. -- There is no special work required to track which phi operands come from which predecessor block, since the operands are attached to the terminator instruction of the predecessor block itself. There is no need to update phi instructions after a CFG change that might affect the predecessor list of a block. The trade-off is that any change the *number* of parameters of a block now requires changes to the terminator of each predecessor, but that is a less common change (isolated to passes that can introduce or eliminate block parameters/phis). +- There is no special work required to track which phi operands come from which predecessor block, since the operands are attached to the terminator instruction of the predecessor block itself. There is no need to update phi instructions after a CFG change that might affect the predecessor list of a block. The trade-off is that any change in the *number* of parameters of a block now requires changes to the terminator of each predecessor, but that is a less common change (isolated to passes that can introduce or eliminate block parameters/phis). - It it much more clear how to give an operational semantics to a "branch with arguments" instead of phi instructions: compute the target block, copy the argumenst to temporary storage (because of the simultaneity requirement), and then copy the temporaries over the parameters of the target block. @@ -178,7 +178,7 @@ D; Note that (unlike what some programming models would say) a join point is *not* necessarily a postdominator of the conditional branch. In the example above the block with `D` does not postdominate the block with `someCondition` nor the one with `B`. It is even possible to construct cases where the high-level join point of a control-flow construct is unreachable (e.g., the block after an infinite loop). The Slang IR encodes structured control flow by making the join point be an explicit operand of a structured conditional branch operation. Note that a join-point operand is *not* used when computing the successor list of a block, since it does not represent a control-flow edge. -This is slightly different from SPIR-V where joint points ("merge points" in SPIR-V) are encoded using a metadata instruction that precedes a branch. Keeping the information on the instruction itself avoids cases where we move one but not the other of the instructions, or where we might accidentally insert code between the metadata instruction and the terminator it modifies. +This is slightly different from SPIR-V where join points ("merge points" in SPIR-V) are encoded using a metadata instruction that precedes a branch. Keeping the information on the instruction itself avoids cases where we move one but not the other of the instructions, or where we might accidentally insert code between the metadata instruction and the terminator it modifies. In the future we might consider using a decoration to represent join points. When using a loop instruction, the join point is also the `break` label. The SPIR-V `OpLoopMerge` includes not only the join point (`break` target) but also a `continue` target. We do not currently represent structured information for `continue` blocks. @@ -188,16 +188,16 @@ The approach we use today means that the code in "continue clause" might end up When it comes time to re-form higher-level structured control flow from Slang IR, we use the structuring information in the IR to form single-entry "regions" of code that map to existing high-level control-flow constructs (things like `if` statements, loops, `break` or `continue` statements, etc.). The current approach we use requires the structuring information to be maintained by all IR transformations, and also currently relies on some invariants about what optimizations are allowed to do (e.g., we had better not introduce multi-level `break`s into the IR). -In the future, it would be good to investigate adapting the "Relooper" algorithm used in Emscripten so that we can reover valid structured control flow from an arbitrary CFG; for now we put off that work. +In the future, it would be good to investigate adapting the "Relooper" algorithm used in Emscripten so that we can recover valid structured control flow from an arbitrary CFG; for now we put off that work. If we had a more powerful restructuring algorithm at hand, we could start to support things like multi-level `break`, and also ensure that `continue` clauses don't lead to code duplication any more. ## IR Global and Hoistable Value Deduplication -Types, constants and certain operations on constants are considered "global value" in the Slang IR. Some other insts like `Specialize()` and `Ptr(x)` are considered as "hoistable" insts, in that they will be defined at the outer most scope where their operands are available. For example, `Ptr(int)` will always be defined at global scope(as direct children of `IRModuleInst`) because its only operand, `int`, is defined at global scope. However if we have `Ptr(T)` where `T` is a generic parameter, then this `Ptr(T)` inst will be always be defined in the block of the generic. Global and hoistable values are always deduplicated and we can always assume two hoistable values with different pointer addresses are distinct values. +Types, constants and certain operations on constants are considered "global value" in the Slang IR. Some other insts like `Specialize()` and `Ptr(x)` are considered as "hoistable" insts, in that they will be defined at the outer most scope where their operands are available. For example, `Ptr(int)` will always be defined at global scope (as direct children of `IRModuleInst`) because its only operand, `int`, is defined at global scope. However if we have `Ptr(T)` where `T` is a generic parameter, then this `Ptr(T)` inst will be always be defined in the block of the generic. Global and hoistable values are always deduplicated and we can always assume two hoistable values with different pointer addresses are distinct values. The `IRBuilder` class is responsible for ensuring the uniqueness of global/hoistable values. If you call any `IRBuilder` methods that creates a new hoistable instruction, e.g. `IRBuilder::createIntrinsicInst`, `IRBuilder::emitXXX` or `IRBuilder::getType`, `IRBuilder` will check if an equivalent value already exists, and if so it returns the existing inst instead of creating a new one. -The trickier part here is to always maintain the uniqueness when we modify the IR. When we update the operand of an inst from a non-hoistable-value to a hoistable-value, we may need to hoist `inst` itself as a result. For example, considered the following code: +The trickier part here is to always maintain the uniqueness when we modify the IR. When we update the operand of an inst from a non-hoistable-value to a hoistable-value, we may need to hoist `inst` itself as a result. For example, consider the following code: ``` %1 = IntType %p = Ptr(%1) @@ -264,7 +264,7 @@ auto ptr = builder.getPtrType(x); // create ptr(x). x->replaceUsesWith(intType); // this renders `ptr` obsolete!! auto var = builder.emitVar(ptr); // use the obsolete inst to create another inst. ``` -In this example, calling `replaceUsesWith` will cause `ptr` to represent `Ptr(int)`, which may already exist in the global scope. After this call, all uses of `ptr` should be replaced with the global `Ptr(int)` inst instead. `IRBuilder` has provided the mechanism to track all the insts that are removed due to deduplication, and map those removed but not yet deleted inst to the existing inst. When using `ptr` to create a new inst, `IRBuilder` will first check if `ptr` should map to some existing hoistable inst in the global deduplication map and replace it if possible. This means that after the call to `builder.emitVar`, `var->type` is not equal to to `ptr`. +In this example, calling `replaceUsesWith` will cause `ptr` to represent `Ptr(int)`, which may already exist in the global scope. After this call, all uses of `ptr` should be replaced with the global `Ptr(int)` inst instead. `IRBuilder` has provided the mechanism to track all the insts that are removed due to deduplication, and map those removed but not yet deleted insts to the existing inst. When using `ptr` to create a new inst, `IRBuilder` will first check if `ptr` should map to some existing hoistable inst in the global deduplication map and replace it if possible. This means that after the call to `builder.emitVar`, `var->type` is not equal to to `ptr`. ### Best Practices -- cgit v1.2.3