Skip to content

Commit

Permalink
Merge branch 'master' into arial/initializer-list
Browse files Browse the repository at this point in the history
  • Loading branch information
kaizhangNV committed Sep 18, 2024
2 parents c0e59e8 + 85b996a commit 2682128
Show file tree
Hide file tree
Showing 49 changed files with 3,242 additions and 495 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ vkd3d-proton.cache
vkd3d-proton.cache.write
*_d3d11.log
*_dxgi.log

# Vim temporary files
*~
.*.swp
.*.swo
191 changes: 191 additions & 0 deletions docs/proposals/002-type-equality-constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
Allow Type Equality Constraints on Generics
===========================================

We propose to allow *type equality* constraints in `where` clauses.

Status
------

In progress.

Background
----------

As of proposal [001](001-where-clauses.md), Slang allows for generic declarations to include a *`where` clause* which enumerates constraints on the generic parameters that must be satisfied by any arguments provided to that generic:

V findOrDefault<K, V>( HashTable<K,V> table, K key )
where K : IHashable,
V : IDefaultInitializable
{ ... }

Currently, the language only accepts *conformance* constraints of the form `T : IFoo`, where `T` is one of the parameters of the generic, and `IFoo` is either an `interface` or a conjunction of interfaces, which indicate that the type `T` must conform to `IFoo`.

This proposal is motivated by the observation that when an interface has associated types, there is currently no way for a programmer to introduce a generic that is only applicable when an associated type satisfies certain constriants.

As an example, consider an interface for types that can be "packed" into a smaller representation for in-memory storage (instead of a default representation optimized for access from registers):

interface IPackable
{
associatedtype Packed;

init(Packed packed);
Packed pack();
}

Next, consider an hypothetical interface for types that can be deserialized from a stream:

interface IDeserializable
{
init( InputStream stream );
}

Given these definitions, we might want to define a function that takes a packable type, and deserializes it from a stream:

T deserializePackable<T>( InputStream stream )
where T : IPackable
{
return T( T.Packed(stream) );
}

As written, this function will fail to compile because the compiler cannot assume that `T.Packed` conforms to `IDeserializable`, in order to support initialization from a stream.

A brute-force solution would be to add the `IDeserializable` constraint to the `IPackable.Packed` associated type, but doing so may not be consistent with the vision the designer of `IPackable` had in mind. Indeed, there is no reason to assume that `IPackable` and `IDeserializable` even have the same author, or are things that the programmer trying to write `deserializePackable` can change.

It might seem that we could improve the situation by introducing another generic type parameter, so that we can explicitly constraint it to be deserializable:

T deserializePackable<T, U>( InputStream stream )
where T : IPackable,
P : IDeserializable
{
return T( U(stream) );
}

This second attempt *also* fails to compile.
In this case, there is no way for the compiler to know that `T` can be initialized from a `P`, because it cannot intuit that `P` is meant to be `T.Packed`.

Our two failed attempts can each be fixed by introducing two new kinds of constraints:

* Conformance constraints on associated types: `T.A : IFoo`

* Equality constraints on associated types: `T.A == X`

Related Work
------------

Both Rust and Swift support additional kinds of constraints on generics, including the cases proposed here.
The syntax in those languages matches what we propose.

Proposed Approach
-----------------

In addition to conformance constraints on generic type parameters (`T : IFoo`), the compiler will also support constraints on associated types of those parameters (`T.A : IFoo`), and associated types of those associated types (`T.A.B : IFoo`), etc.

In addition, the compiler will accept constraints that restrict an associated type (`T.A`, `T.A.B`, etc.) to be equal to some other type.
The other type may be a concrete type, another generic parameter, or another associated type.

Detailed Explanation
--------------------

### Parser

The parser already supports nearly arbitrary type exprssions on both sides of a conformance constraint, and then validates that the types used are allowed during semantic checking.
The only change needed at that level is to split `GenericTypeConstraintDecl` into two cases: one for conformance constraints, and another for equality constraints, and then to support constraints with `==` instead of `:`.

### Semantic Checking

During semantic checking, instead of checking that the left-hand type in a constraint is always one of the generic type parameters, we could instead check that the left-hand type expression is either a generic type parameter or `X.AssociatedType` where `X` would be a valid left-hand type.

The right-hand type for conformance constraints should be checked the same as before.

The right-hand type for an equality constraint should be allowed to be an arbitrary type expression that names a proper (and non-`interface`) type.

