From 9cf86bafc57b10b16485ca1c7a6933e5cb4ccf2e Mon Sep 17 00:00:00 2001 From: Dylan Perks Date: Thu, 12 Sep 2024 14:20:32 +0100 Subject: [PATCH] Save progress, as I am deleting the progress --- generator.json | 2 +- .../Core/Core/Abstractions/BreakneckLock.cs | 195 +++++++++++ .../SilkTouch/SilkTouch/Mods/Common/IMod.cs | 51 +-- .../SilkTouch/Mods/Common/IModConfigBinder.cs | 15 - .../SilkTouch/Mods/Common/IModContext.cs | 76 +++++ .../SilkTouch/Mods/Common/IModLoader.cs | 12 + .../Mods/Common/ModArtifactAccess.cs | 32 ++ .../SilkTouch/Mods/Common/ModContext.cs | 305 ++++++++++++++++++ 8 files changed, 624 insertions(+), 64 deletions(-) create mode 100644 sources/Core/Core/Abstractions/BreakneckLock.cs delete mode 100644 sources/SilkTouch/SilkTouch/Mods/Common/IModConfigBinder.cs create mode 100644 sources/SilkTouch/SilkTouch/Mods/Common/IModContext.cs create mode 100644 sources/SilkTouch/SilkTouch/Mods/Common/IModLoader.cs create mode 100644 sources/SilkTouch/SilkTouch/Mods/Common/ModArtifactAccess.cs create mode 100644 sources/SilkTouch/SilkTouch/Mods/Common/ModContext.cs diff --git a/generator.json b/generator.json index 2f26174825..61040cb760 100644 --- a/generator.json +++ b/generator.json @@ -12,7 +12,7 @@ }, "InputSourceRoot": "eng/submodules/terrafx.interop.windows/sources/Interop/Windows", "InputTestRoot": "eng/submodules/terrafx.interop.windows/tests/Interop/Windows", - "OutputSourceRoot": "sources/Windows", + "OutputSourceRoot": "sources/Win32/Win32", "OutputTestRoot": "tests/Windows", "DefaultLicenseHeader": "eng/silktouch/header.txt", "Solution": "Silk.NET.sln", diff --git a/sources/Core/Core/Abstractions/BreakneckLock.cs b/sources/Core/Core/Abstractions/BreakneckLock.cs new file mode 100644 index 0000000000..588b2d3576 --- /dev/null +++ b/sources/Core/Core/Abstractions/BreakneckLock.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Sourced from https://github.com/john-h-k/SpinLockSlim under the MIT license + +// MIT License +// +// Copyright (c) 2019 John Kelly +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +// ReSharper disable RedundantAssignment + +namespace Silk.NET.Core; + +/// +/// Provided a lightweight spin lock for synchronization in high performance +/// scenarios with a low hold time +/// +/// +/// This lock is very performant, but it is very dangerous (hence breakneck). +/// It's recommended to use the framework-provided locks where possible. +/// +public struct BreakneckLock +{ + private static int True => 1; + private static int False => 0; + + private const MethodImplOptions MaxOpt = (MethodImplOptions)768; + + private volatile int _acquired; // either 1 or 0 + + /// + /// Creates a new + /// + /// A new + [MethodImpl(MaxOpt)] + public static BreakneckLock Create() => new(); + + /// + /// Returns true if the lock is acquired, else false + /// +#pragma warning disable 420 // Unsafe.As<,> doesn't read the reference so the lack of volatility is not an issue, but we do need to treat the returned reference as volatile + public bool IsAcquired => Volatile.Read(ref Unsafe.As(ref _acquired)); +#pragma warning restore 420 + + /// + /// Enter the lock. If this method returns, + /// will be true. If an exception occurs, will indicate + /// whether the lock was taken and needs to be released using . + /// This method may never exit + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted. + /// If the method returns, this is guaranteed to be true + [MethodImpl(MaxOpt)] + public void Enter(ref bool taken) + { + // while acquired == 1, loop, then when it == 0, exit and set it to 1 + while (!TryAcquire()) + { + // NOP + } + + taken = true; + } + + /// + /// Enter the lock if it not acquired, else, do not. will be + /// true if the lock was taken, else false. If is + /// true, must be called to release it, else, it must not be called + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted + [MethodImpl(MaxOpt)] + public void TryEnter(ref bool taken) + { + taken = TryAcquire(); + } + + /// + /// Try to safely enter the lock a certain number of times (). + /// will be true if the lock was taken, else false. + /// If is true, must be called to release + /// it, else, it must not be called + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted + /// The number of attempts to acquire the lock before returning + /// without the lock + [MethodImpl(MaxOpt)] + public void TryEnter(ref bool taken, uint iterations) + { + // if it acquired == 0, change it to 1 and return true, else return false + while (!TryAcquire()) + { + if (unchecked(iterations--) == 0) // postfix decrement, so no issue if iterations == 0 at first + { + return; + } + } + + taken = true; + } + + /// + /// Try to safely enter the lock for a certain (). + /// will be true if the lock was taken, else false. + /// If is true, must be called to release + /// it, else, it must not be called + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted + /// The to attempt to acquire the lock for before + /// returning without the lock. A negative will cause undefined behaviour + [MethodImpl(MaxOpt)] + public void TryEnter(ref bool taken, TimeSpan timeout) + { + long start = Stopwatch.GetTimestamp(); + long end = unchecked((long)timeout.TotalMilliseconds * Stopwatch.Frequency + start); + + // if it acquired == 0, change it to 1 and return true, else return false + while (!TryAcquire()) + { + if (Stopwatch.GetTimestamp() >= end) + { + return; + } + } + + taken = true; + } + + /// + /// Exit the lock. This method is dangerous and must be called only once the caller is sure they have + /// ownership of the lock. + /// + [MethodImpl(MaxOpt)] + public void Exit() + { + // release the lock - int32 write will always be atomic + _acquired = False; + } + + /// + /// Exit the lock with an optional post-release memory barrier. This method is dangerous and must be called only + /// once the caller is sure they have ownership of the lock. + /// + /// Whether a memory barrier should be inserted after the release + [MethodImpl(MaxOpt)] + public void Exit(bool insertMemBarrier) + { + Exit(); + + if (insertMemBarrier) + Thread.MemoryBarrier(); + } + + /// + /// Exit the lock with a post-release memory barrier. This method is dangerous and must be called only once the + /// caller is sure they have ownership of the lock. + /// + [MethodImpl(MaxOpt)] + public void ExitWithBarrier() + { + Exit(); + Thread.MemoryBarrier(); + } + + [MethodImpl(MaxOpt)] + private bool TryAcquire() + { + // if it acquired == 0, change it to 1 and return true, else return false + return Interlocked.CompareExchange(ref _acquired, True, False) == False; + } +} diff --git a/sources/SilkTouch/SilkTouch/Mods/Common/IMod.cs b/sources/SilkTouch/SilkTouch/Mods/Common/IMod.cs index 22a4d77c3e..b492d63b28 100644 --- a/sources/SilkTouch/SilkTouch/Mods/Common/IMod.cs +++ b/sources/SilkTouch/SilkTouch/Mods/Common/IMod.cs @@ -16,53 +16,8 @@ namespace Silk.NET.SilkTouch.Mods; public interface IMod { /// - /// Runs before SilkTouch does anything with the given job name and job configuration. + /// Initializes the mod. /// - /// The job name (corresponds to the configuration key for mod configs). - /// The job config. - /// An asynchronous task. - Task BeforeJobAsync(string key, SilkTouchConfiguration config) => Task.CompletedTask; - - /// - /// Runs before SilkTouch invokes ClangSharp with the given parsed response files. Gives each mod an opportunity to - /// modify the generator configuration. - /// - /// The job name (corresponds to the configuration key for mod configs). - /// The read response files. - /// - /// The modified response files to be passed into either the next mod or ClangSharp if this is the last mod. - /// - Task> BeforeScrapeAsync(string key, List rsps) => - Task.FromResult(rsps); - - /// - /// Runs after SilkTouch has invoked ClangSharp which generated the given syntax nodes. Gives each mod an - /// opportunity to mutate the syntax tree. - /// - /// The job name (corresponds to the configuration key for mod configs). - /// The generated output from ClangSharp (or the previous mod). - /// - /// The modified syntax nodes to be either passed to the next mod or output from the generator if this is the last - /// mod. - /// - Task AfterScrapeAsync(string key, GeneratedSyntax syntax) => - Task.FromResult(syntax); - - /// - /// Runs before SilkTouch is going to output the MSBuild workspace. The generated documents have already been added, - /// so this gives the opportunity for the mod to modify the workspace further. - /// - /// The job name (corresponds to the configuration key for mod configs). - /// The generated output from scraping. - /// The modified MSBuild solution either to be output or passed to the next mod if applicable. - Task BeforeOutputAsync(string key, GeneratorWorkspace workspace) => - Task.FromResult(workspace); - - /// - /// Runs after all generation activities have completed. Gives each mod an opportunity to clean up its state for - /// this job. - /// - /// The job name (corresponds to the configuration key for mod configs). - /// An asynchronous task. - Task AfterJobAsync(string key) => Task.CompletedTask; + /// + void Initialize(IModContext context); } diff --git a/sources/SilkTouch/SilkTouch/Mods/Common/IModConfigBinder.cs b/sources/SilkTouch/SilkTouch/Mods/Common/IModConfigBinder.cs deleted file mode 100644 index 6de07e473d..0000000000 --- a/sources/SilkTouch/SilkTouch/Mods/Common/IModConfigBinder.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace Silk.NET.SilkTouch.Mods; - -/// -/// A mod that customises the binding of its configuration. -/// -public interface IModConfigBinder : IMod -{ - /// - /// Used to configure the Used to configure the . - /// - /// The options to configure. - static abstract void Configure(BinderOptions options); -} diff --git a/sources/SilkTouch/SilkTouch/Mods/Common/IModContext.cs b/sources/SilkTouch/SilkTouch/Mods/Common/IModContext.cs new file mode 100644 index 0000000000..b27372d262 --- /dev/null +++ b/sources/SilkTouch/SilkTouch/Mods/Common/IModContext.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Silk.NET.SilkTouch.Mods; + +/// +/// An interface into the mod pipeline for mod execution. +/// +public interface IModContext +{ + /// + /// The current job key. + /// + string JobKey { get; } + + /// + /// Denotes this mod as producing artifacts of the given type. + /// + /// The modes allowed to be used for this artifact. + /// The type of the artifacts. + /// + /// This method should be used if is not called prior to the enumeration of artifacts + /// added using . does enact the behaviour of this + /// function implicitly provided that it is called before exits. + /// + void ProducesArtifacts( + ModArtifactAccess allowedModes = + ModArtifactAccess.Exclusive | ModArtifactAccess.Shared | ModArtifactAccess.Take + ); + + /// + /// Denotes this mod as receiving artifacts of the given type. + /// + /// The mode with which the artifacts shall be accessed. + /// The type of the artifacts. + /// + /// This method should be used if is not called prior to the enumeration of artifacts + /// added using . does enact the behaviour of this + /// function implicitly provided that it is called before exits. + /// + void ReceivesArtifacts(ModArtifactAccess accessMode); + + /// + /// Gets the artifacts of the given type from previous mods in the pipeline. + /// + /// The type of the artifacts. + /// The artifacts. + /// + /// This should be called within , but should be enumerated as part of the evaluation + /// of . Calling this and using the result either in a LINQ method chain or async + /// generator (i.e. yield) should be sufficient. If this is not achievable, use + /// . + /// + IAsyncEnumerable GetArtifacts(ModArtifactAccess accessMode); + + /// + /// Adds the given artifacts to the pipeline. + /// + /// The artifacts + /// + /// The allowable modes with which the artifacts can be accessed. This is used to determine the concurrent flow of + /// artifacts through the pipeline. Generally this is expected to be consistent for any given artifact type, and + /// should reflect whether the artifact is stateful or not (i.e. stateful/mutable artifacts should use + /// or , whereas immutable artifacts + /// should use or ). This is simply used + /// for validation of calls to /. + /// + /// The type of the artifacts. + void AddArtifacts( + IAsyncEnumerable artifacts, + ModArtifactAccess allowedModes = + ModArtifactAccess.Exclusive | ModArtifactAccess.Shared | ModArtifactAccess.Take + ); +} diff --git a/sources/SilkTouch/SilkTouch/Mods/Common/IModLoader.cs b/sources/SilkTouch/SilkTouch/Mods/Common/IModLoader.cs new file mode 100644 index 0000000000..1c4daf0aea --- /dev/null +++ b/sources/SilkTouch/SilkTouch/Mods/Common/IModLoader.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace Silk.NET.SilkTouch.Mods; + +public interface IModLoader +{ + bool TryLoadMod(string identifier, [NotNullWhen(true)] out IMod? mod); +} diff --git a/sources/SilkTouch/SilkTouch/Mods/Common/ModArtifactAccess.cs b/sources/SilkTouch/SilkTouch/Mods/Common/ModArtifactAccess.cs new file mode 100644 index 0000000000..3488f26f9c --- /dev/null +++ b/sources/SilkTouch/SilkTouch/Mods/Common/ModArtifactAccess.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Silk.NET.SilkTouch.Mods; + +/// +/// Describes the modes in which a mod can access artifacts. +/// +[Flags] +public enum ModArtifactAccess +{ + /// + /// Borrows the artifacts from the pipeline. This allows the mod to read the artifacts without removing them from + /// the pipeline. However, because they are borrowed, they cannot be used by the next mod until this mod has + /// completed execution. + /// + Exclusive = 1 << 0, + + /// + /// Takes the artifacts from the pipeline. No further mods will have access to these artifacts unless the mod + /// explicitly reintroduces them into the pipeline. + /// + Take = 1 << 1, + + /// + /// Reads the artifacts from the pipeline without removing them. This option, unlike , allows + /// subsequent mods to execute immediately. + /// + Shared = 1 << 2 +} diff --git a/sources/SilkTouch/SilkTouch/Mods/Common/ModContext.cs b/sources/SilkTouch/SilkTouch/Mods/Common/ModContext.cs new file mode 100644 index 0000000000..53667f601c --- /dev/null +++ b/sources/SilkTouch/SilkTouch/Mods/Common/ModContext.cs @@ -0,0 +1,305 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Silk.NET.SilkTouch.Mods; + +internal class ModContext : IModContext +{ + public required IMod Mod { get; init; } + public required Task InitGate { get; init; } + + // TODO Parallelize the enumeration of awaitable enumerables instead of using a channel. + // Essentially, make an IAsyncEnumerable that joins multiple IAsyncEnumerables and does a WaitAny on all + // constituents but otherwise acts the same way as the channel currently does, rather than process one enumerable at + // a time. + internal abstract record ArtifactDescriptor( + object? EnumerableOrChannelWriter, + ModArtifactAccess Mode + ) + { + public abstract (object Enumerable, object ChannelWriter) CreateChannel( + bool manyEnumerable + ); + } + + internal record ArtifactDescriptor(object? EnumerableOrChannelWriter, ModArtifactAccess Mode) + : ArtifactDescriptor(EnumerableOrChannelWriter, Mode) + { + public override (object Enumerable, object ChannelWriter) CreateChannel(bool manyEnumerable) + { + var channel = Channel.CreateUnbounded>(); + return ( + manyEnumerable + ? new OneToManyArtifactCollection(channel) + : OneToOneArtifactCollection(channel), + channel.Writer + ); + } + } + + public static async IAsyncEnumerable OneToOneArtifactCollection( + Channel> artifacts, + [EnumeratorCancellation] CancellationToken ct = default + ) + { + await foreach (var enumerable in artifacts.Reader.ReadAllAsync(ct)) + { + await foreach (var artifact in enumerable.WithCancellation(ct)) + { + yield return artifact; + } + } + } + + public class OneToManyArtifactCollection(Channel> artifacts) + : IAsyncEnumerable + { + // TODO Actually parallelize the lazy delivery of artifacts to many consumers. + // This is a dumb implementation that has the first enumerator enumerate the enumerable in its entirety into a + // list, which is then accessed by all enumerators. Realistically we want a way that: + // - The underlying channel and its enumerables are only enumerated when one of the enumerators enumerates it. + // - When the enumeration does happen, it happens on whatever synchronization context that first invoked the + // enumeration. + // - This is so if ever this framework is reused for a source generator form factor (for more info read the + // old 63304c46 version of Proposal - Generation of Library Sources and PInvoke Mechanisms) we can execute + // in a way that plays ball with Roslyn's "no threading in a source generator" rule. + // - When the enumeration is in progress, any additional enumerators that attempt to enumerate will wait for the + // first enumeration to conclude, and also return the result generated for that enumeration. i.e. all + // enumerators get the same results, even if they're not the one that called the underlying enumerator. + // So essentially a "broadcast channel" but where the reader executes the channel writing logic if it detects + // it's not running already. You can see why I didn't do this for the initial release. + + private TaskCompletionSource _tcs = new(); + private int _gate; + private List? _artifacts; + + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) + { + if (Interlocked.CompareExchange(ref _gate, 1, 0) == 0) + { + _artifacts = []; + // TODO make a more cancellation-friendly impl + await foreach ( + var enumerable in artifacts.Reader.ReadAllAsync(CancellationToken.None) + ) + { + await foreach ( + var artifact in enumerable.WithCancellation(CancellationToken.None) + ) + { + _artifacts.Add(artifact); + } + } + + _tcs.SetResult(); + } + + await _tcs.Task.WaitAsync(cancellationToken); + foreach (var artifact in _artifacts!) + { + yield return artifact; + } + } + } + + public Dictionary< + Type, + (ArtifactDescriptor? Inbound, ArtifactDescriptor? Outbound) + > Artifacts { get; set; } = []; + + public required string JobKey { get; init; } + + public void ProducesArtifacts( + ModArtifactAccess allowedModes = + ModArtifactAccess.Exclusive | ModArtifactAccess.Take | ModArtifactAccess.Shared + ) + { + ArtifactDescriptor? ib = null, + ob = null; + if (Artifacts.TryGetValue(typeof(T), out var desc)) + { + (ib, ob) = desc; + } + + if (ob is null) + { + if (InitGate.IsCompleted) + { + throw new InvalidOperationException( + "Cannot make artifact declaration as the mod has already progressed past initialisation." + ); + } + } + else if ((ob.Mode & allowedModes) != allowedModes) + { + throw new ArgumentException( + "Artifact type has already been declared and one or more of the allowed modes was not part of the original declaration.", + nameof(allowedModes) + ); + } + else + { + return; + } + + Artifacts[typeof(T)] = (ib, new ArtifactDescriptor(null, allowedModes)); + } + + public void ReceivesArtifacts(ModArtifactAccess accessMode) + { + if (BitOperations.PopCount((uint)accessMode) != 1) + { + throw new ArgumentException( + "Must only use one mode for artifact access.", + nameof(accessMode) + ); + } + + ArtifactDescriptor? ib = null, + ob = null; + if (Artifacts.TryGetValue(typeof(T), out var desc)) + { + (ib, ob) = desc; + } + + if (ib is null) + { + if (InitGate.IsCompleted) + { + throw new InvalidOperationException( + "Cannot make artifact declaration as the mod has already progressed past initialisation." + ); + } + } + else if (ib.Mode != accessMode) + { + throw new ArgumentException( + "Access mode differs from the mode used at declaration time.", + nameof(accessMode) + ); + } + else + { + return; + } + + Artifacts[typeof(T)] = (new ArtifactDescriptor(null, accessMode), ob); + } + + public async IAsyncEnumerable GetArtifacts(ModArtifactAccess accessMode) + { + ReceivesArtifacts(accessMode); + await InitGate; + if ( + Artifacts[typeof(T)].Inbound?.EnumerableOrChannelWriter + is not IAsyncEnumerable enumerable + ) + { + yield break; + } + + var unborrowChannel = + accessMode == ModArtifactAccess.Exclusive ? Channel.CreateUnbounded() : null; + if ( + unborrowChannel is not null + && Artifacts[typeof(T)].Outbound?.EnumerableOrChannelWriter + is ChannelWriter> cw + ) + { + await cw.WriteAsync(unborrowChannel.Reader.ReadAllAsync()); + } + + await foreach (var artifact in enumerable) + { + yield return artifact; + if (unborrowChannel is not null) + { + await unborrowChannel.Writer.WriteAsync(artifact); + } + } + } + + public void AddArtifacts( + IAsyncEnumerable artifacts, + ModArtifactAccess allowedModes = + ModArtifactAccess.Exclusive | ModArtifactAccess.Take | ModArtifactAccess.Shared + ) + { + ProducesArtifacts(allowedModes); + ( + Artifacts[typeof(T)].Outbound?.EnumerableOrChannelWriter + as ChannelWriter> + )?.TryWrite(artifacts); + } +} + +class ModPipeline +{ + private TaskCompletionSource _initGate = new(); + public ModContext[] Mods { get; } + + public ModPipeline(string job, IEnumerable mods) => + Mods = [ + .. mods.Select(x => new ModContext + { + Mod = x, + InitGate = _initGate.Task, + JobKey = job + }) + ]; + + public async Task ExecuteAsync() + { + if (_initGate.Task.IsCompleted) + { + throw new InvalidOperationException("This pipeline has already executed."); + } + + // First, let's initialize all the mods + foreach (var mod in Mods) + { + mod.Mod.Initialize(mod); + } + + // Now we need to create all the channels between the mods to pass artifacts around. + // We work backwards i.e. to create the channels and configure the receiving end first, rather than the sending + // end. This is primarily to make the cases where there are multiple producers before a single consumer easier, + // as we can just pass those producers the same channel. + var artifactReceivers = + new Dictionary(); + for (var i = Mods.Length - 1; i >= 0; i--) + { + var mod = Mods[i]; + foreach (var (artifactType, (ib, ob)) in mod.Artifacts) + { + // If this mod produces artifacts and we have a receiver for the next mod, then assign that descriptor. + var (newIb, newOb) = (ib, ob); + if (ob is not null && artifactReceivers.TryGetValue(artifactType, out var recv)) + { + if ((ob.Mode & recv.Mode) != recv.Mode) + { + throw new NotSupportedException( + $"\"{recv.Mod}\" is accessing artifact \"{artifactType.FullName}\" with mode " + + $"\"{recv.Mode}\" whereas \"{mod.Mod.GetType().FullName}\" is sending it with " + + $"allowable modes \"{ob.Mode}\". These modes are expected to be consistent for a " + + $"given artifact type." + ); + } + + newOb = ob with { EnumerableOrChannelWriter = recv.ChannelWriter }; + } + } + } + } +}