diff --git a/Bond.Build/Bond.Build.csproj b/Bond.Build/Bond.Build.csproj
index 07b64e9..ec1f1d7 100644
--- a/Bond.Build/Bond.Build.csproj
+++ b/Bond.Build/Bond.Build.csproj
@@ -27,7 +27,6 @@
compile; build; native; contentfiles; analyzers; buildtransitive
-
diff --git a/Bond.Build/GenerateBond.cs b/Bond.Build/GenerateBond.cs
index ab50bf0..399ca25 100644
--- a/Bond.Build/GenerateBond.cs
+++ b/Bond.Build/GenerateBond.cs
@@ -1,90 +1,101 @@
using System;
-using System.Globalization;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security.Cryptography;
using System.Text;
-using System.Threading;
+using Bond.Parser.CodeGeneration;
+using Bond.Parser.Parser;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace BondTools.Build;
-public sealed class GenerateBond : Task, ICancelableTask
+///
+/// Generates C# for each Bond item. Generation is cheap, so it always runs; outputs are only rewritten when their
+/// content changes, which keeps the C# compilation incremental.
+///
+public sealed class GenerateBond : Microsoft.Build.Utilities.Task
{
+ public ITaskItem[] Sources { get; set; } = [];
+
[Required]
- public string ProjectFile { get; set; } = "";
+ public string ProjectDirectory { get; set; } = "";
[Required]
public string OutputDirectory { get; set; } = "";
- public ITaskItem[] Sources { get; set; } = [];
- public ITaskItem[] ImportDirectories { get; set; } = [];
- public ITaskItem[] Usings { get; set; } = [];
- public ITaskItem[] NamespaceMappings { get; set; } = [];
- public ITaskItem[] TypeMappings { get; set; } = [];
[Output]
public ITaskItem[] GeneratedFiles { get; private set; } = [];
- [Output]
- public ITaskItem[] WrittenFiles { get; private set; } = [];
-
- private readonly CancellationTokenSource _cancellation = new();
-
public override bool Execute()
{
- try
+ Directory.CreateDirectory(OutputDirectory);
+ var outputs = new List();
+ foreach (var source in Sources)
{
- var request = BuildRequest.Create(ProjectFile, OutputDirectory, Sources, ImportDirectories, Usings, NamespaceMappings, TypeMappings);
- var result = GenerationEngine.RunAsync(request, _cancellation.Token).GetAwaiter().GetResult();
-
- foreach (var error in result.Errors)
+ var path = source.GetMetadata("FullPath");
+ var output = Path.Combine(OutputDirectory, OutputName(path));
+ outputs.Add(output);
+ try
{
- Log.LogError(null, "BOND1001", null, error.FilePath ?? ProjectFile, error.Line, error.Column, 0, 0, "{0}", error.Message);
+ Generate(source, path, output);
}
-
- if (result.Errors.Count != 0)
+ catch (Exception error) when (error is IOException or UnauthorizedAccessException)
{
- return false;
+ Log.LogError(null, "BOND1001", null, path, 0, 0, 0, 0, "{0}", error.Message);
}
-
- GeneratedFiles = result.GeneratedFiles.Select(CreateOutputItem).ToArray();
- WrittenFiles = result.WrittenFiles.Select(CreateOutputItem).ToArray();
- Log.LogMessage(MessageImportance.High, "{0}", result.Status);
- return true;
}
- catch (OperationCanceledException) when (_cancellation.IsCancellationRequested)
- {
- Log.LogMessage(MessageImportance.High, "Bond generation cancelled.");
- return false;
- }
- catch (Exception error) when (error is IOException or InvalidDataException or UnauthorizedAccessException
- or ArgumentException or NotSupportedException)
+
+ foreach (var stale in Directory.EnumerateFiles(OutputDirectory, "*.g.cs").Except(outputs))
{
- Log.LogError(null, "BOND1001", null, ProjectFile, 0, 0, 0, 0, "{0}", error.Message);
- return false;
+ if (File.ReadLines(stale).FirstOrDefault() == CSharpGenerator.GeneratedHeader)
+ {
+ File.Delete(stale);
+ }
}
+
+ GeneratedFiles = outputs.Select(output => (ITaskItem)new TaskItem(Escape(output))).ToArray();
+ return !Log.HasLoggedErrors;
}
- private static ITaskItem CreateOutputItem(string path)
+ private void Generate(ITaskItem source, string path, string output)
{
- // TaskItem expects an MSBuild-escaped include, not a literal filesystem path.
- var escaped = new StringBuilder(path.Length);
- foreach (var character in path)
+ var options = new CSharpGenerationOptions
{
- switch (character)
- {
- case '%' or '*' or '?' or '@' or '$' or '(' or ')' or ';' or '\'':
- escaped.Append('%');
- escaped.Append(((int)character).ToString("X2", CultureInfo.InvariantCulture));
- break;
- default:
- escaped.Append(character);
- break;
- }
+ UsingNamespaces = List(source, "Usings"),
+ NamespaceMappings = List(source, "NamespaceMappings"),
+ TypeMappings = List(source, "TypeMappings"),
+ ModelFeatures = Feature(source, "Clone", CSharpModelFeatures.Cloning)
+ | Feature(source, "Equality", CSharpModelFeatures.Equality)
+ | Feature(source, "ToString", CSharpModelFeatures.StringRepresentation)
+ };
+ var imports = List(source, "ImportDirectories").Select(directory => Path.GetFullPath(directory, ProjectDirectory));
+ var parsed = ParserFacade.ParseFileAsync(path, DefaultImportResolver.Create(imports)).GetAwaiter().GetResult();
+ var generated = parsed.Success ? CSharpGenerator.Generate(parsed.Ast!, path, options) : null;
+ foreach (var error in generated?.Errors ?? parsed.Errors)
+ {
+ Log.LogError(null, "BOND1001", null, error.FilePath ?? path, error.Line, error.Column, 0, 0, "{0}", error.Message);
}
- return new TaskItem(escaped.ToString());
+ if (generated is { Success: true } && !(File.Exists(output) && File.ReadAllText(output) == generated.Code))
+ {
+ File.WriteAllText(output, generated.Code);
+ }
}
- public void Cancel() => _cancellation.Cancel();
+ // Unique per source path, so schemas with the same name in different directories do not collide.
+ private static string OutputName(string source) =>
+ Path.GetFileNameWithoutExtension(source) + "." +
+ Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(source)))[..8].ToLowerInvariant() + ".g.cs";
+
+ private static string[] List(ITaskItem item, string name) =>
+ item.GetMetadata(name).Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ private static CSharpModelFeatures Feature(ITaskItem item, string name, CSharpModelFeatures feature) =>
+ string.Equals(item.GetMetadata(name), "true", StringComparison.OrdinalIgnoreCase) ? feature : CSharpModelFeatures.None;
+
+ // Task item specs are MSBuild-escaped; literal paths may contain characters such as ';' or '%'.
+ private static string Escape(string path) =>
+ string.Concat(path.Select(character => "%*?@$();'".Contains(character) ? $"%{(int)character:X2}" : character.ToString()));
}
diff --git a/Bond.Build/GenerationEngine.cs b/Bond.Build/GenerationEngine.cs
deleted file mode 100644
index 883cf56..0000000
--- a/Bond.Build/GenerationEngine.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Bond.Parser.CodeGeneration;
-using Bond.Parser.Parser;
-
-namespace BondTools.Build;
-
-internal sealed record GenerationResult(
- string[] GeneratedFiles,
- string[] WrittenFiles,
- IReadOnlyList Errors,
- string Status);
-
-internal sealed class GenerationFailureException(string path, Exception cause) : Exception(cause.Message, cause)
-{
- internal ParseError Error { get; } = new(cause.Message, path, 0, 0);
-
- internal static bool IsExpected(Exception error) =>
- error is IOException or InvalidDataException or UnauthorizedAccessException or ArgumentException
- or NotSupportedException or JsonException;
-}
-
-internal static class GenerationEngine
-{
- internal static async Task RunAsync(BuildRequest request, CancellationToken cancellationToken)
- {
- try
- {
- var (plan, errors) = await PrepareAsync(request, cancellationToken);
- if (plan == null)
- {
- return new GenerationResult([], [], errors, "");
- }
-
- return await plan.ApplyAsync(cancellationToken);
- }
- catch (GenerationFailureException error)
- {
- return new GenerationResult([], [], [error.Error], "");
- }
- }
-
- private static async Task<(GenerationPlan? Plan, IReadOnlyList Errors)> PrepareAsync(
- BuildRequest request, CancellationToken cancellationToken)
- {
- var (previous, identity) = await ReadPreviousGenerationAsync(request, cancellationToken);
- var generatorUnchanged = previous?.GeneratorIdentity == identity;
- var oldEntries = previous?.Entries.ToDictionary(entry => entry.Source, BuildFiles.PathComparer)
- ?? new Dictionary(BuildFiles.PathComparer);
- var outputs = new List();
- var errors = new List();
-
- foreach (var input in request.Inputs)
- {
- cancellationToken.ThrowIfCancellationRequested();
- oldEntries.Remove(input.Source, out var oldEntry);
- var (output, inputErrors) = await PrepareOutputAsync(
- input, request.OutputDirectory, generatorUnchanged ? oldEntry : null, cancellationToken);
- if (output != null)
- {
- outputs.Add(output);
- }
- else
- {
- errors.AddRange(inputErrors);
- }
- }
-
- // Stale-output ownership errors take precedence over collected schema diagnostics.
- var removed = await FindRemovedOutputsAsync(request.OutputDirectory, oldEntries.Values, cancellationToken);
- if (errors.Count != 0)
- {
- return (null, errors);
- }
-
- return (new GenerationPlan(request, identity, outputs.ToArray(), removed), []);
- }
-
- private static async Task<(BuildManifest? Manifest, string GeneratorIdentity)> ReadPreviousGenerationAsync(
- BuildRequest request, CancellationToken cancellationToken)
- {
- var errorPath = request.OutputDirectory;
- try
- {
- BuildFiles.EnsureSafePath(request.OutputDirectory, directory: true);
- errorPath = Path.Combine(request.OutputDirectory, "manifest.json");
- var previous = await BuildManifest.ReadAsync(errorPath, request, cancellationToken);
- var identity = await GeneratorIdentityAsync(cancellationToken);
- return (previous, identity);
- }
- catch (Exception error) when (GenerationFailureException.IsExpected(error))
- {
- throw new GenerationFailureException(errorPath, error);
- }
- }
-
- private static async Task<(PreparedOutput? Output, IReadOnlyList Errors)> PrepareOutputAsync(
- BuildInput input, string outputDirectory, BuildManifestEntry? cachedEntry, CancellationToken cancellationToken)
- {
- var errorPath = outputDirectory;
- try
- {
- var outputPath = BuildFiles.OutputPath(outputDirectory, input.Output);
- errorPath = outputPath;
- var existingOutput = await BuildFiles.ReadOwnedOutputAsync(outputPath, cancellationToken);
- var optionsHash = BuildFiles.HashText(JsonSerializer.Serialize(input));
-
- if (cachedEntry != null
- && await CanReuseOutputAsync(cachedEntry, optionsHash, existingOutput, cancellationToken))
- {
- return (new PreparedOutput(outputPath, cachedEntry, null, Reused: true), []);
- }
-
- errorPath = input.Source;
- var (generated, dependencies) = await GenerateInputAsync(input, cancellationToken);
- if (!generated.Success)
- {
- return (null, generated.Errors);
- }
-
- var content = Encoding.UTF8.GetBytes(generated.Code!);
- var entry = new BuildManifestEntry
- {
- Source = input.Source,
- Output = input.Output,
- OptionsHash = optionsHash,
- OutputHash = BuildFiles.Hash(content),
- Dependencies = dependencies
- };
- var contentChanged = existingOutput == null || !existingOutput.AsSpan().SequenceEqual(content);
-
- return (new PreparedOutput(outputPath, entry, contentChanged ? content : null, Reused: false), []);
- }
- catch (Exception error) when (GenerationFailureException.IsExpected(error))
- {
- throw new GenerationFailureException(errorPath, error);
- }
- }
-
- private static async Task FindRemovedOutputsAsync(
- string outputDirectory, IEnumerable oldEntries, CancellationToken cancellationToken)
- {
- var removed = new List();
- foreach (var entry in oldEntries)
- {
- var path = BuildFiles.OutputPath(outputDirectory, entry.Output);
- try
- {
- if (await BuildFiles.ReadOwnedOutputAsync(path, cancellationToken) != null)
- {
- removed.Add(path);
- }
- }
- catch (Exception error) when (GenerationFailureException.IsExpected(error))
- {
- throw new GenerationFailureException(path, error);
- }
- }
-
- return removed.ToArray();
- }
-
- private static async Task<(CSharpGenerationResult Result, BuildDependency[] Dependencies)> GenerateInputAsync(
- BuildInput input, CancellationToken cancellationToken)
- {
- var dependencies = new Dictionary(BuildFiles.PathComparer);
-
- async Task ReadSourceAsync(string path)
- {
- var bytes = await BuildFiles.ReadIfPresentAsync(path, cancellationToken);
-
- // Missing candidates also matter: a new higher-priority import must invalidate the cache.
- dependencies[path] = bytes == null ? null : BuildFiles.Hash(bytes);
- return bytes == null ? null : BuildFiles.Text(bytes);
- }
-
- ImportResolver resolver = async (importingFile, import) =>
- {
- var importPath = import.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
- var directories = new[] { Path.GetDirectoryName(importingFile)! }.Concat(input.ImportDirectories);
-
- foreach (var directory in directories)
- {
- var candidate = BuildFiles.FullPath(importPath, directory);
- var content = await ReadSourceAsync(candidate);
- if (content != null)
- {
- return (candidate, content);
- }
- }
-
- throw new FileNotFoundException($"Imported file not found: {import}", import);
- };
-
- var source = await ReadSourceAsync(input.Source);
- if (source == null)
- {
- return (new CSharpGenerationResult(null,
- [new ParseError("Bond input file was not found.", input.Source, 0, 0)]), []);
- }
-
- var parsed = await ParserFacade.ParseContentAsync(source, input.Source, resolver);
- cancellationToken.ThrowIfCancellationRequested();
- if (!parsed.Success)
- {
- IReadOnlyList errors = parsed.Errors.Count != 0
- ? parsed.Errors
- : [new ParseError("Parsing produced no schema.", input.Source, 0, 0)];
- return (new CSharpGenerationResult(null, errors), []);
- }
-
- var generated = CSharpGenerator.Generate(parsed.Ast!, input.Source, input.Options);
- if (!generated.Success && generated.Errors.Count == 0)
- {
- generated = new CSharpGenerationResult(null,
- [new ParseError("C# generation produced no output.", input.Source, 0, 0)]);
- }
-
- var dependencyHashes = dependencies
- .OrderBy(pair => pair.Key, StringComparer.Ordinal)
- .Select(pair => new BuildDependency(pair.Key, pair.Value))
- .ToArray();
-
- return (generated, dependencyHashes);
- }
-
- private static async Task CanReuseOutputAsync(BuildManifestEntry entry, string optionsHash,
- byte[]? content, CancellationToken cancellationToken)
- {
- if (content == null || entry.OptionsHash != optionsHash)
- {
- return false;
- }
-
- if (BuildFiles.Hash(content) != entry.OutputHash)
- {
- return false;
- }
-
- return await DependenciesMatchAsync(entry.Dependencies, cancellationToken);
- }
-
- private static async Task DependenciesMatchAsync(IEnumerable dependencies, CancellationToken cancellationToken)
- {
- foreach (var dependency in dependencies)
- {
- var bytes = await BuildFiles.ReadIfPresentAsync(dependency.Path, cancellationToken);
- if ((bytes == null ? null : BuildFiles.Hash(bytes)) != dependency.Hash)
- {
- return false;
- }
- }
-
- return true;
- }
-
- private static async Task GeneratorIdentityAsync(CancellationToken cancellationToken)
- {
- var assemblies = new[]
- {
- typeof(GenerationEngine).Assembly,
- typeof(ParserFacade).Assembly,
- typeof(Antlr4.Runtime.AntlrInputStream).Assembly
- };
- var identities = new List { Environment.Version.ToString() };
-
- foreach (var assembly in assemblies.OrderBy(assembly => assembly.GetName().Name, StringComparer.Ordinal))
- {
- // A reused build node can still hold an older module after files are replaced.
- var hash = BuildFiles.Hash(await File.ReadAllBytesAsync(assembly.Location, cancellationToken));
- identities.Add($"{assembly.GetName().Name}:{assembly.ManifestModule.ModuleVersionId}:{hash}");
- }
-
- return BuildFiles.HashText(string.Join("\n", identities));
- }
-}
diff --git a/Bond.Build/GenerationFiles.cs b/Bond.Build/GenerationFiles.cs
deleted file mode 100644
index 7c7a89b..0000000
--- a/Bond.Build/GenerationFiles.cs
+++ /dev/null
@@ -1,174 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Bond.Parser.CodeGeneration;
-
-namespace BondTools.Build;
-
-internal static class BuildFiles
-{
- internal static readonly StringComparer PathComparer = OperatingSystem.IsWindows()
- ? StringComparer.OrdinalIgnoreCase
- : StringComparer.Ordinal;
-
- internal static string FullPath(string path, string? baseDirectory = null)
- {
- if (string.IsNullOrWhiteSpace(path) || path.Any(char.IsControl))
- {
- throw new InvalidDataException("Bond paths must be nonempty and cannot contain control characters.");
- }
-
- return baseDirectory == null ? Path.GetFullPath(path) : Path.GetFullPath(path, baseDirectory);
- }
-
- internal static bool IsRelativeChild(string path) =>
- !Path.IsPathRooted(path) && path != "." && path != ".."
- && !path.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal)
- && !path.StartsWith(".." + Path.AltDirectorySeparatorChar, StringComparison.Ordinal);
-
- internal static string OutputPath(string root, string relative)
- {
- if (!IsRelativeChild(relative))
- {
- throw new InvalidDataException("A Bond output path escapes its intermediate directory.");
- }
-
- var path = FullPath(relative, root);
- if (!IsRelativeChild(Path.GetRelativePath(root, path)))
- {
- throw new InvalidDataException("A Bond output path escapes its intermediate directory.");
- }
-
- return path;
- }
-
- internal static string Hash(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes));
- internal static string HashText(string text) => Hash(Encoding.UTF8.GetBytes(text));
-
- internal static bool IsHash(string? value) =>
- value is { Length: 64 } && value.All(character => Uri.IsHexDigit(character));
-
- internal static async Task ReadIfPresentAsync(string path, CancellationToken cancellationToken)
- {
- try
- {
- return await File.ReadAllBytesAsync(path, cancellationToken);
- }
- catch (FileNotFoundException)
- {
- return null;
- }
- catch (DirectoryNotFoundException)
- {
- return null;
- }
- }
-
- internal static string Text(byte[] bytes)
- {
- using var stream = new MemoryStream(bytes, writable: false);
- using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
- return reader.ReadToEnd();
- }
-
- internal static void EnsureSafePath(string path, bool directory)
- {
- var current = path;
- while (current != null)
- {
- FileAttributes? attributes = null;
- try
- {
- attributes = File.GetAttributes(current);
- }
- catch (FileNotFoundException)
- {
- }
- catch (DirectoryNotFoundException)
- {
- }
-
- if (attributes is { } existing)
- {
- if ((existing & FileAttributes.ReparsePoint) != 0)
- {
- throw new IOException($"Bond output paths cannot be symbolic links: {current}");
- }
-
- if (((existing & FileAttributes.Directory) != 0) != directory)
- {
- throw new IOException($"Bond output path has the wrong file/directory kind: {current}");
- }
- }
-
- var parent = Path.GetDirectoryName(current);
- if (parent != null && Directory.Exists(parent))
- {
- var name = Path.GetFileName(current);
- foreach (var entry in Directory.EnumerateFileSystemEntries(parent))
- {
- if (string.Equals(Path.GetFileName(entry), name, StringComparison.OrdinalIgnoreCase)
- && !string.Equals(Path.GetFileName(entry), name, StringComparison.Ordinal))
- {
- throw new IOException($"Bond output path collides with an existing entry (case-insensitive): {current}");
- }
- }
- }
-
- current = parent;
- directory = true;
- }
- }
-
- internal static async Task ReadOwnedOutputAsync(string path, CancellationToken cancellationToken)
- {
- EnsureSafePath(path, directory: false);
- var bytes = await ReadIfPresentAsync(path, cancellationToken);
- if (bytes == null)
- {
- return null;
- }
-
- var text = Text(bytes);
- if (text != CSharpGenerator.GeneratedHeader
- && !text.StartsWith(CSharpGenerator.GeneratedHeader + "\n", StringComparison.Ordinal)
- && !text.StartsWith(CSharpGenerator.GeneratedHeader + "\r\n", StringComparison.Ordinal))
- {
- throw new IOException($"Refusing to overwrite or remove a non-generated file: {path}");
- }
-
- return bytes;
- }
-
- internal static async Task WriteAtomicAsync(string path, byte[] bytes, CancellationToken cancellationToken)
- {
- EnsureSafePath(path, directory: false);
- var existing = await ReadIfPresentAsync(path, cancellationToken);
- if (existing != null && existing.AsSpan().SequenceEqual(bytes))
- {
- return;
- }
-
- Directory.CreateDirectory(Path.GetDirectoryName(path)!);
- var temporary = path + "." + Guid.NewGuid().ToString("N") + ".new";
- try
- {
- await using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
- {
- await stream.WriteAsync(bytes, cancellationToken);
- await stream.FlushAsync(cancellationToken);
- }
-
- EnsureSafePath(path, directory: false);
- File.Move(temporary, path, overwrite: true);
- }
- finally
- {
- File.Delete(temporary);
- }
- }
-}
diff --git a/Bond.Build/GenerationManifest.cs b/Bond.Build/GenerationManifest.cs
deleted file mode 100644
index a07e511..0000000
--- a/Bond.Build/GenerationManifest.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace BondTools.Build;
-
-internal sealed record BuildDependency(
- [property: JsonRequired] string Path,
- [property: JsonRequired] string? Hash);
-
-internal sealed record BuildManifestEntry
-{
- public required string Source { get; init; }
- public required string Output { get; init; }
- public required string OptionsHash { get; init; }
- public required string OutputHash { get; init; }
- public required BuildDependency[] Dependencies { get; init; }
-}
-
-internal sealed record BuildManifest
-{
- public required int Version { get; init; }
- public required string ProjectFile { get; init; }
- public required string OutputDirectory { get; init; }
- public required string GeneratorIdentity { get; init; }
- public required string RequestHash { get; init; }
- public required BuildManifestEntry[] Entries { get; init; }
-
- internal static readonly JsonSerializerOptions JsonOptions = new()
- {
- WriteIndented = true,
- UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
- };
-
- internal static async Task ReadAsync(string path, BuildRequest request, CancellationToken cancellationToken)
- {
- BuildFiles.EnsureSafePath(path, directory: false);
- var bytes = await BuildFiles.ReadIfPresentAsync(path, cancellationToken);
- if (bytes == null)
- {
- return null;
- }
-
- try
- {
- var manifest = ValidateIdentity(JsonSerializer.Deserialize(bytes, JsonOptions), request);
- ValidateEntries(manifest.Entries, request);
- return manifest;
- }
- catch (Exception error) when (error is JsonException or InvalidDataException or ArgumentException or NotSupportedException)
- {
- throw new InvalidDataException($"Bond manifest '{path}' is invalid. Run dotnet clean and rebuild, " +
- $"or remove this manifest and rebuild. {error.Message}", error);
- }
- }
-
- private static BuildManifest ValidateIdentity(BuildManifest? manifest, BuildRequest request)
- {
- // JSON can contain explicit nulls even for required, non-nullable properties.
- if (manifest == null || manifest.Version != 1
- || !BuildFiles.PathComparer.Equals(manifest.ProjectFile, request.ProjectFile)
- || !BuildFiles.PathComparer.Equals(manifest.OutputDirectory, request.OutputDirectory)
- || !BuildFiles.IsHash(manifest.GeneratorIdentity) || !BuildFiles.IsHash(manifest.RequestHash)
- || manifest.Entries == null)
- {
- throw new InvalidDataException("Invalid manifest identity or version.");
- }
-
- return manifest;
- }
-
- private static void ValidateEntries(BuildManifestEntry[] entries, BuildRequest request)
- {
- var sources = new HashSet(BuildFiles.PathComparer);
- var outputs = new HashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var entry in entries)
- {
- if (entry == null || !CanonicalPath(entry.Source) || !sources.Add(entry.Source)
- || !string.Equals(Path.GetExtension(entry.Source), ".bond", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidDataException("Invalid manifest output entry.");
- }
-
- if (entry.Output != BuildRequest.OutputFor(request.ProjectFile, entry.Source) || !outputs.Add(entry.Output))
- {
- throw new InvalidDataException("Invalid manifest output entry.");
- }
-
- if (!BuildFiles.IsHash(entry.OptionsHash) || !BuildFiles.IsHash(entry.OutputHash) || entry.Dependencies == null)
- {
- throw new InvalidDataException("Invalid manifest output entry.");
- }
-
- BuildFiles.OutputPath(request.OutputDirectory, entry.Output);
- ValidateDependencies(entry);
- }
- }
-
- private static void ValidateDependencies(BuildManifestEntry entry)
- {
- var paths = new HashSet(BuildFiles.PathComparer);
- foreach (var dependency in entry.Dependencies)
- {
- if (dependency == null || !CanonicalPath(dependency.Path) || !paths.Add(dependency.Path)
- || (dependency.Hash != null && !BuildFiles.IsHash(dependency.Hash)))
- {
- throw new InvalidDataException("Invalid manifest dependency.");
- }
- }
-
- if (!entry.Dependencies.Any(dependency =>
- BuildFiles.PathComparer.Equals(dependency.Path, entry.Source) && dependency.Hash != null))
- {
- throw new InvalidDataException("Manifest does not include the root input hash.");
- }
- }
-
- private static bool CanonicalPath(string? path) =>
- !string.IsNullOrWhiteSpace(path) && Path.IsPathFullyQualified(path)
- && string.Equals(path, BuildFiles.FullPath(path), StringComparison.Ordinal);
-}
diff --git a/Bond.Build/GenerationPlan.cs b/Bond.Build/GenerationPlan.cs
deleted file mode 100644
index 88ae296..0000000
--- a/Bond.Build/GenerationPlan.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace BondTools.Build;
-
-internal sealed record PreparedOutput(string Path, BuildManifestEntry Entry, byte[]? NewContent, bool Reused);
-
-internal sealed record GenerationPlan(
- BuildRequest Request,
- string GeneratorIdentity,
- PreparedOutput[] Outputs,
- string[] RemovedOutputs)
-{
- internal async Task ApplyAsync(CancellationToken cancellationToken)
- {
- foreach (var output in Outputs)
- {
- if (output.NewContent == null)
- {
- continue;
- }
-
- try
- {
- // Recheck ownership in case a file changed after preparation.
- await BuildFiles.ReadOwnedOutputAsync(output.Path, cancellationToken);
- await BuildFiles.WriteAtomicAsync(output.Path, output.NewContent, cancellationToken);
- }
- catch (Exception error) when (GenerationFailureException.IsExpected(error))
- {
- throw new GenerationFailureException(output.Path, error);
- }
- }
-
- foreach (var path in RemovedOutputs)
- {
- try
- {
- await BuildFiles.ReadOwnedOutputAsync(path, cancellationToken);
- File.Delete(path);
- }
- catch (Exception error) when (GenerationFailureException.IsExpected(error))
- {
- throw new GenerationFailureException(path, error);
- }
- }
-
- var manifestPath = Path.Combine(Request.OutputDirectory, "manifest.json");
- await SaveManifestAsync(manifestPath, cancellationToken);
-
- var skippedCount = Outputs.Count(output => output.Reused);
- var generatedCount = Outputs.Length - skippedCount;
- var status = generatedCount == 0 && RemovedOutputs.Length == 0
- ? $"Bond: up-to-date ({skippedCount} schemas)."
- : $"Bond: generated {generatedCount}, skipped {skippedCount}, removed {RemovedOutputs.Length}.";
-
- // MSBuild needs every compile and clean item, including outputs reused without a write.
- var generatedFiles = Outputs.Select(output => output.Path).ToArray();
- return new GenerationResult(generatedFiles, generatedFiles.Append(manifestPath).ToArray(), [], status);
- }
-
- private async Task SaveManifestAsync(string path, CancellationToken cancellationToken)
- {
- try
- {
- var manifest = new BuildManifest
- {
- Version = 1,
- ProjectFile = Request.ProjectFile,
- OutputDirectory = Request.OutputDirectory,
- GeneratorIdentity = GeneratorIdentity,
- RequestHash = BuildFiles.HashText(JsonSerializer.Serialize(Request)),
- Entries = Outputs.Select(output => output.Entry).ToArray()
- };
- await BuildFiles.WriteAtomicAsync(path,
- JsonSerializer.SerializeToUtf8Bytes(manifest, BuildManifest.JsonOptions), cancellationToken);
- }
- catch (Exception error) when (GenerationFailureException.IsExpected(error))
- {
- throw new GenerationFailureException(path, error);
- }
- }
-}
diff --git a/Bond.Build/GenerationRequest.cs b/Bond.Build/GenerationRequest.cs
deleted file mode 100644
index 6e16927..0000000
--- a/Bond.Build/GenerationRequest.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using Bond.Parser.CodeGeneration;
-using Microsoft.Build.Framework;
-
-namespace BondTools.Build;
-
-internal sealed record BuildInput(
- string Source,
- string Output,
- string[] ImportDirectories,
- CSharpGenerationOptions Options);
-
-internal sealed record BuildRequest(string ProjectFile, string OutputDirectory, BuildInput[] Inputs)
-{
- internal static BuildRequest Create(string projectFile, string outputDirectory, ITaskItem[] sources,
- ITaskItem[] importDirectories, ITaskItem[] usings, ITaskItem[] namespaceMappings, ITaskItem[] typeMappings)
- {
- var project = BuildFiles.FullPath(projectFile);
- var projectDirectory = Path.GetDirectoryName(project)!;
- outputDirectory = Path.TrimEndingDirectorySeparator(BuildFiles.FullPath(outputDirectory, projectDirectory));
- if (outputDirectory == Path.GetPathRoot(outputDirectory) || BuildFiles.PathComparer.Equals(outputDirectory, projectDirectory))
- {
- throw new InvalidDataException("Bond outputs must use a dedicated intermediate directory.");
- }
-
- var inputs = new List();
- var sourcePaths = new HashSet(BuildFiles.PathComparer);
- var outputs = new HashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var item in sources)
- {
- var source = BuildFiles.FullPath(item.ItemSpec, projectDirectory);
- if (!string.Equals(Path.GetExtension(source), ".bond", StringComparison.OrdinalIgnoreCase)
- || string.IsNullOrEmpty(Path.GetFileNameWithoutExtension(source)))
- {
- throw new InvalidDataException($"Input must be a named .bond file: {source}");
- }
-
- if (!sourcePaths.Add(source))
- {
- throw new InvalidDataException($"Bond input is included more than once: {source}");
- }
-
- var output = OutputFor(project, source);
- if (!outputs.Add(output))
- {
- throw new InvalidDataException($"Bond output paths collide (case-insensitive): {output}");
- }
-
- var features = ReadFeatures(item, source);
- var imports = CombinedMetadata(item, "ImportDirectories", importDirectories)
- .Select(value => BuildFiles.FullPath(value, projectDirectory))
- .ToArray();
- var options = new CSharpGenerationOptions
- {
- UsingNamespaces = CombinedMetadata(item, "Usings", usings),
- NamespaceMappings = CombinedMetadata(item, "NamespaceMappings", namespaceMappings),
- TypeMappings = CombinedMetadata(item, "TypeMappings", typeMappings),
- ModelFeatures = features
- };
-
- inputs.Add(new BuildInput(source, output, imports, options));
- }
-
- return new BuildRequest(project, outputDirectory, inputs.ToArray());
- }
-
- private static CSharpModelFeatures ReadFeatures(ITaskItem item, string source)
- {
- var features = CSharpModelFeatures.None;
- var flags = new[]
- {
- ("Descriptors", CSharpModelFeatures.Descriptors),
- ("Clone", CSharpModelFeatures.Cloning),
- ("Equality", CSharpModelFeatures.Equality),
- ("Debugger", CSharpModelFeatures.Debugger),
- ("ToString", CSharpModelFeatures.StringRepresentation)
- };
-
- foreach (var (name, flag) in flags)
- {
- var value = item.GetMetadata(name);
- if (!bool.TryParse(value, out var enabled))
- {
- throw new InvalidDataException($"Bond {name} for '{source}' must be true or false, not '{value}'.");
- }
-
- if (enabled)
- {
- features |= flag;
- }
- }
-
- return features;
- }
-
- private static string[] CombinedMetadata(ITaskItem item, string name, ITaskItem[] shared)
- {
- var local = item.GetMetadata(name).Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- return shared.Select(option => option.ItemSpec.Trim()).Concat(local).ToArray();
- }
-
- internal static string OutputFor(string projectFile, string source)
- {
- var relative = Path.GetRelativePath(Path.GetDirectoryName(projectFile)!, source);
- var directory = BuildFiles.IsRelativeChild(relative)
- ? Path.Combine("sources", BuildFiles.HashText(relative.Replace(Path.DirectorySeparatorChar, '/')))
- : Path.Combine("external", BuildFiles.HashText(source));
- return Path.Combine(directory, Path.GetFileNameWithoutExtension(source) + ".g.cs");
- }
-}
diff --git a/Bond.Build/README.md b/Bond.Build/README.md
index 1103bcd..c72933c 100644
--- a/Bond.Build/README.md
+++ b/Bond.Build/README.md
@@ -1,11 +1,23 @@
# BondTools.Build
-In-process generation with .NET 8+ SDK MSBuild.
+Generates C# for `Bond` items during `dotnet build` (.NET 8+ SDK).
```xml
+
+
+ true
+ true
+ true
+ Schemas/Shared
+ System.Collections.Generic
+ Contracts=MyApp.Contracts
+ Contracts.Timestamp=System.DateTime
+
```
+
+Lists are separated by `;`. Generated files live in the intermediate output directory.
diff --git a/Bond.Build/build/BondTools.Build.props b/Bond.Build/build/BondTools.Build.props
deleted file mode 100644
index 0c7d52a..0000000
--- a/Bond.Build/build/BondTools.Build.props
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- false
- false
- false
- false
- false
-
-
diff --git a/Bond.Build/build/BondTools.Build.targets b/Bond.Build/build/BondTools.Build.targets
index 326307d..727e51b 100644
--- a/Bond.Build/build/BondTools.Build.targets
+++ b/Bond.Build/build/BondTools.Build.targets
@@ -1,48 +1,36 @@
-
- true
-
-
- $(BondDescriptors)
+ $(BondImportDirectories)
+ $(BondUsings)
+ $(BondNamespaceMappings)
+ $(BondTypeMappings)
$(BondClone)
$(BondEquality)
- $(BondDebugger)
$(BondToString)
-
+
+
+
+
+
+
- <_BondOutputDirectory>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', 'bond'))
- <_BondUnsupportedSdk Condition="'$(NETCoreSdkVersion)' != ''">$([MSBuild]::VersionLessThan('$(NETCoreSdkVersion)', '8.0'))
+ <_BondOutputDirectory>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', 'bond'))
-
+
-
-
-
+
+
+
-
-
-
-
-
diff --git a/Bond.Models/Bond.Models.csproj b/Bond.Models/Bond.Models.csproj
deleted file mode 100644
index 4b58dbd..0000000
--- a/Bond.Models/Bond.Models.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
- net8.0
- enable
- true
- true
- BondTools.Models
- Bond Generated Model Support
- BondTools.Models
- Mirco De Zorzi
- Support for generated Bond C# descriptors, cloning, equality, and debugger views. This package does not provide serialization.
- bond;codegen;models;metadata
- README.md
- LICENSE
-
-
-
-
-
-
diff --git a/Bond.Models/ModelAdapterArguments.cs b/Bond.Models/ModelAdapterArguments.cs
deleted file mode 100644
index 4e0a78e..0000000
--- a/Bond.Models/ModelAdapterArguments.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace BondTools.Models;
-
-/// Statically supplied operations for a closed generic model argument.
-public sealed class ModelAdapterArgument
-{
- private ModelAdapterArgument(Type type, object adapter)
- {
- Type = type;
- Adapter = adapter;
- }
-
- internal Type Type { get; }
- internal object Adapter { get; }
-
- /// Supplies an argument's operations without inspecting its CLR members.
- public static ModelAdapterArgument Create(IModelAdapter adapter)
- {
- ArgumentNullException.ThrowIfNull(adapter);
- return new(typeof(T), adapter);
- }
-}
-
-internal sealed class ModelAdapterFrame(IReadOnlyList arguments, ModelAdapterFrame? parent)
-{
- internal ModelBindingSemantics? Semantics { get; } = GetSemantics(arguments, parent);
-
- internal IModelAdapter? Find()
- {
- foreach (var argument in arguments)
- {
- if (argument.Type == typeof(T))
- {
- return (IModelAdapter)argument.Adapter;
- }
- }
-
- return parent?.Find();
- }
-
- private static ModelBindingSemantics? GetSemantics(
- IReadOnlyList arguments, ModelAdapterFrame? parent)
- {
- var bindings = parent?.Semantics is { } inherited
- ? new Dictionary(inherited.Bindings)
- : new Dictionary();
- var seen = new HashSet();
- foreach (var argument in arguments)
- {
- if (!seen.Add(argument.Type))
- {
- continue;
- }
-
- // Standard generated bindings do not change semantics; masks also remove outer overrides.
- if (ModelAdapterSemantics.ForBinding(argument.Adapter) is { } semantics)
- {
- bindings[argument.Type] = semantics;
- }
- else
- {
- bindings.Remove(argument.Type);
- }
- }
-
- return bindings.Count == 0 ? null : new ModelBindingSemantics(bindings);
- }
-}
-
-internal sealed class ArgumentModelAdapter : IModelAdapter, IModelAdapterSemantics
-{
- private readonly IModelAdapter _inner;
- private readonly ModelAdapterArgument[] _arguments;
-
- public object? Semantics { get; }
-
- internal ArgumentModelAdapter(IModelAdapter inner, ModelAdapterArgument[] arguments)
- {
- _inner = inner;
-
- // Mask an outer registration for T before dispatching to the underlying adapter.
- _arguments = new[] { ModelAdapterArgument.Create(inner) }.Concat(arguments).ToArray();
- Semantics = ModelAdapterSemantics.Compose(typeof(ArgumentModelAdapter),
- ModelAdapterSemantics.For(inner), new ModelAdapterFrame(_arguments, null).Semantics);
- }
-
- public T Clone(T value, CloneContext context)
- {
- using var scope = context.Traversal.PushBindings(_arguments);
- return ModelAdapterSemantics.Clone(_inner, value, context);
- }
-
- public bool Equals(T left, T right, EqualityContext context)
- {
- using var scope = context.Traversal.PushBindings(_arguments);
- return ModelAdapterSemantics.Equals(_inner, left, right, context);
- }
-
- public int GetHashCode(T value, HashContext context)
- {
- using var scope = context.Traversal.PushBindings(_arguments);
- return ModelAdapterSemantics.Hash(_inner, value, context);
- }
-}
-
-internal sealed class CapturedModelAdapter(IModelAdapter inner, ModelAdapterFrame bindings)
- : IModelAdapter, IModelAdapterSemantics
-{
- public object? Semantics { get; } =
- ModelAdapterSemantics.Compose(typeof(CapturedModelAdapter), ModelAdapterSemantics.For(inner), bindings.Semantics);
-
- public T Clone(T value, CloneContext context)
- {
- using var scope = context.Traversal.ReplaceBindings(bindings);
- return ModelAdapterSemantics.Clone(inner, value, context);
- }
-
- public bool Equals(T left, T right, EqualityContext context)
- {
- using var scope = context.Traversal.ReplaceBindings(bindings);
- return ModelAdapterSemantics.Equals(inner, left, right, context);
- }
-
- public int GetHashCode(T value, HashContext context)
- {
- using var scope = context.Traversal.ReplaceBindings(bindings);
- return ModelAdapterSemantics.Hash(inner, value, context);
- }
-}
diff --git a/Bond.Models/ModelAdapterSemantics.cs b/Bond.Models/ModelAdapterSemantics.cs
deleted file mode 100644
index e3e8ef2..0000000
--- a/Bond.Models/ModelAdapterSemantics.cs
+++ /dev/null
@@ -1,149 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-
-namespace BondTools.Models;
-
-internal interface IModelAdapterSemantics
-{
- object? Semantics { get; }
-}
-
-internal static class ModelAdapterSemantics
-{
- internal static object? For(object adapter) =>
- adapter is IModelAdapterSemantics known ? known.Semantics : new Identity(adapter);
-
- internal static object? ForBinding(object adapter) => NormalizeBinding(For(adapter));
-
- // Standard generated paths share one graph scope; custom components form stable structural keys.
- internal static object? Compose(Type kind, object? first, object? second = null) =>
- first is null && second is null ? null : new Composition(kind, first, second);
-
- internal static object Projection(Type kind, object? payload, bool defaultBinding) =>
- new Composition(kind, new ProjectionKind(kind, defaultBinding), payload);
-
- private static object? NormalizeBinding(object? semantics) => semantics switch
- {
- ProjectionKind { DefaultBinding: true } => null,
- Composition composition =>
- Compose(composition.Kind, NormalizeBinding(composition.First), NormalizeBinding(composition.Second)),
- _ => semantics
- };
-
- internal static T Clone(IModelAdapter adapter, T value, CloneContext context)
- {
- using var scope = context.Traversal.Enter(For(adapter));
- return adapter.Clone(value, context);
- }
-
- internal static bool Equals(IModelAdapter adapter, T left, T right, EqualityContext context)
- {
- using var scope = context.Traversal.Enter(For(adapter));
- return adapter.Equals(left, right, context);
- }
-
- internal static int Hash(IModelAdapter adapter, T value, HashContext context)
- {
- using var scope = context.Traversal.Enter(For(adapter));
- return adapter.GetHashCode(value, context);
- }
-
- private readonly record struct Composition(Type Kind, object? First, object? Second);
- private readonly record struct ProjectionKind(Type Kind, bool DefaultBinding);
-
- private readonly struct Identity(object value) : IEquatable
- {
- public bool Equals(Identity other) => ReferenceEquals(value, other.Value);
- public override bool Equals(object? other) => other is Identity identity && Equals(identity);
- public override int GetHashCode() => RuntimeHelpers.GetHashCode(value);
- private object Value => value;
- }
-}
-
-internal sealed class ModelTraversal
-{
- private object? _adapter;
-
- internal ModelAdapterFrame? Adapters { get; private set; }
- internal ModelOperationSemantics Semantics => new(_adapter, Adapters?.Semantics);
-
- internal ModelTraversal Fork() => new() { _adapter = _adapter, Adapters = Adapters };
-
- internal AdapterScope Enter(object? adapter)
- {
- var scope = new AdapterScope(this, _adapter);
- _adapter = adapter;
- return scope;
- }
-
- internal BindingScope PushBindings(IReadOnlyList arguments) =>
- ReplaceBindings(new ModelAdapterFrame(arguments, Adapters));
-
- internal BindingScope ReplaceBindings(ModelAdapterFrame bindings)
- {
- var scope = new BindingScope(this, Adapters);
- Adapters = bindings;
- return scope;
- }
-
- internal readonly struct AdapterScope(ModelTraversal traversal, object? previous) : IDisposable
- {
- public void Dispose() => traversal._adapter = previous;
- }
-
- internal readonly struct BindingScope(ModelTraversal traversal, ModelAdapterFrame? previous) : IDisposable
- {
- public void Dispose() => traversal.Adapters = previous;
- }
-}
-
-internal readonly record struct ModelOperationSemantics(object? Adapter, ModelBindingSemantics? Bindings);
-
-internal sealed class ModelBindingSemantics(Dictionary bindings) : IEquatable
-{
- internal IReadOnlyDictionary Bindings => bindings;
-
- public bool Equals(ModelBindingSemantics? other)
- {
- if (other is null || bindings.Count != other.Bindings.Count)
- {
- return false;
- }
-
- foreach (var binding in bindings)
- {
- if (!other.Bindings.TryGetValue(binding.Key, out var value) || !binding.Value.Equals(value))
- {
- return false;
- }
- }
-
- return true;
- }
-
- public override bool Equals(object? other) => other is ModelBindingSemantics semantics && Equals(semantics);
-
- public override int GetHashCode()
- {
- var hash = 0;
- foreach (var binding in bindings)
- {
- hash = unchecked(hash + HashCode.Combine(binding.Key, binding.Value));
- }
-
- return hash;
- }
-}
-
-internal readonly struct ModelReferenceKey(object source, ModelOperationSemantics semantics) : IEquatable
-{
- private object Source => source;
- private ModelOperationSemantics Semantics => semantics;
-
- public bool Equals(ModelReferenceKey other) =>
- ReferenceEquals(source, other.Source) && semantics.Equals(other.Semantics);
-
- public override bool Equals(object? other) => other is ModelReferenceKey key && Equals(key);
- public override int GetHashCode() => HashCode.Combine(RuntimeHelpers.GetHashCode(source), semantics);
-}
diff --git a/Bond.Models/ModelAdapters.cs b/Bond.Models/ModelAdapters.cs
deleted file mode 100644
index 2a206a1..0000000
--- a/Bond.Models/ModelAdapters.cs
+++ /dev/null
@@ -1,545 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace BondTools.Models;
-
-/// Typed container combinators used by generated code; no runtime type discovery is performed.
-public static class ModelAdapters
-{
- /// Dispatches to generated operations, known immutable values, or an explicitly registered adapter.
- public static IModelAdapter Value() => ValueAdapter.Instance;
-
- /// Uses .NET equality and identity cloning for a type explicitly known to be immutable.
- public static IModelAdapter Immutable() => ImmutableAdapter.Instance;
-
- /// Supplies statically known generic argument operations for one model traversal.
- public static IModelAdapter WithArguments(IModelAdapter value, params ModelAdapterArgument[] arguments)
- {
- ArgumentNullException.ThrowIfNull(value);
- ArgumentNullException.ThrowIfNull(arguments);
- return new ArgumentModelAdapter(value, arguments);
- }
-
- /// Preserves nullable value semantics.
- public static IModelAdapter Nullable(IModelAdapter element) where T : struct =>
- new NullableAdapter(element);
-
- /// Clones a vector and compares its elements in order.
- public static IModelAdapter> List(IModelAdapter element) =>
- new SequenceAdapter, T>(element, value => new(value.Count), (list, item) => list.Add(item), unordered: false);
-
- /// Clones a linked list and compares its elements in order.
- public static IModelAdapter> LinkedList(IModelAdapter element) =>
- new SequenceAdapter, T>(element, _ => new(), (list, item) => list.AddLast(item), unordered: false);
-
- /// Clones a set, preserving its comparer; structural equality and hashing ignore insertion order.
- public static IModelAdapter> Set(IModelAdapter element) =>
- new SequenceAdapter, T>(element, value => new(value.Comparer), (set, item) => set.Add(item), unordered: true);
-
- /// Clones a map, preserving its comparer; structural equality and hashing ignore insertion order.
- public static IModelAdapter> Map(
- IModelAdapter key, IModelAdapter value) where TKey : notnull =>
- new MapAdapter(key, value);
-
- /// Copies owned backing storage, preserving shared buffers and segment offsets.
- public static IModelAdapter> Blob { get; } = new BlobAdapter();
-
- /// Copies byte-array storage and compares byte contents.
- public static IModelAdapter ByteArray { get; } = new ByteArrayAdapter();
-
- ///
- /// Compares and clones actual lazy payload values. Reading a payload crosses into the original runtime,
- /// which may use reflection or deserialize. Each wrapper is materialized at most once per operation.
- /// Covariant views share cloned payloads but may receive separate typed wrappers.
- ///
- public static IModelAdapter Materialized(
- Func read, Func wrap, IModelAdapter value)
- where TWrapper : class =>
- new MaterializedAdapter(read, (payload, _) => wrap(payload), value, defaultBinding: false);
-
- ///
- /// Clones lazy payloads and supplies their captured typed operations to the wrapper factory.
- /// The supplied adapter retains argument bindings, not graph caches, and can be used with fresh contexts.
- /// The factory must retain the supplied payload and its value semantics, as generated bonded wrappers do.
- ///
- public static IModelAdapter Materialized(
- Func read, Func, TWrapper> wrap, IModelAdapter value)
- where TWrapper : class =>
- new MaterializedAdapter(read, wrap, value, defaultBinding: true);
-
- internal static object? EffectiveSemantics(IModelAdapter adapter, ModelAdapterFrame? arguments)
- {
- if (adapter is ValueAdapter dispatcher)
- {
- var selected = dispatcher.ResolveOverride(arguments);
- return selected is null ? null : ModelAdapterSemantics.For(selected);
- }
-
- return ModelAdapterSemantics.For(adapter);
- }
-
- private sealed class ValueAdapter : IModelAdapter, IModelAdapterSemantics
- {
- internal static readonly ValueAdapter Instance = new();
- public object? Semantics => null;
-
- internal IModelAdapter? ResolveOverride(ModelAdapterFrame? bindings)
- {
- if (ModelOperations.Registered() is { } registered)
- {
- return registered;
- }
-
- var contextual = bindings?.Find();
- return ReferenceEquals(contextual, this) ? null : contextual;
- }
-
- public T Clone(T value, CloneContext context)
- {
- using var scope = context.Traversal.Enter(null);
- if (value is null)
- {
- return value;
- }
-
- if (ResolveOverride(context.Adapters) is { } adapter)
- {
- return ModelAdapterSemantics.Clone(adapter, value, context);
- }
-
- if (value is IGeneratedCloneable model)
- {
- return (T)model.Clone(context);
- }
-
- if (value is byte[] bytes)
- {
- return (T)(object)ByteArray.Clone(bytes, context);
- }
-
- if (value is ArraySegment blob)
- {
- return (T)(object)Blob.Clone(blob, context);
- }
-
- if (ModelOperations.IsKnownImmutable(value))
- {
- return value;
- }
-
- throw ModelOperations.MissingAdapter(typeof(T));
- }
-
- public bool Equals(T left, T right, EqualityContext context)
- {
- using var scope = context.Traversal.Enter(null);
- if (ReferenceEquals(left, right))
- {
- return true;
- }
-
- if (left is null || right is null)
- {
- return false;
- }
-
- if (ResolveOverride(context.Adapters) is { } adapter)
- {
- return ModelAdapterSemantics.Equals(adapter, left, right, context);
- }
-
- if (left is IGeneratedEquatable model)
- {
- return right is IGeneratedEquatable other && left.GetType() == right.GetType() &&
- context.CompareReferences(left, right, () => model.ValueEquals(other, context));
- }
-
- if (left is byte[] bytes)
- {
- return right is byte[] otherBytes && ByteArray.Equals(bytes, otherBytes, context);
- }
-
- if (left is ArraySegment blob)
- {
- return right is ArraySegment otherBlob && Blob.Equals(blob, otherBlob, context);
- }
-
- if (ModelOperations.IsKnownImmutable(left))
- {
- return EqualityComparer.Default.Equals(left, right);
- }
-
- throw ModelOperations.MissingAdapter(typeof(T));
- }
-
- public int GetHashCode(T value, HashContext context)
- {
- using var scope = context.Traversal.Enter(null);
- if (value is null || context.RemainingDepth == 0)
- {
- return 0;
- }
-
- if (ResolveOverride(context.Adapters) is { } adapter)
- {
- return ModelAdapterSemantics.Hash(adapter, value, context);
- }
-
- if (value is IGeneratedEquatable model)
- {
- return context.HashReference(model, () => model.ValueHashCode(context));
- }
-
- if (value is byte[] bytes)
- {
- return ByteArray.GetHashCode(bytes, context);
- }
-
- if (value is ArraySegment blob)
- {
- return Blob.GetHashCode(blob, context);
- }
-
- if (ModelOperations.IsKnownImmutable(value))
- {
- return EqualityComparer.Default.GetHashCode(value);
- }
-
- throw ModelOperations.MissingAdapter(typeof(T));
- }
- }
-
- private sealed class ImmutableAdapter : IModelAdapter, IModelAdapterSemantics
- {
- internal static readonly ImmutableAdapter Instance = new();
- public object Semantics => typeof(ImmutableAdapter);
-
- public T Clone(T value, CloneContext context) => value;
-
- public bool Equals(T left, T right, EqualityContext context) => EqualityComparer.Default.Equals(left, right);
-
- public int GetHashCode(T value, HashContext context) =>
- value is null || context.RemainingDepth == 0 ? 0 : EqualityComparer.Default.GetHashCode(value);
- }
-
- private sealed class NullableAdapter(IModelAdapter element) : IModelAdapter, IModelAdapterSemantics
- where T : struct
- {
- public object? Semantics { get; } =
- ModelAdapterSemantics.Compose(typeof(NullableAdapter), ModelAdapterSemantics.For(element));
-
- public T? Clone(T? value, CloneContext context) =>
- value.HasValue ? ModelAdapterSemantics.Clone(element, value.Value, context) : null;
-
- public bool Equals(T? left, T? right, EqualityContext context) =>
- left.HasValue == right.HasValue &&
- (!left.HasValue || ModelAdapterSemantics.Equals(element, left.Value, right!.Value, context));
-
- public int GetHashCode(T? value, HashContext context) =>
- !value.HasValue || context.RemainingDepth == 0 ? 0 : ModelAdapterSemantics.Hash(element, value.Value, context);
- }
-
- private sealed class SequenceAdapter(
- IModelAdapter element,
- Func create,
- Action add,
- bool unordered) : IModelAdapter, IModelAdapterSemantics where TCollection : class, ICollection
- {
- public object? Semantics { get; } =
- ModelAdapterSemantics.Compose(typeof(SequenceAdapter), ModelAdapterSemantics.For(element));
-
- public TCollection Clone(TCollection value, CloneContext context)
- {
- using var scope = context.Traversal.Enter(Semantics);
- if (value is null)
- {
- return null!;
- }
-
- if (context.TryGetClone(value, out var existing))
- {
- return existing;
- }
-
- var clone = create(value);
- context.Register(value, clone);
- foreach (var item in value)
- {
- add(clone, ModelAdapterSemantics.Clone(element, item, context));
- }
-
- return clone;
- }
-
- public bool Equals(TCollection left, TCollection right, EqualityContext context)
- {
- using var scope = context.Traversal.Enter(Semantics);
- return context.CompareReferences(left, right, () =>
- {
- if (left.Count != right.Count)
- {
- return false;
- }
-
- if (unordered)
- {
- return UnorderedEquals(left, right, context,
- (a, b, branch) => ModelAdapterSemantics.Equals(element, a, b, branch));
- }
-
- using var leftItems = left.GetEnumerator();
- using var rightItems = right.GetEnumerator();
- while (leftItems.MoveNext())
- {
- if (!rightItems.MoveNext() ||
- !ModelAdapterSemantics.Equals(element, leftItems.Current, rightItems.Current, context))
- {
- return false;
- }
- }
-
- return !rightItems.MoveNext();
- });
- }
-
- public int GetHashCode(TCollection value, HashContext context)
- {
- using var scope = context.Traversal.Enter(Semantics);
- if (value is null || context.RemainingDepth == 0)
- {
- return 0;
- }
-
- return context.HashReference(value, () =>
- {
- var hash = 17;
- var child = context.Descend();
- foreach (var item in value)
- {
- var itemHash = ModelAdapterSemantics.Hash(element, item, child);
- hash = unordered ? unchecked(hash + itemHash) : HashContext.Combine(hash, itemHash);
- }
-
- return HashContext.Combine(hash, value.Count);
- });
- }
- }
-
- private sealed class MapAdapter(IModelAdapter key, IModelAdapter value)
- : IModelAdapter>, IModelAdapterSemantics where TKey : notnull
- {
- public object? Semantics { get; } = ModelAdapterSemantics.Compose(typeof(MapAdapter),
- ModelAdapterSemantics.For(key), ModelAdapterSemantics.For(value));
-
- public Dictionary Clone(Dictionary source, CloneContext context)
- {
- using var scope = context.Traversal.Enter(Semantics);
- if (source is null)
- {
- return null!;
- }
-
- if (context.TryGetClone>(source, out var existing))
- {
- return existing;
- }
-
- var clone = new Dictionary(source.Count, source.Comparer);
- context.Register(source, clone);
- foreach (var item in source)
- {
- clone.Add(ModelAdapterSemantics.Clone(key, item.Key, context),
- ModelAdapterSemantics.Clone(value, item.Value, context));
- }
-
- return clone;
- }
-
- public bool Equals(Dictionary left, Dictionary right, EqualityContext context)
- {
- using var scope = context.Traversal.Enter(Semantics);
- return context.CompareReferences(left, right, () =>
- {
- if (left.Count != right.Count)
- {
- return false;
- }
-
- return UnorderedEquals(left, right, context, (leftEntry, rightEntry, branch) =>
- ModelAdapterSemantics.Equals(key, leftEntry.Key, rightEntry.Key, branch) &&
- ModelAdapterSemantics.Equals(value, leftEntry.Value, rightEntry.Value, branch));
- });
- }
-
- public int GetHashCode(Dictionary source, HashContext context)
- {
- using var scope = context.Traversal.Enter(Semantics);
- if (source is null || context.RemainingDepth == 0)
- {
- return 0;
- }
-
- return context.HashReference(source, () =>
- {
- var hash = 17;
- var child = context.Descend();
- foreach (var item in source)
- {
- var entryHash = HashContext.Combine(
- ModelAdapterSemantics.Hash(key, item.Key, child), ModelAdapterSemantics.Hash(value, item.Value, child));
- hash = unchecked(hash + entryHash);
- }
-
- return HashContext.Combine(hash, source.Count);
- });
- }
- }
-
- private sealed class ByteArrayAdapter : IModelAdapter, IModelAdapterSemantics
- {
- public object? Semantics => null;
-
- public byte[] Clone(byte[] value, CloneContext context)
- {
- using var scope = context.Traversal.Enter(null);
- if (value is null)
- {
- return null!;
- }
-
- if (context.TryGetClone(value, out var clone))
- {
- return clone;
- }
-
- clone = (byte[])value.Clone();
- context.Register(value, clone);
- return clone;
- }
-
- public bool Equals(byte[] left, byte[] right, EqualityContext context) =>
- ReferenceEquals(left, right) || left is not null && right is not null && left.AsSpan().SequenceEqual(right);
-
- public int GetHashCode(byte[] value, HashContext context) =>
- value is null ? 0 : Blob.GetHashCode(new ArraySegment(value), context);
- }
-
- private sealed class BlobAdapter : IModelAdapter>, IModelAdapterSemantics
- {
- public object? Semantics => null;
-
- public ArraySegment Clone(ArraySegment value, CloneContext context)
- {
- if (value.Array is null)
- {
- return default;
- }
-
- var copy = ByteArray.Clone(value.Array, context);
- return new(copy, value.Offset, value.Count);
- }
-
- public bool Equals(ArraySegment left, ArraySegment right, EqualityContext context) =>
- (left.Array is null) == (right.Array is null) && left.AsSpan().SequenceEqual(right.AsSpan());
-
- public int GetHashCode(ArraySegment value, HashContext context)
- {
- if (value.Array is null || context.RemainingDepth == 0)
- {
- return 0;
- }
-
- var hash = 17;
- foreach (var item in value.AsSpan())
- {
- hash = HashContext.Combine(hash, item);
- }
-
- return hash;
- }
- }
-
- private sealed class MaterializedAdapter(
- Func read, Func, TWrapper> wrap,
- IModelAdapter value, bool defaultBinding)
- : IModelAdapter, IModelAdapterSemantics where TWrapper : class
- {
- public object Semantics { get; } =
- ModelAdapterSemantics.Projection(
- typeof(MaterializedAdapter), ModelAdapterSemantics.For(value), defaultBinding);
-
- public TWrapper Clone(TWrapper source, CloneContext context)
- {
- using var scope = context.Traversal.Enter(SemanticsFor(context.Adapters));
- return context.CloneMaterialized(source, read, wrap, value);
- }
-
- public bool Equals(TWrapper left, TWrapper right, EqualityContext context)
- {
- using var scope = context.Traversal.Enter(SemanticsFor(context.Adapters));
- return context.CompareReferences(left, right, () =>
- {
- var leftPayload = context.Materialize(left, () => read(left));
- var rightPayload = context.Materialize(right, () => read(right));
- return ModelAdapterSemantics.Equals(value, leftPayload, rightPayload, context);
- });
- }
-
- public int GetHashCode(TWrapper source, HashContext context)
- {
- using var scope = context.Traversal.Enter(SemanticsFor(context.Adapters));
- if (source is null || context.RemainingDepth == 0)
- {
- return 0;
- }
-
- return context.HashReference(source, () =>
- {
- var payload = context.Materialize(source, () => read(source));
- return ModelAdapterSemantics.Hash(value, payload, context.Descend());
- });
- }
-
- private object SemanticsFor(ModelAdapterFrame? arguments) =>
- ModelAdapterSemantics.Projection(
- typeof(MaterializedAdapter), EffectiveSemantics(value, arguments), defaultBinding);
- }
-
- private static bool UnorderedEquals(
- IEnumerable left, IEnumerable right, EqualityContext context,
- Func equals)
- {
- var candidates = right.ToArray();
- var matched = new bool[candidates.Length];
- foreach (var item in left)
- {
- var found = false;
- for (var i = 0; i < candidates.Length; i++)
- {
- if (matched[i])
- {
- continue;
- }
-
- var branch = context.Fork();
- if (!equals(item, candidates[i], branch))
- {
- continue;
- }
-
- matched[i] = true;
- context.Accept(branch);
- found = true;
- break;
- }
-
- if (!found)
- {
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/Bond.Models/ModelContexts.cs b/Bond.Models/ModelContexts.cs
deleted file mode 100644
index 0f5dbc6..0000000
--- a/Bond.Models/ModelContexts.cs
+++ /dev/null
@@ -1,284 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-
-namespace BondTools.Models;
-
-/// Graph state shared by one clone operation, scoped to compatible adapter semantics.
-public sealed class CloneContext
-{
- private readonly Dictionary> _clones = new();
- private readonly Dictionary