One subtlety is that in a type expression like `T.A.B` where both `A` and `B` are associated types, it may be that the `B` member of `T.A` can only be looked up because of another constraint like `T.A : IFoo`.
When performing semantic checking of a constraint in a `where` clause, we need to decide which of the constraints may inform lookup when resolving a type expression like `X.A`.
Some options are:

* We could consider only constraints that appear before the constraint that includes that type expression. In this case, a programmer must always introduce a constraint `X : IFoo` before a constraint that names `X.A`, if `A` is an associated type introduced by `IFoo`.

* We could consider *all* of the constraints simultaneously (except, perhaps, the constraint that we are in the middle of checking).

The latter option is more flexible, but may be (much) harder to implement in practice.
We propose that for now we use for first option, but remain open to implementing the more general case in the future.

Given an equality constraint like `T.A.B == X`, semantic checking needs detect cases where an `X` is used and a `T.A.B` is expected, or vice versa.
These cases should introduce some kind of cast-like expression, which references the type equality witness as evidence that the cast is valid (and should, in theory, be a no-op).

Semantic checking of equality constraints should identify contradictory sets of constraints.
Such contradictions can be simple to spot:

interface IThing { associatedtype A; }
void f<T>()
where T : IThing,
T.A == String,
T.A == Float,
{ ... }

but they can also be more complicated:

void f<T,U>()
where T : IThing,
U : IThing,
T.A == String,
U.A == Float,
T.A == U.A
{ ... }

In each case, an associated type is being constrained to be equal to two *different* concrete types.
The is no possible set of generic arguments that could satisfy these constraints, so declarations like these should be rejected.

We propose that the simplest way to identify and diagnose contradictory constraints like this is during canonicalization, as described below.

### IR

At the IR level, a conformance constraint on an associated type is no different than any other conformance constraint: it lowers to an explicit generic parameter that will accept a witness table as an argument.

The choice of how to represent equality constraints is more subtle.
One option is to lower an equality constraint to *nothing* at the IR level, under the assumption that the casts that reference these constraints should lower to nothing.
Doing so would introduce yet another case where the IR we generate doesn't "type-check."
The other option is to lower a type equality constraint to an explicit generic parameter which is then applied via an explicit op to convert between the associated type and its known concrete equivalent.
The representation of the witnesses required to provide *arguments* for such parameters is something that hasn't been fully explored, so for now we prpose to take the first (easier) option.

### Canonicalization

Adding new kinds of constraints affects *canonicalization*, which was discussed in proposal 0001.
Conformane constraints involving associated types should already be order-able according to the rules in that proposal, so we primarily need to concern ourselves with equality constraints.

We propose the following approach:

* Take all of the equality constraints that arise after any expansion steps
* Divide the types named on either side of any equality constraint into *equivalence classes*, where if `X == Y` is a constraint, then `X` and `Y` must in teh same equivalence class
* Each type in an equivalence class will either be an associated type of the form `T.A.B...Z`, derived from a generic type parameter, or a *independent* type, which here means anything other than those associated types.
* Because of the rules enforced during semantic checking, each equivalence class must have at least one associated type in it.
* Each equivalence class may have zero or more independent types in it.
* For each equivalence class with more than one independent type in it, diagnose an error; the application is attempting to constrain one or more associated types to be equal to multiple distinct types at once
* For each equivalence class with exactly one independent type in it, produce new constraints of the form `T.A.B...Z == C`, one for each associated type in the equivalence class, where `C` is the independent type
* For each equivalence class with zero independent types in it, pick the *minimal* associated type (according to the type ordering), and produce new constraints of the form `T.A... == U.B...` for each *other* associated type in the equivalence class, where `U.B...` is the minimal associated type.
* Sort the new constraints by the associated type on their left-hand side.

Alternatives Considered
-----------------------

The main alternative here would be to simply not have these kinds of constraints, and push programmers to use type parameters instead of associated types in cases where they want to be able to enforce constraints on those types.
E.g., the `IPackable` interface from earlier could be rewritten into this form:


interface IPackable<Packed>
{
init(Packed packed);
Packed pack();
}

With this form for `IPackable`, it becomes possible to use additional type parameters to constraint the `Packed` type:

T deserializePackable<T, U>( InputStream stream )
where T : IPackable<U>,
P : IDeserializable
{
return T( U(stream) );
}

While this workaround may seem reasomable in an isolated example like this, there is a strong reason why languages like Slang choose to have both generic type parameters (which act as *inputs* to an abstraction) and associated types (which act as *outputs*).
We believe that associated types are an important feature, and that they justify the complexity of these new kinds of constraints.
4 changes: 2 additions & 2 deletions docs/scripts/release-note.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ verbose=true
$verbose && echo "Reminder: PLEASE make sure your local repo is up-to-date before running the script." >&2

gh=""
for candidate in "$(which gh.exe)" "/mnt/c/Program Files/GitHub CLI/gh.exe" "/c/Program Files/GitHub CLI/gh.exe"
for candidate in "$(which gh.exe)" "/mnt/c/Program Files/GitHub CLI/gh.exe" "/c/Program Files/GitHub CLI/gh.exe" "/cygdrive/c/Program Files/GitHub CLI/gh.exe"
do
if [ -x "$candidate" ]
then
Expand Down Expand Up @@ -51,7 +51,7 @@ do

# Get PR number from the git commit title
pr="$(echo "$line" | grep '#[1-9][0-9][0-9][0-9][0-9]*' | sed 's|.* (\#\([1-9][0-9][0-9][0-9][0-9]*\))|\1|')"
[ "x$pr" = "x" ] && break
[ "x$pr" = "x" ] && continue

# Check if the PR is marked as a breaking change
if "$gh" issue view $pr --json labels | grep -q 'pr: breaking change'
Expand Down
9 changes: 9 additions & 0 deletions include/slang.h
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ extern "C"
SLANG_METAL_LIB, ///< Metal library
SLANG_METAL_LIB_ASM, ///< Metal library assembly
SLANG_HOST_SHARED_LIBRARY, ///< A shared library/Dll for host code (for hosting CPU/OS)
SLANG_WGSL, ///< WebGPU shading language
SLANG_TARGET_COUNT_OF,
};

Expand Down Expand Up @@ -636,6 +637,7 @@ extern "C"
SLANG_PASS_THROUGH_LLVM, ///< LLVM 'compiler' - includes LLVM and Clang
SLANG_PASS_THROUGH_SPIRV_OPT, ///< SPIRV-opt
SLANG_PASS_THROUGH_METAL, ///< Metal compiler
SLANG_PASS_THROUGH_WGSL, ///< WGSL compiler
SLANG_PASS_THROUGH_COUNT_OF,
};

Expand Down Expand Up @@ -735,6 +737,7 @@ extern "C"
SLANG_SOURCE_LANGUAGE_CUDA,
SLANG_SOURCE_LANGUAGE_SPIRV,
SLANG_SOURCE_LANGUAGE_METAL,
SLANG_SOURCE_LANGUAGE_WGSL,
SLANG_SOURCE_LANGUAGE_COUNT_OF,
};

Expand Down Expand Up @@ -2587,6 +2590,7 @@ extern "C"
SLANG_API SlangReflectionType* spReflectionFunction_GetResultType(SlangReflectionFunction* func);
SLANG_API SlangReflectionGeneric* spReflectionFunction_GetGenericContainer(SlangReflectionFunction* func);
SLANG_API SlangReflectionFunction* spReflectionFunction_applySpecializations(SlangReflectionFunction* func, SlangReflectionGeneric* generic);
SLANG_API SlangReflectionFunction* spReflectionFunction_specializeWithArgTypes(SlangReflectionFunction* func, SlangInt argTypeCount, SlangReflectionType* const* argTypes);

// Abstract Decl Reflection

Expand Down Expand Up @@ -3585,6 +3589,11 @@ namespace slang
{
return (FunctionReflection*)spReflectionFunction_applySpecializations((SlangReflectionFunction*)this, (SlangReflectionGeneric*)generic);
}

FunctionReflection* specializeWithArgTypes(unsigned int argCount, TypeReflection* const* types)
{
return (FunctionReflection*)spReflectionFunction_specializeWithArgTypes((SlangReflectionFunction*)this, argCount, (SlangReflectionType* const*)types);
}
};

struct GenericReflection
Expand Down
3 changes: 3 additions & 0 deletions source/compiler-core/slang-artifact-desc-util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ SLANG_HIERARCHICAL_ENUM(ArtifactKind, SLANG_ARTIFACT_KIND, SLANG_ARTIFACT_KIND_E
x(CUDA, Source) \
x(Metal, Source) \
x(Slang, Source) \
x(WGSL, Source) \
x(KernelLike, Base) \
x(DXIL, KernelLike) \
x(DXBC, KernelLike) \
Expand Down Expand Up @@ -288,6 +289,7 @@ SLANG_HIERARCHICAL_ENUM(ArtifactStyle, SLANG_ARTIFACT_STYLE, SLANG_ARTIFACT_STYL
case SLANG_METAL: return Desc::make(Kind::Source, Payload::Metal, Style::Kernel, 0);
case SLANG_METAL_LIB: return Desc::make(Kind::Executable, Payload::MetalAIR, Style::Kernel, 0);
case SLANG_METAL_LIB_ASM: return Desc::make(Kind::Assembly, Payload::MetalAIR, Style::Kernel, 0);
case SLANG_WGSL: return Desc::make(Kind::Source, Payload::WGSL, Style::Kernel, 0);
default: break;
}

Expand Down Expand Up @@ -330,6 +332,7 @@ SLANG_HIERARCHICAL_ENUM(ArtifactStyle, SLANG_ARTIFACT_STYLE, SLANG_ARTIFACT_STYL
case Payload::Cpp: return (desc.style == Style::Host) ? SLANG_HOST_CPP_SOURCE : SLANG_CPP_SOURCE;
case Payload::CUDA: return SLANG_CUDA_SOURCE;
case Payload::Metal: return SLANG_METAL;
case Payload::WGSL: return SLANG_WGSL;
default: break;
}
break;
Expand Down
1 change: 1 addition & 0 deletions source/compiler-core/slang-artifact.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ enum class ArtifactPayload : uint8_t
CUDA, ///< CUDA source
Metal, ///< Metal source
Slang, ///< Slang source
WGSL, ///< WGSL source

KernelLike, ///< GPU Kernel like

Expand Down
1 change: 1 addition & 0 deletions source/core/slang-type-text-util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ static const TypeTextUtil::CompileTargetInfo s_compileTargetInfos[] =
{ SLANG_METAL, "metal", "metal", "Metal shader source" },
{ SLANG_METAL_LIB, "metallib", "metallib", "Metal Library Bytecode" },
{ SLANG_METAL_LIB_ASM, "metallib-asm" "metallib-asm", "Metal Library Bytecode assembly" },
{ SLANG_WGSL, "wgsl", "wgsl", "WebGPU shading language source" },
};

static const NamesDescriptionValue s_languageInfos[] =
Expand Down
7 changes: 1 addition & 6 deletions source/slang-glslang/slang-glslang.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@


#include "glslang/Public/ResourceLimits.h"
#include "StandAlone/Worklist.h"
#include "glslang/Include/ShHandle.h"
#include "glslang/Public/ShaderLang.h"
#include "SPIRV/GlslangToSpv.h"
#include "SPIRV/GLSL.std.450.h"
#include "SPIRV/doc.h"
#include "SPIRV/disassemble.h"

#include "glslang/MachineIndependent/localintermediate.h"

#include "slang.h"

#include "spirv-tools/optimizer.hpp"
Expand All @@ -23,6 +17,7 @@
#endif

#include <memory>
#include <mutex>
#include <sstream>

// This is a wrapper to allow us to run the `glslang` compiler
Expand Down
1 change: 1 addition & 0 deletions source/slang-record-replay/util/emum-to-string.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace SlangRecord
CASE(SLANG_METAL_LIB);
CASE(SLANG_METAL_LIB_ASM);
CASE(SLANG_HOST_SHARED_LIBRARY);
CASE(SLANG_WGSL);
CASE(SLANG_TARGET_COUNT_OF);
default:
Slang::StringBuilder str;
Expand Down
2 changes: 1 addition & 1 deletion source/slang/core.meta.slang
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ ${{{{
__intrinsic_op($(kIROp_Sub)) This sub(This other);
__intrinsic_op($(kIROp_Mul)) This mul(This other);
__intrinsic_op($(kIROp_Div)) This div(This other);
__intrinsic_op($(kIROp_FRem)) This mod(This other);
__intrinsic_op($(kIROp_IRem)) This mod(This other);
__intrinsic_op($(kIROp_Neg)) This neg();
__intrinsic_op($(kIROp_Lsh)) This shl(int other);
__intrinsic_op($(kIROp_Rsh)) This shr(int other);
Expand Down
Loading

0 comments on commit 2682128

Please sign in to comment.