From 50a0ea2e8db4dbcd53e97c754250748ca584e824 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Wed, 10 Dec 2025 07:28:04 +0200 Subject: [PATCH 01/27] Starting additions to COntract API --- DesignContracts/Core/Contract-New.cs | 106 ++++++++++++++++++++++ DesignContracts/Core/ContractRuntime.cs | 116 ++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 DesignContracts/Core/Contract-New.cs create mode 100644 DesignContracts/Core/ContractRuntime.cs diff --git a/DesignContracts/Core/Contract-New.cs b/DesignContracts/Core/Contract-New.cs new file mode 100644 index 0000000..3c2e2e4 --- /dev/null +++ b/DesignContracts/Core/Contract-New.cs @@ -0,0 +1,106 @@ +namespace Odin.DesignContracts; + +public static partial class Contract +{ + + + /// + /// Specifies a postcondition that must hold true when the enclosing method returns. + /// + /// The condition that must be true. + /// An optional message describing the postcondition. + /// An optional text representation of the condition expression. + /// + /// Postconditions are evaluated only when is true. + /// Calls to this method become no-ops when postconditions are disabled. + /// It is expected that source-generated code will invoke this method at + /// appropriate points (typically immediately before method exit). + /// + public static void Ensures(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!ContractRuntime.PostconditionsEnabled) + { + return; + } + + if (!condition) + { + ReportFailure( + ContractFailureKind.Postcondition, + userMessage, + conditionText); + } + } + + + /// + /// Specifies an object invariant that must hold true whenever the object is in a valid state. + /// + /// The condition that must be true. + /// An optional message describing the invariant. + /// An optional text representation of the condition expression. + /// + /// Invariants are evaluated only when is true. + /// Calls to this method become no-ops when invariants are disabled. + /// It is expected that source-generated code will invoke invariant methods + /// (marked with ) at appropriate points. + /// + public static void Invariant(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!ContractRuntime.InvariantsEnabled) + { + return; + } + + if (!condition) + { + ReportFailure( + ContractFailureKind.Invariant, + userMessage, + conditionText); + } + } + + + /// + /// Specifies an assertion that must hold true at the given point in the code. + /// + /// The condition that must be true. + /// An optional message describing the assertion. + /// An optional text representation of the condition expression. + /// + /// Assertions are always evaluated at runtime. + /// + public static void Assert(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!condition) + { + ReportFailure( + ContractFailureKind.Assert, + userMessage, + conditionText); + } + } + + /// + /// Specifies an assumption that the analysis environment may rely on. + /// + /// The condition that is assumed to be true. + /// An optional message describing the assumption. + /// An optional text representation of the condition expression. + /// + /// At runtime, behaves identically to , + /// but analyzers may interpret assumptions differently. + /// + public static void Assume(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!condition) + { + ReportFailure( + ContractFailureKind.Assume, + userMessage, + conditionText); + } + } + +} \ No newline at end of file diff --git a/DesignContracts/Core/ContractRuntime.cs b/DesignContracts/Core/ContractRuntime.cs new file mode 100644 index 0000000..f2f03e0 --- /dev/null +++ b/DesignContracts/Core/ContractRuntime.cs @@ -0,0 +1,116 @@ +using System; + +namespace Odin.DesignContracts +{ + /// + /// Provides access to runtime configuration for design contract evaluation. + /// + /// + /// This type is intended for application startup configuration. For most usage, + /// prefer calling with values read from + /// configuration or environment variables. + /// + public static class ContractRuntime + { + private const string EnvPostconditions = "ODIN_CONTRACTS_ENABLE_POSTCONDITIONS"; + private const string EnvInvariants = "ODIN_CONTRACTS_ENABLE_INVARIANTS"; + + private static readonly object Sync = new(); + + private static ContractSettings _settings = CreateDefaultSettings(); + + /// + /// Gets a snapshot of the current contract settings. + /// + public static ContractSettings Settings + { + get + { + lock (Sync) + { + // Return a shallow copy to avoid external mutation. + return new ContractSettings + { + EnablePostconditions = _settings.EnablePostconditions, + EnableInvariants = _settings.EnableInvariants + }; + } + } + } + + /// + /// Gets a value indicating whether postconditions are evaluated at runtime. + /// + public static bool PostconditionsEnabled => Settings.EnablePostconditions; + + /// + /// Gets a value indicating whether invariants are evaluated at runtime. + /// + public static bool InvariantsEnabled => Settings.EnableInvariants; + + /// + /// Configures the runtime evaluation behavior for design contracts. + /// + /// + /// The settings to apply. A copy of the argument is stored; modifications + /// to the instance after this call do not affect runtime behavior. + /// + /// is null. + public static void Configure(ContractSettings settings) + { + if (settings is null) + throw new ArgumentNullException(nameof(settings)); + + lock (Sync) + { + _settings = new ContractSettings + { + EnablePostconditions = settings.EnablePostconditions, + EnableInvariants = settings.EnableInvariants + }; + } + } + + /// + /// Resets the runtime configuration to the default values. + /// + /// + /// Default values are derived from environment variables, falling back to + /// true for both postconditions and invariants when no values are present. + /// + public static void ResetToDefaults() + { + lock (Sync) + { + _settings = CreateDefaultSettings(); + } + } + + private static ContractSettings CreateDefaultSettings() + { + return new ContractSettings + { + EnablePostconditions = ReadBooleanEnv(EnvPostconditions, defaultValue: true), + EnableInvariants = ReadBooleanEnv(EnvInvariants, defaultValue: true) + }; + } + + private static bool ReadBooleanEnv(string name, bool defaultValue) + { + string? value = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrWhiteSpace(value)) + return defaultValue; + + if (bool.TryParse(value, out bool parsed)) + return parsed; + + // Accept 0/1 as shorthand. + if (value == "0") + return false; + if (value == "1") + return true; + + return defaultValue; + } + } +} From 89ff1dbeaab3023c8ce9e017d1aeee7664933635 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 15 Dec 2025 20:00:21 +0200 Subject: [PATCH 02/27] Starting with the rewriter and initial postcondition support. --- .../Analyzers/PostconditionsAnalyzer.cs | 270 +++++++++++++++++ DesignContracts/Core/Contract-New.cs | 38 ++- DesignContracts/Core/Contract.cs | 2 +- DesignContracts/README.md | 54 ++++ .../Odin.DesignContracts.Rewriter.csproj | 16 + DesignContracts/Rewriter/Program.cs | 274 ++++++++++++++++++ .../Odin.DesignContracts.Tooling.csproj | 39 +++ .../Odin.DesignContracts.Tooling.targets | 31 ++ Odin.sln | 14 + 9 files changed, 734 insertions(+), 4 deletions(-) create mode 100644 DesignContracts/Analyzers/PostconditionsAnalyzer.cs create mode 100644 DesignContracts/README.md create mode 100644 DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj create mode 100644 DesignContracts/Rewriter/Program.cs create mode 100644 DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj create mode 100644 DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets diff --git a/DesignContracts/Analyzers/PostconditionsAnalyzer.cs b/DesignContracts/Analyzers/PostconditionsAnalyzer.cs new file mode 100644 index 0000000..0c7c542 --- /dev/null +++ b/DesignContracts/Analyzers/PostconditionsAnalyzer.cs @@ -0,0 +1,270 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Odin.DesignContracts.Analyzers; + +/// +/// Validates usage of postconditions expressed via Odin.DesignContracts.Contract.Ensures +/// and return-value placeholders expressed via Odin.DesignContracts.Contract.Result<T>. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PostconditionsAnalyzer : DiagnosticAnalyzer +{ + /// + /// Postconditions must be declared in a contract block at the top of a method. + /// + public const string ContractBlockDiagnosticId = "ODIN101"; + + /// + /// Contract.Result<T> must only be used inside an Ensures condition. + /// + public const string ResultUsageDiagnosticId = "ODIN102"; + + /// + /// Postconditions are not supported for async/iterator methods (v1). + /// + public const string UnsupportedMethodDiagnosticId = "ODIN103"; + + /// + /// Postconditions require the build-time rewriter to be enabled. + /// + public const string RewriterNotEnabledDiagnosticId = "ODIN104"; + + /// + /// A contract block must be terminated by Contract.EndContractBlock() (v1). + /// + public const string MissingEndContractBlockDiagnosticId = "ODIN105"; + + private static readonly DiagnosticDescriptor ContractBlockRule = new( + ContractBlockDiagnosticId, + title: "Postconditions must be declared in a contract block at the start of the method", + messageFormat: "Postconditions must appear only at the start of the method (contract block) before any non-contract statements.", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor ResultUsageRule = new( + ResultUsageDiagnosticId, + title: "Contract.Result() can only be used inside a postcondition", + messageFormat: "Contract.Result() must only be used inside the first argument of Contract.Ensures(...).", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor UnsupportedMethodRule = new( + UnsupportedMethodDiagnosticId, + title: "Postconditions are not supported for async/iterator methods (v1)", + messageFormat: "Postconditions are not supported for this method kind (async/iterator).", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor RewriterNotEnabledRule = new( + RewriterNotEnabledDiagnosticId, + title: "Postconditions require the build-time rewriter", + messageFormat: "Postconditions are present but the Odin DesignContracts rewriter is not enabled for this project.", + category: "Build", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MissingEndContractBlockRule = new( + MissingEndContractBlockDiagnosticId, + title: "Contract.EndContractBlock() is required (v1)", + messageFormat: "Postconditions are present but the contract block is not terminated by Contract.EndContractBlock().", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(ContractBlockRule, ResultUsageRule, UnsupportedMethodRule, RewriterNotEnabledRule, + MissingEndContractBlockRule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration); + } + + private static void AnalyzeMethod(SyntaxNodeAnalysisContext context) + { + if (context.Node is not MethodDeclarationSyntax method) + return; + + // Expression-bodied methods can't have a contract block. + if (method.ExpressionBody is not null) + { + // Still validate Result() usage if present. + AnalyzeResultUsage(context, method); + return; + } + + if (method.Body is null) + return; + + bool hasPostconditions = ContainsEnsuresInvocation(context, method); + if (!hasPostconditions) + { + // Even without Ensures, ensure Result() isn't misused. + AnalyzeResultUsage(context, method); + return; + } + + // Enforce sync-only (v1). + if (method.Modifiers.Any(SyntaxKind.AsyncKeyword) || ContainsYield(method.Body)) + { + context.ReportDiagnostic(Diagnostic.Create( + UnsupportedMethodRule, + method.Identifier.GetLocation())); + } + + // Enforce contract block positioning at the top of the method. + bool seenNonContractStatement = false; + bool sawEndContractBlock = false; + foreach (StatementSyntax statement in method.Body.Statements) + { + if (IsContractBlockStatement(context, statement)) + { + if (seenNonContractStatement) + { + context.ReportDiagnostic(Diagnostic.Create( + ContractBlockRule, + statement.GetLocation())); + } + + if (statement is ExpressionStatementSyntax es && es.Expression is InvocationExpressionSyntax inv && + IsEndContractBlockInvocation(context, inv)) + { + sawEndContractBlock = true; + } + } + else + { + seenNonContractStatement = true; + } + } + + if (!sawEndContractBlock) + { + context.ReportDiagnostic(Diagnostic.Create( + MissingEndContractBlockRule, + method.Identifier.GetLocation())); + } + + // Ensure Result() is used only inside Ensures condition. + AnalyzeResultUsage(context, method); + + // Require rewriter enabled when postconditions are present. + if (!IsRewriterEnabled(context)) + { + context.ReportDiagnostic(Diagnostic.Create( + RewriterNotEnabledRule, + method.Identifier.GetLocation())); + } + } + + private static bool IsRewriterEnabled(SyntaxNodeAnalysisContext context) + { + if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions + .TryGetValue("build_property.OdinDesignContractsRewriterEnabled", out string? enabledText)) + { + return string.Equals(enabledText?.Trim(), "true", System.StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private static bool ContainsYield(BlockSyntax body) + => body.DescendantNodes().OfType().Any(); + + private static bool ContainsEnsuresInvocation(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax method) + => method.DescendantNodes().OfType().Any(i => IsEnsuresInvocation(context, i)); + + private static bool IsContractBlockStatement(SyntaxNodeAnalysisContext context, StatementSyntax statement) + { + if (statement is not ExpressionStatementSyntax exprStmt) + return false; + + if (exprStmt.Expression is not InvocationExpressionSyntax invocation) + return false; + + return IsEnsuresInvocation(context, invocation) || IsEndContractBlockInvocation(context, invocation); + } + + private static bool IsEnsuresInvocation(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) + => IsContractInvocation(context, invocation, methodName: "Ensures"); + + private static bool IsEndContractBlockInvocation(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) + => IsContractInvocation(context, invocation, methodName: "EndContractBlock"); + + private static bool IsResultInvocation(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) + => IsContractInvocation(context, invocation, methodName: "Result"); + + private static bool IsContractInvocation( + SyntaxNodeAnalysisContext context, + InvocationExpressionSyntax invocation, + string methodName) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + return false; + + if (memberAccess.Name.Identifier.Text != methodName) + return false; + + SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess); + if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) + return false; + + if (methodSymbol.Name != methodName) + return false; + + if (methodSymbol.ContainingType is not { Name: "Contract", ContainingNamespace: { } ns }) + return false; + + return ns.ToDisplayString() == "Odin.DesignContracts"; + } + + private static void AnalyzeResultUsage(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax method) + { + foreach (InvocationExpressionSyntax invocation in method.DescendantNodes().OfType()) + { + if (!IsResultInvocation(context, invocation)) + continue; + + // Must be inside: Contract.Ensures( /* condition */ ... ) - specifically inside argument #0. + if (!IsWithinEnsuresConditionArgument(context, invocation)) + { + context.ReportDiagnostic(Diagnostic.Create( + ResultUsageRule, + invocation.GetLocation())); + } + } + } + + private static bool IsWithinEnsuresConditionArgument(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax resultInvocation) + { + // Walk up: Result() -> ... -> Argument -> ArgumentList -> Invocation (Ensures) + SyntaxNode? node = resultInvocation; + while (node is not null) + { + if (node is ArgumentSyntax arg && arg.Parent is ArgumentListSyntax argList && + argList.Parent is InvocationExpressionSyntax parentInvocation && + IsEnsuresInvocation(context, parentInvocation)) + { + // Ensure it's the first argument. + int index = argList.Arguments.IndexOf(arg); + return index == 0; + } + + node = node.Parent; + } + + return false; + } +} diff --git a/DesignContracts/Core/Contract-New.cs b/DesignContracts/Core/Contract-New.cs index 3c2e2e4..f9a062d 100644 --- a/DesignContracts/Core/Contract-New.cs +++ b/DesignContracts/Core/Contract-New.cs @@ -2,7 +2,40 @@ namespace Odin.DesignContracts; public static partial class Contract { - + /// + /// Represents the return value of the enclosing method for use within postconditions. + /// + /// The enclosing method return type. + /// + /// The value returned by the enclosing method. + /// + /// + /// This API is intended to be used only inside postconditions expressed via + /// . + /// + /// When postconditions are enabled, it is expected that a build-time rewriter will + /// replace calls to this method with the actual method return value. + /// + /// Without rewriting, this method returns default. + /// + public static T Result() + { + return default!; + } + + /// + /// Marks the end of a contract block at the start of a method. + /// + /// + /// This method exists to support classic Design-by-Contract authoring styles and + /// build-time rewriting. + /// + /// A rewriter may use this as a hint to know where contract declarations end and + /// normal method logic begins. + /// + public static void EndContractBlock() + { + } /// /// Specifies a postcondition that must hold true when the enclosing method returns. @@ -31,7 +64,6 @@ public static void Ensures(bool condition, string? userMessage = null, string? c conditionText); } } - /// /// Specifies an object invariant that must hold true whenever the object is in a valid state. @@ -103,4 +135,4 @@ public static void Assume(bool condition, string? userMessage = null, string? co } } -} \ No newline at end of file +} diff --git a/DesignContracts/Core/Contract.cs b/DesignContracts/Core/Contract.cs index be2d5c3..fa5f681 100644 --- a/DesignContracts/Core/Contract.cs +++ b/DesignContracts/Core/Contract.cs @@ -9,7 +9,7 @@ namespace Odin.DesignContracts /// System.Diagnostics.Contracts.Contract from the classic .NET Framework, /// but it is implemented independently under the Odin.DesignContracts namespace. /// - public static class Contract + public static partial class Contract { /// /// Occurs when a contract fails and before a is thrown. diff --git a/DesignContracts/README.md b/DesignContracts/README.md new file mode 100644 index 0000000..f42b098 --- /dev/null +++ b/DesignContracts/README.md @@ -0,0 +1,54 @@ +# Odin.DesignContracts (Design by Contract) + +This repository includes a lightweight Design-by-Contract runtime in [`DesignContracts/Core/Contract.cs`](Core/Contract.cs:1) plus tooling to support **postconditions** (Ensures) by rewriting compiled IL. + +## Why IL rewriting (and not source generators) + +Roslyn **source generators** can only *add* new source; they cannot rewrite an existing method body to: + +- capture a return value, and +- run postconditions on **every** exit path. + +To support Code-Contracts-style authoring such as `Contract.Ensures(Contract.Result() != null)` we use a build-time IL rewriter. + +## Postconditions (v1) + +Postconditions are declared with [`Contract.Ensures()`](Core/Contract-New.cs:41) and may refer to the enclosing method return value via [`Contract.Result()`](Core/Contract-New.cs:13). + +### Authoring rules (v1) + +- Postconditions must appear in a **contract block** at the start of a method. +- The contract block must be terminated with [`Contract.EndContractBlock()`](Core/Contract-New.cs:31). +- Sync methods only (no `async`, no iterators) in v1. + +Example: + +```csharp +using Odin.DesignContracts; + +public static string GetName(User user) +{ + Contract.Requires(user is not null); + + Contract.Ensures(Contract.Result() is not null); + Contract.EndContractBlock(); + + return user.Name; +} +``` + +At build time the rewriter injects the `Ensures(...)` checks immediately before method exit, after capturing the return value. + +### Runtime enable/disable + +`Ensures` checks are gated by `ContractRuntime.PostconditionsEnabled` (see [`ContractRuntime`](Core/ContractRuntime.cs:1)). When disabled, postconditions are a no-op. + +## Tooling package + +The meta-package [`Odin.DesignContracts.Tooling`](Tooling/Odin.DesignContracts.Tooling.csproj:1) is responsible for: + +- shipping analyzers (to validate authoring), and +- running the IL rewriter after compilation via `buildTransitive` targets. + +The IL rewriter implementation lives in [`DesignContracts/Rewriter/Program.cs`](Rewriter/Program.cs:1). + diff --git a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj new file mode 100644 index 0000000..a0bce58 --- /dev/null +++ b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj @@ -0,0 +1,16 @@ + + + Exe + net8.0 + true + enable + Odin.DesignContracts.Rewriter + 1591;1573; + + + + + + + + diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs new file mode 100644 index 0000000..7e603aa --- /dev/null +++ b/DesignContracts/Rewriter/Program.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace Odin.DesignContracts.Rewriter; + +/// +/// Build-time IL rewriter that injects Design-by-Contract postconditions into method exit paths. +/// +internal static class Program +{ + private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; + + private static int Main(string[] args) + { + if (args.Length < 2) + { + Console.Error.WriteLine("Usage: Odin.DesignContracts.Rewriter "); + return 2; + } + + string assemblyPath = args[0]; + string outputPath = args[1]; + + if (!File.Exists(assemblyPath)) + { + Console.Error.WriteLine($"Assembly not found: {assemblyPath}"); + return 2; + } + + try + { + RewriteAssembly(assemblyPath, outputPath); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + return 1; + } + } + + private static void RewriteAssembly(string assemblyPath, string outputPath) + { + string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(assemblyPath))!; + + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(assemblyDir); + + // Portable PDBs are optional. If present, Cecil will pick them up with ReadSymbols = true. + var readerParameters = new ReaderParameters + { + AssemblyResolver = resolver, + ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")) + }; + + using AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); + + int rewritten = 0; + foreach (ModuleDefinition module in assembly.Modules) + { + foreach (TypeDefinition type in module.GetTypes()) + { + foreach (MethodDefinition method in type.Methods) + { + if (!method.HasBody) + continue; + + if (!TryRewriteMethod(method)) + continue; + + rewritten++; + } + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))!); + + var writerParameters = new WriterParameters + { + WriteSymbols = readerParameters.ReadSymbols + }; + + assembly.Write(outputPath, writerParameters); + Console.WriteLine($"Rewriter: rewritten methods: {rewritten}"); + } + + private static bool TryRewriteMethod(MethodDefinition method) + { + // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. + if (method.IsAsync) + return false; + + method.Body.SimplifyMacros(); + + if (!TryExtractContractBlock(method, out List contractBlockInstructions)) + { + method.Body.OptimizeMacros(); + return false; + } + + // No Ensures in contract block => nothing to rewrite. + if (!contractBlockInstructions.Any(IsEnsuresCall)) + { + method.Body.OptimizeMacros(); + return false; + } + + // Remove the contract block from the method entry. + var il = method.Body.GetILProcessor(); + foreach (Instruction inst in contractBlockInstructions) + { + // If the instruction was already removed as part of a previous remove, skip. + if (method.Body.Instructions.Contains(inst)) + il.Remove(inst); + } + + // Prepare unified epilogue and ensure all returns branch to it. + bool isVoid = method.ReturnType.MetadataType == MetadataType.Void; + VariableDefinition? resultVar = null; + + if (!isVoid) + { + resultVar = new VariableDefinition(method.ReturnType); + method.Body.Variables.Add(resultVar); + method.Body.InitLocals = true; + } + + Instruction epilogueStart = Instruction.Create(OpCodes.Nop); + + // Rewrite all returns to branch to epilogueStart. + // For non-void, store the return value into resultVar before branching. + List rets = method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToList(); + foreach (Instruction ret in rets) + { + if (!isVoid) + { + il.InsertBefore(ret, Instruction.Create(OpCodes.Stloc, resultVar!)); + } + + ret.OpCode = OpCodes.Br; + ret.Operand = epilogueStart; + } + + // Append epilogue start. + il.Append(epilogueStart); + + // Re-insert contract block at epilogue (excluding EndContractBlock calls). + // Also rewrite Contract.Result() to load the stored return value. + foreach (Instruction inst in contractBlockInstructions) + { + if (IsEndContractBlockCall(inst)) + continue; + + Instruction cloned = CloneInstruction(inst); + + if (!isVoid && IsResultCall(cloned)) + { + // Replace call Contract.Result() with ldloc resultVar. + cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); + } + + il.Append(cloned); + } + + // Final return. + if (!isVoid) + { + il.Append(Instruction.Create(OpCodes.Ldloc, resultVar!)); + } + + il.Append(Instruction.Create(OpCodes.Ret)); + + method.Body.OptimizeMacros(); + return true; + } + + private static bool TryExtractContractBlock(MethodDefinition method, out List contractBlock) + { + // v1: contract block must be explicitly terminated by Contract.EndContractBlock(). + // This makes extraction deterministic without needing sequence points. + contractBlock = new List(); + + IList instructions = method.Body.Instructions; + if (instructions.Count == 0) + return false; + + int endIndex = -1; + for (int i = 0; i < instructions.Count; i++) + { + Instruction inst = instructions[i]; + contractBlock.Add(inst); + + if (IsEndContractBlockCall(inst)) + { + endIndex = i; + break; + } + } + + if (endIndex < 0) + { + // No explicit contract block end. + contractBlock.Clear(); + return false; + } + + // Also include trailing nops immediately after EndContractBlock, as they often belong + // to the same source statement / sequence point. + for (int i = endIndex + 1; i < instructions.Count; i++) + { + if (instructions[i].OpCode != OpCodes.Nop) + break; + contractBlock.Add(instructions[i]); + } + + return true; + } + + private static bool IsEnsuresCall(Instruction inst) + => IsStaticCallToContractMethod(inst, "Ensures"); + + private static bool IsEndContractBlockCall(Instruction inst) + => IsStaticCallToContractMethod(inst, "EndContractBlock"); + + private static bool IsResultCall(Instruction inst) + => IsStaticCallToContractMethod(inst, "Result"); + + private static bool IsStaticCallToContractMethod(Instruction inst, string methodName) + { + if (inst.OpCode != OpCodes.Call) + return false; + + if (inst.Operand is not MethodReference mr) + return false; + + if (mr.Name != methodName) + return false; + + // Handle generic instance method as well. + string declaringType = mr.DeclaringType.FullName; + return declaringType == ContractTypeFullName; + } + + private static Instruction CloneInstruction(Instruction inst) + { + // Minimal cloning sufficient for contract block statements. + // We intentionally do not support cloning branch targets / exception handler operands in v1. + if (inst.Operand is null) + return Instruction.Create(inst.OpCode); + + return inst.Operand switch + { + sbyte b => Instruction.Create(inst.OpCode, b), + byte b => Instruction.Create(inst.OpCode, (sbyte)b), // Cecil uses sbyte for short forms. + int i => Instruction.Create(inst.OpCode, i), + long l => Instruction.Create(inst.OpCode, l), + float f => Instruction.Create(inst.OpCode, f), + double d => Instruction.Create(inst.OpCode, d), + string s => Instruction.Create(inst.OpCode, s), + MethodReference mr => Instruction.Create(inst.OpCode, mr), + FieldReference fr => Instruction.Create(inst.OpCode, fr), + TypeReference tr => Instruction.Create(inst.OpCode, tr), + ParameterDefinition pd => Instruction.Create(inst.OpCode, pd), + VariableDefinition vd => Instruction.Create(inst.OpCode, vd), + _ => throw new NotSupportedException( + $"Unsupported operand type in contract block cloning: {inst.Operand.GetType().FullName} (opcode {inst.OpCode}).") + }; + } +} diff --git a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj new file mode 100644 index 0000000..212d2f1 --- /dev/null +++ b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj @@ -0,0 +1,39 @@ + + + net8.0 + true + enable + + true + Odin.DesignContracts.Tooling + Design-by-Contract tooling for Odin.DesignContracts: analyzers + IL rewriter integration. + icon.png + 1591;1573; + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets new file mode 100644 index 0000000..93cfc85 --- /dev/null +++ b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets @@ -0,0 +1,31 @@ + + + + true + + + true + + + + + + <_OdinDcRewriterPath>$(MSBuildThisFileDirectory)..\tools\net8.0\any\Odin.DesignContracts.Rewriter.dll + <_OdinDcWeavedAssembly>$(IntermediateOutputPath)$(AssemblyName).odindc.weaved$(TargetExt) + <_OdinDcWeavedPdb>$(IntermediateOutputPath)$(AssemblyName).odindc.weaved.pdb + + + + + + + + + + + + + diff --git a/Odin.sln b/Odin.sln index 459b048..ba71d48 100644 --- a/Odin.sln +++ b/Odin.sln @@ -97,6 +97,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.System", "System EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Analyzers", "DesignContracts\Analyzers\Odin.DesignContracts.Analyzers.csproj", "{12C40512-CA83-4209-9318-6CBCABF8C798}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Rewriter", "DesignContracts\Rewriter\Odin.DesignContracts.Rewriter.csproj", "{60A2A141-EAC8-4EAB-850D-AF534ABFE98C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Tooling", "DesignContracts\Tooling\Odin.DesignContracts.Tooling.csproj", "{B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -223,6 +227,14 @@ Global {12C40512-CA83-4209-9318-6CBCABF8C798}.Debug|Any CPU.Build.0 = Debug|Any CPU {12C40512-CA83-4209-9318-6CBCABF8C798}.Release|Any CPU.ActiveCfg = Release|Any CPU {12C40512-CA83-4209-9318-6CBCABF8C798}.Release|Any CPU.Build.0 = Release|Any CPU + {60A2A141-EAC8-4EAB-850D-AF534ABFE98C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60A2A141-EAC8-4EAB-850D-AF534ABFE98C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60A2A141-EAC8-4EAB-850D-AF534ABFE98C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60A2A141-EAC8-4EAB-850D-AF534ABFE98C}.Release|Any CPU.Build.0 = Release|Any CPU + {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE323D9C-635B-EFD3-5B3F-7CE371D8A86A} = {BF440C74-E223-3CBF-8FA7-83F7D164F7C3} @@ -255,5 +267,7 @@ Global {690312C5-1948-4530-832D-6332625D4E9C} = {4140B1D5-6C97-4F68-8DEA-C0D139BA9FBC} {957966BC-FE0E-4206-BDD8-F04591AB836E} = {73BA62BB-2B41-2DC4-C714-51B3D4C2A215} {12C40512-CA83-4209-9318-6CBCABF8C798} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} + {60A2A141-EAC8-4EAB-850D-AF534ABFE98C} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} + {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} EndGlobalSection EndGlobal From 118438b9d7d256acde69493e5f8a2239081eaa95 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 15 Dec 2025 20:51:48 +0200 Subject: [PATCH 03/27] Starting invariant support --- DesignContracts/README.md | 17 +- DesignContracts/Rewriter/AssemblyInfo.cs | 4 + DesignContracts/Rewriter/Program.cs | 202 +++++++++--- .../Tests/InvariantWeavingRewriterTests.cs | 307 ++++++++++++++++++ .../Tests/Tests.Odin.DesignContracts.csproj | 4 +- 5 files changed, 481 insertions(+), 53 deletions(-) create mode 100644 DesignContracts/Rewriter/AssemblyInfo.cs create mode 100644 DesignContracts/Tests/InvariantWeavingRewriterTests.cs diff --git a/DesignContracts/README.md b/DesignContracts/README.md index f42b098..076a81b 100644 --- a/DesignContracts/README.md +++ b/DesignContracts/README.md @@ -43,6 +43,22 @@ At build time the rewriter injects the `Ensures(...)` checks immediately before `Ensures` checks are gated by `ContractRuntime.PostconditionsEnabled` (see [`ContractRuntime`](Core/ContractRuntime.cs:1)). When disabled, postconditions are a no-op. +## Object invariants (v1) + +Object invariants are authored by declaring a private, parameterless, `void` method marked with +[`ContractInvariantMethodAttribute`](Core/ContractAttributes.cs:1), and calling +[`Contract.Invariant()`](Core/Contract-New.cs:68) inside that method. + +At build time the IL rewriter wires up invariant execution as follows: + +- If a type declares exactly one invariant method (either `Odin.DesignContracts.ContractInvariantMethodAttribute` + or `System.Diagnostics.Contracts.ContractInvariantMethodAttribute`), the rewriter injects calls to that invariant method + at **entry and exit** of all **public instance** methods and **public instance** property accessors. +- Public instance constructors (`.ctor`) get invariant calls at **exit only**. +- Any public method/property accessor marked `[System.Diagnostics.Contracts.Pure]` is **excluded** from invariant weaving. + +Invariant checks are gated by `ContractRuntime.InvariantsEnabled`. + ## Tooling package The meta-package [`Odin.DesignContracts.Tooling`](Tooling/Odin.DesignContracts.Tooling.csproj:1) is responsible for: @@ -51,4 +67,3 @@ The meta-package [`Odin.DesignContracts.Tooling`](Tooling/Odin.DesignContracts.T - running the IL rewriter after compilation via `buildTransitive` targets. The IL rewriter implementation lives in [`DesignContracts/Rewriter/Program.cs`](Rewriter/Program.cs:1). - diff --git a/DesignContracts/Rewriter/AssemblyInfo.cs b/DesignContracts/Rewriter/AssemblyInfo.cs new file mode 100644 index 0000000..43847b6 --- /dev/null +++ b/DesignContracts/Rewriter/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests.Odin.DesignContracts")] + diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 7e603aa..434a8af 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -15,6 +15,10 @@ internal static class Program { private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; + private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ContractInvariantMethodAttribute"; + private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ContractInvariantMethodAttribute"; + private const string PureAttributeFullName = "System.Diagnostics.Contracts.PureAttribute"; + private static int Main(string[] args) { if (args.Length < 2) @@ -44,7 +48,7 @@ private static int Main(string[] args) } } - private static void RewriteAssembly(string assemblyPath, string outputPath) + internal static void RewriteAssembly(string assemblyPath, string outputPath) { string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(assemblyPath))!; @@ -65,12 +69,14 @@ private static void RewriteAssembly(string assemblyPath, string outputPath) { foreach (TypeDefinition type in module.GetTypes()) { + MethodDefinition? invariantMethod = FindInvariantMethodOrThrow(type); + foreach (MethodDefinition method in type.Methods) { if (!method.HasBody) continue; - if (!TryRewriteMethod(method)) + if (!TryRewriteMethod(type, method, invariantMethod)) continue; rewritten++; @@ -89,7 +95,7 @@ private static void RewriteAssembly(string assemblyPath, string outputPath) Console.WriteLine($"Rewriter: rewritten methods: {rewritten}"); } - private static bool TryRewriteMethod(MethodDefinition method) + private static bool TryRewriteMethod(TypeDefinition declaringType, MethodDefinition method, MethodDefinition? invariantMethod) { // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. if (method.IsAsync) @@ -97,86 +103,180 @@ private static bool TryRewriteMethod(MethodDefinition method) method.Body.SimplifyMacros(); - if (!TryExtractContractBlock(method, out List contractBlockInstructions)) + bool hasInvariant = invariantMethod is not null; + bool canWeaveInvariant = hasInvariant && IsPublicInstanceMethodOrAccessor(method); + bool isInvariantMethodItself = invariantMethod is not null && method == invariantMethod; + + // Per requirements: + // - Constructors: invariants at exit only + // - Other public instance methods + public instance property accessors: invariants at entry and exit + bool isConstructor = method.IsConstructor && !method.IsStatic; + + bool weaveInvariantOnEntry = canWeaveInvariant && !isInvariantMethodItself && !isConstructor; + bool weaveInvariantOnExit = canWeaveInvariant && !isInvariantMethodItself; + + // Exclude [Pure] methods and [Pure] properties (for accessors). + if (!isConstructor && weaveInvariantOnEntry && IsPure(declaringType, method)) { - method.Body.OptimizeMacros(); - return false; + weaveInvariantOnEntry = false; + weaveInvariantOnExit = false; } - // No Ensures in contract block => nothing to rewrite. - if (!contractBlockInstructions.Any(IsEnsuresCall)) + // If we have no invariant weaving and no postconditions, skip quickly. + if (!weaveInvariantOnEntry && !weaveInvariantOnExit) + { + // Still allow postconditions rewriting. + } + + bool hasContractBlock = TryExtractContractBlock(method, out List contractBlockInstructions); + bool hasEnsures = hasContractBlock && contractBlockInstructions.Any(IsEnsuresCall); + + if (!hasEnsures && !weaveInvariantOnEntry && !weaveInvariantOnExit) { method.Body.OptimizeMacros(); return false; } - // Remove the contract block from the method entry. var il = method.Body.GetILProcessor(); - foreach (Instruction inst in contractBlockInstructions) + + // Inject invariant call at entry (before any user code). + if (weaveInvariantOnEntry) { - // If the instruction was already removed as part of a previous remove, skip. - if (method.Body.Instructions.Contains(inst)) - il.Remove(inst); - } + Instruction first = method.Body.Instructions.FirstOrDefault() ?? Instruction.Create(OpCodes.Nop); + if (method.Body.Instructions.Count == 0) + il.Append(first); - // Prepare unified epilogue and ensure all returns branch to it. - bool isVoid = method.ReturnType.MetadataType == MetadataType.Void; - VariableDefinition? resultVar = null; + InsertInvariantCallBefore(il, first, invariantMethod!); + } - if (!isVoid) + // Remove the contract block from the method entry when postconditions are present. + if (hasEnsures) { - resultVar = new VariableDefinition(method.ReturnType); - method.Body.Variables.Add(resultVar); - method.Body.InitLocals = true; + foreach (Instruction inst in contractBlockInstructions) + { + // If the instruction was already removed as part of a previous remove, skip. + if (method.Body.Instructions.Contains(inst)) + il.Remove(inst); + } } - Instruction epilogueStart = Instruction.Create(OpCodes.Nop); - - // Rewrite all returns to branch to epilogueStart. - // For non-void, store the return value into resultVar before branching. - List rets = method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToList(); - foreach (Instruction ret in rets) + // If we need to inject postconditions and/or invariant calls at exit, do it per-ret. + if (hasEnsures || weaveInvariantOnExit) { + bool isVoid = method.ReturnType.MetadataType == MetadataType.Void; + VariableDefinition? resultVar = null; + if (!isVoid) { - il.InsertBefore(ret, Instruction.Create(OpCodes.Stloc, resultVar!)); + resultVar = new VariableDefinition(method.ReturnType); + method.Body.Variables.Add(resultVar); + method.Body.InitLocals = true; } - ret.OpCode = OpCodes.Br; - ret.Operand = epilogueStart; - } + List rets = method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToList(); + foreach (Instruction ret in rets) + { + // For non-void methods we must preserve the return value while we call extra code. + if (!isVoid) + { + il.InsertBefore(ret, Instruction.Create(OpCodes.Stloc, resultVar!)); + } - // Append epilogue start. - il.Append(epilogueStart); + if (hasEnsures) + { + foreach (Instruction inst in contractBlockInstructions) + { + if (IsEndContractBlockCall(inst)) + continue; - // Re-insert contract block at epilogue (excluding EndContractBlock calls). - // Also rewrite Contract.Result() to load the stored return value. - foreach (Instruction inst in contractBlockInstructions) - { - if (IsEndContractBlockCall(inst)) - continue; + Instruction cloned = CloneInstruction(inst); - Instruction cloned = CloneInstruction(inst); + if (!isVoid && IsResultCall(cloned)) + { + // Replace call Contract.Result() with ldloc resultVar. + cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); + } - if (!isVoid && IsResultCall(cloned)) - { - // Replace call Contract.Result() with ldloc resultVar. - cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); + il.InsertBefore(ret, cloned); + } + } + + if (weaveInvariantOnExit) + { + InsertInvariantCallBefore(il, ret, invariantMethod!); + } + + if (!isVoid) + { + il.InsertBefore(ret, Instruction.Create(OpCodes.Ldloc, resultVar!)); + } } + } + + method.Body.OptimizeMacros(); + return true; + } - il.Append(cloned); + private static bool IsPublicInstanceMethodOrAccessor(MethodDefinition method) + => method.IsPublic && !method.IsStatic; + + private static bool IsPure(TypeDefinition declaringType, MethodDefinition method) + { + if (HasAttribute(method, PureAttributeFullName)) + return true; + + // For accessors, also honour [Pure] on the property itself. + if (method.IsGetter || method.IsSetter) + { + PropertyDefinition? prop = declaringType.Properties.FirstOrDefault(p => p.GetMethod == method || p.SetMethod == method); + if (prop is not null && HasAttribute(prop, PureAttributeFullName)) + return true; } - // Final return. - if (!isVoid) + return false; + } + + private static bool HasAttribute(ICustomAttributeProvider provider, string attributeFullName) + => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => a.AttributeType.FullName == attributeFullName); + + private static MethodDefinition? FindInvariantMethodOrThrow(TypeDefinition type) + { + List candidates = type.Methods + .Where(m => HasAttribute(m, OdinInvariantAttributeFullName) || HasAttribute(m, BclInvariantAttributeFullName)) + .ToList(); + + if (candidates.Count == 0) + return null; + + if (candidates.Count > 1) { - il.Append(Instruction.Create(OpCodes.Ldloc, resultVar!)); + string names = string.Join(", ", candidates.Select(m => m.FullName)); + throw new InvalidOperationException( + $"Type '{type.FullName}' has multiple invariant methods. Exactly one method may be marked with ContractInvariantMethodAttribute. Candidates: {names}"); } - il.Append(Instruction.Create(OpCodes.Ret)); + MethodDefinition invariant = candidates[0]; - method.Body.OptimizeMacros(); - return true; + if (invariant.IsStatic) + throw new InvalidOperationException($"Invariant method must be an instance method: {invariant.FullName}"); + + if (invariant.Parameters.Count != 0) + throw new InvalidOperationException($"Invariant method must be parameterless: {invariant.FullName}"); + + if (invariant.ReturnType.MetadataType != MetadataType.Void) + throw new InvalidOperationException($"Invariant method must return void: {invariant.FullName}"); + + if (!invariant.HasBody) + throw new InvalidOperationException($"Invariant method must have a body: {invariant.FullName}"); + + return invariant; + } + + private static void InsertInvariantCallBefore(ILProcessor il, Instruction before, MethodDefinition invariantMethod) + { + // instance.Invariant(); + il.InsertBefore(before, Instruction.Create(OpCodes.Ldarg_0)); + il.InsertBefore(before, Instruction.Create(OpCodes.Call, invariantMethod)); } private static bool TryExtractContractBlock(MethodDefinition method, out List contractBlock) diff --git a/DesignContracts/Tests/InvariantWeavingRewriterTests.cs b/DesignContracts/Tests/InvariantWeavingRewriterTests.cs new file mode 100644 index 0000000..3fa90ec --- /dev/null +++ b/DesignContracts/Tests/InvariantWeavingRewriterTests.cs @@ -0,0 +1,307 @@ +using System.Reflection; +using System.Runtime.Loader; +using Mono.Cecil; +using NUnit.Framework; +using Odin.DesignContracts; + +namespace Tests.Odin.DesignContracts; + +[TestFixture] +public sealed class InvariantWeavingRewriterTests +{ + [SetUp] + public void SetUp() + { + // Ensure invariants are enabled even if the test environment sets env vars. + ContractRuntime.Configure(new ContractSettings + { + EnableInvariants = true, + EnablePostconditions = true + }); + } + + [Test] + public void Public_constructor_runs_invariant_on_exit() + { + using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); + + Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); + + ContractException ex = Assert.Throws(() => + { + try + { + Activator.CreateInstance(t, -1); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Public_method_runs_invariant_on_entry() + { + using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); + Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + ContractException ex = Assert.Throws(() => + { + Invoke(t, instance, nameof(RewriterTargets.InvariantTarget.Increment)); + })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Public_method_runs_invariant_on_exit() + { + using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); + Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + + ContractException ex = Assert.Throws(() => + { + Invoke(t, instance, nameof(RewriterTargets.InvariantTarget.MakeInvalid)); + })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Pure_method_is_excluded_from_invariant_weaving() + { + using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); + Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + // If [Pure] is honoured, this returns the invalid value without invariant checks throwing. + object? result = Invoke(t, instance, nameof(RewriterTargets.InvariantTarget.PureGetValue)); + Assert.That(result, Is.EqualTo(-1)); + } + + [Test] + public void Pure_property_is_excluded_from_invariant_weaving() + { + using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); + Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + PropertyInfo p = t.GetProperty(nameof(RewriterTargets.InvariantTarget.PureValue))!; + object? value = p.GetValue(instance); + Assert.That(value, Is.EqualTo(-1)); + } + + [Test] + public void Non_pure_property_is_woven_and_checks_invariants() + { + using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); + Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + PropertyInfo p = t.GetProperty(nameof(RewriterTargets.InvariantTarget.NonPureValue))!; + + ContractException ex = Assert.Throws(() => + { + try + { + _ = p.GetValue(instance); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Multiple_invariant_methods_causes_rewrite_to_fail() + { + // Arrange: create a temp copy of this test assembly and inject a second [ContractInvariantMethod]. + string originalPath = typeof(RewriterTargets.InvariantTarget).Assembly.Location; + using var temp = new TempDir(); + + string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); + File.Copy(originalPath, inputPath, overwrite: true); + + using (AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) + { + TypeDefinition targetType = ad.MainModule.GetType(typeof(RewriterTargets.InvariantTarget).FullName!) + ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); + + MethodDefinition increment = targetType.Methods + .First(m => m.Name == nameof(RewriterTargets.InvariantTarget.Increment)); + + // Add a second invariant attribute to a different method. + var ctor = typeof(Odin.DesignContracts.ContractInvariantMethodAttribute).GetConstructor(Type.EmptyTypes) + ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); + var ctorRef = ad.MainModule.ImportReference(ctor); + increment.CustomAttributes.Add(new CustomAttribute(ctorRef)); + + ad.Write(inputPath); + } + + // Act + Assert + string outputPath = Path.Combine(temp.Path, "out.dll"); + Assert.Throws(() => Odin.DesignContracts.Rewriter.Program.RewriteAssembly(inputPath, outputPath)); + } + + private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) + { + FieldInfo f = declaringType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException($"Missing field '{fieldName}'."); + f.SetValue(instance, value); + } + + private static object? Invoke(Type declaringType, object instance, string methodName) + { + MethodInfo m = declaringType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public) + ?? throw new InvalidOperationException($"Missing method '{methodName}'."); + + try + { + return m.Invoke(instance, null); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + } + + private sealed class RewrittenAssemblyContext : IDisposable + { + private readonly AssemblyLoadContext _alc; + private readonly string _tempDir; + + public Assembly RewrittenAssembly { get; } + + public RewrittenAssemblyContext(Assembly sourceAssembly) + { + _tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "odindc-rewriter-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + + string inputPath = Path.Combine(_tempDir, Path.GetFileName(sourceAssembly.Location)); + File.Copy(sourceAssembly.Location, inputPath, overwrite: true); + + CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".pdb"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".pdb")))); + CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); + + string outputPath = Path.Combine(_tempDir, "rewritten.dll"); + Odin.DesignContracts.Rewriter.Program.RewriteAssembly(inputPath, outputPath); + + _alc = new TestAssemblyLoadContext(outputPath); + RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); + } + + public Type GetTypeOrThrow(string fullName) + => RewrittenAssembly.GetType(fullName, throwOnError: true)! + ?? throw new InvalidOperationException($"Type not found in rewritten assembly: {fullName}"); + + public void Dispose() + { + _alc.Unload(); + + // Best-effort cleanup. Unload is async-ish; ignore IO failures. + try { Directory.Delete(_tempDir, recursive: true); } catch { /* ignore */ } + } + + private static void CopyIfExists(string from, string to) + { + if (File.Exists(from)) + { + File.Copy(from, to, overwrite: true); + } + } + + private sealed class TestAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + public TestAssemblyLoadContext(string mainAssemblyPath) + : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(mainAssemblyPath); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + string? path = _resolver.ResolveAssemblyToPath(assemblyName); + if (path is null) + return null; + + return LoadFromAssemblyPath(path); + } + } + } + + private sealed class TempDir : IDisposable + { + public string Path { get; } + + public TempDir() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "odindc-rewriter-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path); + } + + public void Dispose() + { + try { Directory.Delete(Path, recursive: true); } catch { /* ignore */ } + } + } +} + +namespace Tests.Odin.DesignContracts.RewriterTargets; + +/// +/// Target type used by . The rewriter is expected to inject +/// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. +/// +public sealed class InvariantTarget +{ + private int _value; + + public InvariantTarget(int value) + { + _value = value; + } + + [Odin.DesignContracts.ContractInvariantMethod] + private void ObjectInvariant() + { + Contract.Invariant(_value >= 0, "_value must be >= 0", "_value >= 0"); + } + + public void Increment() + { + _value++; + } + + public void MakeInvalid() + { + _value = -1; + } + + [System.Diagnostics.Contracts.Pure] + public int PureGetValue() => _value; + + [System.Diagnostics.Contracts.Pure] + public int PureValue => _value; + + public int NonPureValue => _value; +} diff --git a/DesignContracts/Tests/Tests.Odin.DesignContracts.csproj b/DesignContracts/Tests/Tests.Odin.DesignContracts.csproj index a951a29..1b6438c 100644 --- a/DesignContracts/Tests/Tests.Odin.DesignContracts.csproj +++ b/DesignContracts/Tests/Tests.Odin.DesignContracts.csproj @@ -13,10 +13,12 @@ + + - \ No newline at end of file + From 274b055cd6e7242a067c09c9bfb7d2750378c9c0 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Fri, 19 Dec 2025 23:13:14 +0200 Subject: [PATCH 04/27] Saving progress on Design Contracts --- .../BackgroundProcessingProviders.cs | 3 +- .../Analyzers/PostconditionsAnalyzer.cs | 1 - DesignContracts/Core/Contract.cs | 31 +- DesignContracts/Core/ContractAttributes.cs | 36 +- DesignContracts/Core/ContractException.cs | 2 - .../Core/ContractFailedEventArgs.cs | 2 +- DesignContracts/Core/ContractRuntime.cs | 116 ------- DesignContracts/Core/ContractSettings.cs | 28 -- DesignContracts/Core/DesignContractOptions.cs | 45 +++ .../{Contract-New.cs => Postcondition.cs} | 35 +- DesignContracts/Core/Precondition.cs | 5 +- .../CoreTests/PreconditionTests.cs | 60 ++++ .../Tests.Odin.DesignContracts.csproj | 3 - DesignContracts/Rewriter/AssemblyInfo.cs | 2 +- .../Odin.DesignContracts.Rewriter.csproj | 1 - DesignContracts/Rewriter/Program.cs | 20 +- .../InvariantWeavingRewriterTests.cs | 184 +++++++++++ .../RewriterTests/RewrittenAssemblyContext.cs | 82 +++++ .../RewriterTests/Targets/InvariantTarget.cs | 42 +++ .../RewriterTests/TempDirectory.cs | 24 ++ ...Tests.Odin.DesignContracts.Rewriter.csproj | 23 ++ DesignContracts/Tests/ContractTests.cs | 49 --- .../Tests/InvariantWeavingRewriterTests.cs | 307 ------------------ Email/Office365/Office365EmailSender.cs | 1 - Odin.sln | 9 +- System/Tests/ResultOfTMessageTests.cs | 3 +- .../Tests/Razor/RazorTemplateRendererTests.cs | 1 - 27 files changed, 549 insertions(+), 566 deletions(-) delete mode 100644 DesignContracts/Core/ContractRuntime.cs delete mode 100644 DesignContracts/Core/ContractSettings.cs create mode 100644 DesignContracts/Core/DesignContractOptions.cs rename DesignContracts/Core/{Contract-New.cs => Postcondition.cs} (87%) create mode 100644 DesignContracts/CoreTests/PreconditionTests.cs rename DesignContracts/{Tests => CoreTests}/Tests.Odin.DesignContracts.csproj (76%) create mode 100644 DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs create mode 100644 DesignContracts/RewriterTests/RewrittenAssemblyContext.cs create mode 100644 DesignContracts/RewriterTests/Targets/InvariantTarget.cs create mode 100644 DesignContracts/RewriterTests/TempDirectory.cs create mode 100644 DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj delete mode 100644 DesignContracts/Tests/ContractTests.cs delete mode 100644 DesignContracts/Tests/InvariantWeavingRewriterTests.cs diff --git a/BackgroundProcessing/Abstractions/BackgroundProcessingProviders.cs b/BackgroundProcessing/Abstractions/BackgroundProcessingProviders.cs index 42bb890..e72622f 100644 --- a/BackgroundProcessing/Abstractions/BackgroundProcessingProviders.cs +++ b/BackgroundProcessing/Abstractions/BackgroundProcessingProviders.cs @@ -1,5 +1,4 @@ -using System.Collections.Specialized; -using Odin.System; +using Odin.System; namespace Odin.BackgroundProcessing { diff --git a/DesignContracts/Analyzers/PostconditionsAnalyzer.cs b/DesignContracts/Analyzers/PostconditionsAnalyzer.cs index 0c7c542..7789411 100644 --- a/DesignContracts/Analyzers/PostconditionsAnalyzer.cs +++ b/DesignContracts/Analyzers/PostconditionsAnalyzer.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/DesignContracts/Core/Contract.cs b/DesignContracts/Core/Contract.cs index 35e139f..11e5591 100644 --- a/DesignContracts/Core/Contract.cs +++ b/DesignContracts/Core/Contract.cs @@ -9,7 +9,7 @@ namespace Odin.DesignContracts /// System.Diagnostics.Contracts.Contract from the classic .NET Framework, /// but it is implemented independently under the Odin.DesignContracts namespace. /// - public static partial class Contract + internal static class Contract { /// /// Occurs when a contract fails and before a is thrown. @@ -34,26 +34,43 @@ internal static void ReportFailure(ContractFailureKind kind, string? userMessage throw new ContractException(kind, message, userMessage, conditionText); } + internal static string GetKindFailedText(ContractFailureKind kind) + { + switch (kind) + { + case ContractFailureKind.Precondition: + return "Precondition not met"; + case ContractFailureKind.Postcondition: + return "Postcondition not honoured"; + case ContractFailureKind.Invariant: + return "Invariant broken"; + case ContractFailureKind.Assertion: + return "Assertion failed"; + case ContractFailureKind.Assumption: + return "Assumption failed"; + default: + throw new ArgumentOutOfRangeException(nameof(kind), kind, null); + } + } + internal static string BuildFailureMessage(ContractFailureKind kind, string? userMessage, string? conditionText) { - string kindText = kind.ToString(); - if (!string.IsNullOrWhiteSpace(userMessage) && !string.IsNullOrWhiteSpace(conditionText)) { - return $"{kindText} failed: {userMessage} [Condition: {conditionText}]"; + return $"{GetKindFailedText(kind)}: {userMessage} [Condition: {conditionText}]"; } if (!string.IsNullOrWhiteSpace(userMessage)) { - return $"{kindText} failed: {userMessage}"; + return $"{GetKindFailedText(kind)}: {userMessage}"; } if (!string.IsNullOrWhiteSpace(conditionText)) { - return $"{kindText} failed: {conditionText}"; + return $"{GetKindFailedText(kind)}: {conditionText}"; } - return $"{kindText} failed."; + return $"{GetKindFailedText(kind)}."; } } } \ No newline at end of file diff --git a/DesignContracts/Core/ContractAttributes.cs b/DesignContracts/Core/ContractAttributes.cs index c7ac89b..6f6d8f7 100644 --- a/DesignContracts/Core/ContractAttributes.cs +++ b/DesignContracts/Core/ContractAttributes.cs @@ -1,19 +1,19 @@ namespace Odin.DesignContracts { - // /// - // /// Identifies a method that contains class invariant checks for its declaring type. - // /// - // /// - // /// Methods marked with this attribute are expected to be private, parameterless, - // /// and to invoke for each invariant. - // /// Source generators can use this attribute to discover and invoke invariant methods - // /// at appropriate points (for example, at the end of constructors and public methods). - // /// - // [AttributeUsage(AttributeTargets.Method, Inherited = false)] - // public sealed class ClassInvariantMethodAttribute : Attribute - // { - // } - // + /// + /// Identifies a method that contains class invariant checks for its declaring type. + /// + /// + /// Methods marked with this attribute are expected to be private, parameterless, + /// and to invoke for each invariant. + /// Source generators can use this attribute to discover and invoke invariant methods + /// at appropriate points (for example, at the end of constructors and public methods). + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class ClassInvariantMethodAttribute : Attribute + { + } + // /// // /// Indicates that the decorated method is intended to be used only by // /// generated code for contract injection purposes. @@ -26,4 +26,12 @@ namespace Odin.DesignContracts // public sealed class ContractGeneratedMethodAttribute : Attribute // { // } + + /// + /// Identifies a method, property or field that does not change any state in the class. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field, Inherited = false)] + public sealed class PureAttribute : Attribute + { + } } \ No newline at end of file diff --git a/DesignContracts/Core/ContractException.cs b/DesignContracts/Core/ContractException.cs index d56df52..59fb70c 100644 --- a/DesignContracts/Core/ContractException.cs +++ b/DesignContracts/Core/ContractException.cs @@ -1,5 +1,3 @@ -using System.Runtime.Serialization; - namespace Odin.DesignContracts { /// diff --git a/DesignContracts/Core/ContractFailedEventArgs.cs b/DesignContracts/Core/ContractFailedEventArgs.cs index 502863d..152d3d0 100644 --- a/DesignContracts/Core/ContractFailedEventArgs.cs +++ b/DesignContracts/Core/ContractFailedEventArgs.cs @@ -1,7 +1,7 @@ namespace Odin.DesignContracts { /// - /// Provides data for the event. + /// Provides data for the event. /// public sealed class ContractFailedEventArgs : EventArgs { diff --git a/DesignContracts/Core/ContractRuntime.cs b/DesignContracts/Core/ContractRuntime.cs deleted file mode 100644 index f2f03e0..0000000 --- a/DesignContracts/Core/ContractRuntime.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; - -namespace Odin.DesignContracts -{ - /// - /// Provides access to runtime configuration for design contract evaluation. - /// - /// - /// This type is intended for application startup configuration. For most usage, - /// prefer calling with values read from - /// configuration or environment variables. - /// - public static class ContractRuntime - { - private const string EnvPostconditions = "ODIN_CONTRACTS_ENABLE_POSTCONDITIONS"; - private const string EnvInvariants = "ODIN_CONTRACTS_ENABLE_INVARIANTS"; - - private static readonly object Sync = new(); - - private static ContractSettings _settings = CreateDefaultSettings(); - - /// - /// Gets a snapshot of the current contract settings. - /// - public static ContractSettings Settings - { - get - { - lock (Sync) - { - // Return a shallow copy to avoid external mutation. - return new ContractSettings - { - EnablePostconditions = _settings.EnablePostconditions, - EnableInvariants = _settings.EnableInvariants - }; - } - } - } - - /// - /// Gets a value indicating whether postconditions are evaluated at runtime. - /// - public static bool PostconditionsEnabled => Settings.EnablePostconditions; - - /// - /// Gets a value indicating whether invariants are evaluated at runtime. - /// - public static bool InvariantsEnabled => Settings.EnableInvariants; - - /// - /// Configures the runtime evaluation behavior for design contracts. - /// - /// - /// The settings to apply. A copy of the argument is stored; modifications - /// to the instance after this call do not affect runtime behavior. - /// - /// is null. - public static void Configure(ContractSettings settings) - { - if (settings is null) - throw new ArgumentNullException(nameof(settings)); - - lock (Sync) - { - _settings = new ContractSettings - { - EnablePostconditions = settings.EnablePostconditions, - EnableInvariants = settings.EnableInvariants - }; - } - } - - /// - /// Resets the runtime configuration to the default values. - /// - /// - /// Default values are derived from environment variables, falling back to - /// true for both postconditions and invariants when no values are present. - /// - public static void ResetToDefaults() - { - lock (Sync) - { - _settings = CreateDefaultSettings(); - } - } - - private static ContractSettings CreateDefaultSettings() - { - return new ContractSettings - { - EnablePostconditions = ReadBooleanEnv(EnvPostconditions, defaultValue: true), - EnableInvariants = ReadBooleanEnv(EnvInvariants, defaultValue: true) - }; - } - - private static bool ReadBooleanEnv(string name, bool defaultValue) - { - string? value = Environment.GetEnvironmentVariable(name); - if (string.IsNullOrWhiteSpace(value)) - return defaultValue; - - if (bool.TryParse(value, out bool parsed)) - return parsed; - - // Accept 0/1 as shorthand. - if (value == "0") - return false; - if (value == "1") - return true; - - return defaultValue; - } - } -} diff --git a/DesignContracts/Core/ContractSettings.cs b/DesignContracts/Core/ContractSettings.cs deleted file mode 100644 index 3237bca..0000000 --- a/DesignContracts/Core/ContractSettings.cs +++ /dev/null @@ -1,28 +0,0 @@ -// namespace Odin.DesignContracts -// { -// /// -// /// Represents the configuration for runtime design contract evaluation. -// /// -// /// -// /// Preconditions are always evaluated. This configuration controls the runtime -// /// evaluation of postconditions and invariants. -// /// -// public sealed class ContractSettings -// { -// /// -// /// Gets or sets a value indicating whether postconditions should be evaluated at runtime. -// /// -// /// -// /// When false, calls to become no-ops. -// /// -// public bool EnablePostconditions { get; set; } = true; -// -// /// -// /// Gets or sets a value indicating whether object invariants should be evaluated at runtime. -// /// -// /// -// /// When false, calls to become no-ops. -// /// -// public bool EnableInvariants { get; set; } = true; -// } -// } \ No newline at end of file diff --git a/DesignContracts/Core/DesignContractOptions.cs b/DesignContracts/Core/DesignContractOptions.cs new file mode 100644 index 0000000..b388f14 --- /dev/null +++ b/DesignContracts/Core/DesignContractOptions.cs @@ -0,0 +1,45 @@ +namespace Odin.DesignContracts +{ + /// + /// Represents the configuration for runtime design contract evaluation. + /// + /// + /// Preconditions are always evaluated. This configuration controls whether runtime + /// evaluation of postconditions and invariants are skipped or not. + /// + public sealed class DesignContractOptions + { + private static DesignContractOptions? _current; + + /// + /// Static facade to the current runtime instance of DesignContractOptions, which must be set + /// early on in application startup by calling Initialize. + /// + /// + public static DesignContractOptions Current => + _current ?? throw new InvalidOperationException("Current DesignContractOptions not initialized."); + + /// + /// + /// + /// + public static void Initialize(DesignContractOptions options) + => _current = options; + + /// + /// Gets or sets a value indicating whether postconditions should be evaluated at runtime. + /// + /// + /// When false, calls to become no-ops. + /// + public bool EnablePostconditions { get; init; } = false; + + /// + /// Gets or sets a value indicating whether object invariants should be evaluated at runtime. + /// + /// + /// When false, calls to become no-ops. + /// + public bool EnableInvariants { get; init; } = false; + } +} \ No newline at end of file diff --git a/DesignContracts/Core/Contract-New.cs b/DesignContracts/Core/Postcondition.cs similarity index 87% rename from DesignContracts/Core/Contract-New.cs rename to DesignContracts/Core/Postcondition.cs index f9a062d..0de8380 100644 --- a/DesignContracts/Core/Contract-New.cs +++ b/DesignContracts/Core/Postcondition.cs @@ -1,7 +1,12 @@ -namespace Odin.DesignContracts; - -public static partial class Contract +namespace Odin.DesignContracts { + + /// + /// Provides methods for runtime validation and enforcement of postconditions, + /// ensuring that the supplier class has met their advertised\agreed obligations. + /// + public static class Postcondition + { /// /// Represents the return value of the enclosing method for use within postconditions. /// @@ -51,20 +56,20 @@ public static void EndContractBlock() /// public static void Ensures(bool condition, string? userMessage = null, string? conditionText = null) { - if (!ContractRuntime.PostconditionsEnabled) + if (!DesignContractOptions.Current.EnablePostconditions) { return; } if (!condition) { - ReportFailure( + Contract.ReportFailure( ContractFailureKind.Postcondition, userMessage, conditionText); } } - + /// /// Specifies an object invariant that must hold true whenever the object is in a valid state. /// @@ -75,18 +80,18 @@ public static void Ensures(bool condition, string? userMessage = null, string? c /// Invariants are evaluated only when is true. /// Calls to this method become no-ops when invariants are disabled. /// It is expected that source-generated code will invoke invariant methods - /// (marked with ) at appropriate points. + /// (marked with ) at appropriate points. /// public static void Invariant(bool condition, string? userMessage = null, string? conditionText = null) { - if (!ContractRuntime.InvariantsEnabled) + if (!DesignContractOptions.Current.EnableInvariants) { return; } if (!condition) { - ReportFailure( + Contract.ReportFailure( ContractFailureKind.Invariant, userMessage, conditionText); @@ -107,8 +112,8 @@ public static void Assert(bool condition, string? userMessage = null, string? co { if (!condition) { - ReportFailure( - ContractFailureKind.Assert, + Contract.ReportFailure( + ContractFailureKind.Assertion, userMessage, conditionText); } @@ -128,11 +133,11 @@ public static void Assume(bool condition, string? userMessage = null, string? co { if (!condition) { - ReportFailure( - ContractFailureKind.Assume, + Contract.ReportFailure( + ContractFailureKind.Assumption, userMessage, conditionText); } } - -} + } +} \ No newline at end of file diff --git a/DesignContracts/Core/Precondition.cs b/DesignContracts/Core/Precondition.cs index 9bbfe65..6a0e68f 100644 --- a/DesignContracts/Core/Precondition.cs +++ b/DesignContracts/Core/Precondition.cs @@ -2,8 +2,7 @@ /// /// Provides methods for runtime validation and enforcement of preconditions. -/// These assertions are used to ensure that client callers meet the preconditions -/// advertised as required for a given method. +/// ensuring that the calling consumer has met the agreed\advertised requirements. /// public static class Precondition { @@ -30,7 +29,7 @@ public static void Requires(bool precondition, string? userMessage = null, strin public static void RequiresNotNull(object? argument, string? userMessage = "Argument must not be null" , string? conditionText = null) { - Requires(argument != null, userMessage, conditionText); + Requires(argument != null, userMessage, conditionText); } /// diff --git a/DesignContracts/CoreTests/PreconditionTests.cs b/DesignContracts/CoreTests/PreconditionTests.cs new file mode 100644 index 0000000..1f1e680 --- /dev/null +++ b/DesignContracts/CoreTests/PreconditionTests.cs @@ -0,0 +1,60 @@ +using Odin.DesignContracts; +using NUnit.Framework; + +namespace Tests.Odin.DesignContracts +{ + [TestFixture] + public sealed class PreconditionTests + { + [Test] + [TestCase("not fred.", "(arg != fred)", "Precondition not met: not fred. [Condition: (arg != fred)]")] + [TestCase("not fred.", " ", "Precondition not met: not fred.")] + [TestCase("not fred.", null, "Precondition not met: not fred.")] + [TestCase("not fred.", "", "Precondition not met: not fred.")] + [TestCase("", "", "Precondition not met.")] + [TestCase("", null, "Precondition not met.")] + [TestCase(" ", null, "Precondition not met.")] + [TestCase(null, null, "Precondition not met.")] + [TestCase(null, "", "Precondition not met.")] + [TestCase(null, " ", "Precondition not met.")] + [TestCase(null, "(arg==0)", "Precondition not met: (arg==0)")] + public void Requires_throws_exception_with_correct_message_on_precondition_failure(string conditionDescription, string? conditionText, string expectedExceptionMessage) + { + ContractException? ex = Assert.Throws(() => Precondition.Requires(false, conditionDescription,conditionText)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex!.Message, Is.EqualTo(expectedExceptionMessage), "Exception message is incorrect"); + } + + [Test] + public void Requires_does_not_throw_exception_on_precondition_success() + { + Assert.DoesNotThrow(() => Precondition.Requires(true, "Message"), "Precondition success must not throw an Exception"); + } + + [Test] + public void Requires_not_null_throws_contract_exception_if_argument_null() + { + ContractException? exception = Assert.Throws(() => + Precondition.RequiresNotNull(null as string, "myArg is required.")); + + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Is.EqualTo("Precondition not met: myArg is required.")); + Assert.That(exception.UserMessage, Is.EqualTo("myArg is required.")); + } + } + + [TestFixture(typeof(ArgumentNullException))] + [TestFixture(typeof(ArgumentException))] + [TestFixture(typeof(DivideByZeroException))] + public sealed class PreconditionGenericTests where TException : Exception + { + [Test] + public void Requires_throws_specific_exception_on_precondition_failure() + { + TException? ex = Assert.Throws(() => Precondition.Requires(false, "msg")); + + Assert.That(ex, Is.Not.Null); + Assert.That(ex, Is.InstanceOf()); + } + } +} \ No newline at end of file diff --git a/DesignContracts/Tests/Tests.Odin.DesignContracts.csproj b/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj similarity index 76% rename from DesignContracts/Tests/Tests.Odin.DesignContracts.csproj rename to DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj index 1b6438c..18ee214 100644 --- a/DesignContracts/Tests/Tests.Odin.DesignContracts.csproj +++ b/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj @@ -2,7 +2,6 @@ net8.0;net9.0;net10.0 enable - Tests.Odin.DesignContracts true true 1591; @@ -13,12 +12,10 @@ - - diff --git a/DesignContracts/Rewriter/AssemblyInfo.cs b/DesignContracts/Rewriter/AssemblyInfo.cs index 43847b6..e8ea779 100644 --- a/DesignContracts/Rewriter/AssemblyInfo.cs +++ b/DesignContracts/Rewriter/AssemblyInfo.cs @@ -1,4 +1,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Tests.Odin.DesignContracts")] +[assembly: InternalsVisibleTo("Tests.Odin.DesignContracts.Rewriter")] diff --git a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj index a0bce58..87fa4b3 100644 --- a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj @@ -10,7 +10,6 @@ - diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 434a8af..6f7e208 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; @@ -9,14 +5,16 @@ namespace Odin.DesignContracts.Rewriter; /// -/// Build-time IL rewriter that injects Design-by-Contract postconditions into method exit paths. +/// Build-time IL rewriter that injects Design-by-Contract postconditions into method exit paths, +/// as well as Design-by-Contract class invariant calls at both entry to and exit from all +/// public members on the API surface, unless marked 'Pure'. /// -internal static class Program +internal static class RewriterProgram { private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; - private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ContractInvariantMethodAttribute"; - private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ContractInvariantMethodAttribute"; + private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ClassInvariantMethodAttribute"; + private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ClassInvariantMethodAttribute"; private const string PureAttributeFullName = "System.Diagnostics.Contracts.PureAttribute"; private static int Main(string[] args) @@ -98,8 +96,8 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) private static bool TryRewriteMethod(TypeDefinition declaringType, MethodDefinition method, MethodDefinition? invariantMethod) { // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. - if (method.IsAsync) - return false; + // if (method.IsAsync) + // return false; method.Body.SimplifyMacros(); @@ -252,7 +250,7 @@ private static bool HasAttribute(ICustomAttributeProvider provider, string attri { string names = string.Join(", ", candidates.Select(m => m.FullName)); throw new InvalidOperationException( - $"Type '{type.FullName}' has multiple invariant methods. Exactly one method may be marked with ContractInvariantMethodAttribute. Candidates: {names}"); + $"Type '{type.FullName}' has multiple invariant methods. Exactly one method may be marked with ClassInvariantMethodAttribute. Candidates: {names}"); } MethodDefinition invariant = candidates[0]; diff --git a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs new file mode 100644 index 0000000..c76ff9b --- /dev/null +++ b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs @@ -0,0 +1,184 @@ +using System.Reflection; +using System.Runtime.Loader; +using Mono.Cecil; +using NUnit.Framework; +using Odin.DesignContracts; +using Odin.DesignContracts.Rewriter; +using Tests.Odin.DesignContracts.Rewriter.Targets; + +namespace Tests.Odin.DesignContracts.Rewriter; + +[TestFixture] +public sealed class InvariantWeavingRewriterTests +{ + [SetUp] + public void SetUp() + { + // Ensure invariants are enabled even if the test environment sets env vars. + DesignContractOptions.Initialize(new DesignContractOptions + { + EnableInvariants = true, + EnablePostconditions = true + }); + } + + [Test] + public void Public_constructor_runs_invariant_on_exit() + { + using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Assert.That(DesignContractOptions.Current.EnableInvariants, Is.True); + Assert.That(DesignContractOptions.Current.EnablePostconditions, Is.True); + + Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + ContractException ex = Assert.Throws(() => + { + try + { + Activator.CreateInstance(t, -1); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Public_method_runs_invariant_on_entry() + { + using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(InvariantTarget.Increment)); })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Public_method_runs_invariant_on_exit() + { + using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + + ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(InvariantTarget.MakeInvalid)); })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Pure_method_is_excluded_from_invariant_weaving() + { + using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + // If [Pure] is honoured, this returns the invalid value without invariant checks throwing. + object? result = Invoke(t, instance, nameof(InvariantTarget.PureGetValue)); + Assert.That(result, Is.EqualTo(-1)); + } + + [Test] + public void Pure_property_is_excluded_from_invariant_weaving() + { + using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + PropertyInfo p = t.GetProperty(nameof(InvariantTarget.PureValue))!; + object? value = p.GetValue(instance); + Assert.That(value, Is.EqualTo(-1)); + } + + [Test] + public void Non_pure_property_is_woven_and_checks_invariants() + { + using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + object instance = Activator.CreateInstance(t, 1)!; + SetPrivateField(t, instance, "_value", -1); + + PropertyInfo p = t.GetProperty(nameof(InvariantTarget.NonPureValue))!; + + ContractException ex = Assert.Throws(() => + { + try + { + _ = p.GetValue(instance); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + })!; + + Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + + [Test] + public void Multiple_invariant_methods_causes_rewrite_to_fail() + { + // Arrange: create a temp copy of this test assembly and inject a second [ClassInvariantMethod]. + string originalPath = typeof(InvariantTarget).Assembly.Location; + using var temp = new TempDirectory(); + + string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); + File.Copy(originalPath, inputPath, overwrite: true); + + using (AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) + { + TypeDefinition targetType = ad.MainModule.GetType(typeof(InvariantTarget).FullName!) + ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); + + MethodDefinition increment = targetType.Methods + .First(m => m.Name == nameof(InvariantTarget.Increment)); + + // Add a second invariant attribute to a different method. + var ctor = typeof(ClassInvariantMethodAttribute).GetConstructor(Type.EmptyTypes) + ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); + var ctorRef = ad.MainModule.ImportReference(ctor); + increment.CustomAttributes.Add(new CustomAttribute(ctorRef)); + + ad.Write(inputPath); + } + + // Act + Assert + string outputPath = Path.Combine(temp.Path, "out.dll"); + Assert.Throws(() => RewriterProgram.RewriteAssembly(inputPath, outputPath)); + } + + private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) + { + FieldInfo f = declaringType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException($"Missing field '{fieldName}'."); + f.SetValue(instance, value); + } + + private static object? Invoke(Type declaringType, object instance, string methodName) + { + MethodInfo m = declaringType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public) + ?? throw new InvalidOperationException($"Missing method '{methodName}'."); + + try + { + return m.Invoke(instance, null); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + } +} + diff --git a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs new file mode 100644 index 0000000..6ade61b --- /dev/null +++ b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs @@ -0,0 +1,82 @@ +using System.Reflection; +using System.Runtime.Loader; +using Odin.DesignContracts; +using Odin.DesignContracts.Rewriter; + +namespace Tests.Odin.DesignContracts.Rewriter; + +internal sealed class RewrittenAssemblyContext : IDisposable +{ + private readonly AssemblyLoadContext _alc; + private readonly string _tempDir; + + public Assembly RewrittenAssembly { get; } + + public RewrittenAssemblyContext(Assembly sourceAssembly, DesignContractOptions? initializeOptions = null) + { + _tempDir = Path.Combine(Path.GetTempPath(), "rewrite", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + + string inputPath = Path.Combine(_tempDir, Path.GetFileName(sourceAssembly.Location)); + File.Copy(sourceAssembly.Location, inputPath, overwrite: true); + + CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".pdb"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".pdb")))); + CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); + + string outputPath = Path.Combine(_tempDir, "rewritten.dll"); + RewriterProgram.RewriteAssembly(inputPath, outputPath); + + _alc = new TestAssemblyLoadContext(outputPath); + RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); + + // if (initializeOptions == null) return; + // GetTypeOrThrow("Odin.DesignContracts.") + } + + public Type GetTypeOrThrow(string fullName) + => RewrittenAssembly.GetType(fullName, throwOnError: true)! + ?? throw new InvalidOperationException($"Type not found in rewritten assembly: {fullName}"); + + public void Dispose() + { + _alc.Unload(); + + // Best-effort cleanup. Unload is async-ish; ignore IO failures. + try + { + Directory.Delete(_tempDir, recursive: true); + } + catch + { + /* ignore */ + } + } + + private static void CopyIfExists(string from, string to) + { + if (File.Exists(from)) + { + File.Copy(from, to, overwrite: true); + } + } + + private sealed class TestAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + public TestAssemblyLoadContext(string mainAssemblyPath) + : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(mainAssemblyPath); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + string? path = _resolver.ResolveAssemblyToPath(assemblyName); + if (path is null) + return null; + + return LoadFromAssemblyPath(path); + } + } +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/Targets/InvariantTarget.cs b/DesignContracts/RewriterTests/Targets/InvariantTarget.cs new file mode 100644 index 0000000..3c0b930 --- /dev/null +++ b/DesignContracts/RewriterTests/Targets/InvariantTarget.cs @@ -0,0 +1,42 @@ +using Odin.DesignContracts; + +namespace Tests.Odin.DesignContracts.Rewriter.Targets +{ + /// + /// Target type used by . The rewriter is expected to inject + /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// + public sealed class InvariantTarget + { + private int _value; + + public InvariantTarget(int value) + { + _value = value; + } + + [ClassInvariantMethod] + private void ObjectInvariant() + { + Postcondition.Invariant(_value >= 0, "_value must be >= 0", "_value >= 0"); + } + + public void Increment() + { + _value++; + } + + public void MakeInvalid() + { + _value = -1; + } + + [Pure] + public int PureGetValue() => _value; + + [Pure] + public int PureValue => _value; + + public int NonPureValue => _value; + } +} diff --git a/DesignContracts/RewriterTests/TempDirectory.cs b/DesignContracts/RewriterTests/TempDirectory.cs new file mode 100644 index 0000000..4000c11 --- /dev/null +++ b/DesignContracts/RewriterTests/TempDirectory.cs @@ -0,0 +1,24 @@ +namespace Tests.Odin.DesignContracts.Rewriter; + +internal sealed class TempDirectory : IDisposable +{ + public string Path { get; } + + public TempDirectory() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "rewrite", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path); + } + + public void Dispose() + { + try + { + Directory.Delete(Path, recursive: true); + } + catch + { + /* ignore */ + } + } +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj new file mode 100644 index 0000000..a74132e --- /dev/null +++ b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj @@ -0,0 +1,23 @@ + + + net8.0;net9.0;net10.0 + enable + true + true + 1591; + + + + + + + + + + + + + + + + diff --git a/DesignContracts/Tests/ContractTests.cs b/DesignContracts/Tests/ContractTests.cs deleted file mode 100644 index 80af0f0..0000000 --- a/DesignContracts/Tests/ContractTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Odin.DesignContracts; -using NUnit.Framework; - -namespace Tests.Odin.DesignContracts -{ - [TestFixture] - public sealed class ContractTests - { - [Test] - [TestCase("not fred.", "(arg != fred)", "Precondition failed: not fred. [Condition: (arg != fred)]")] - [TestCase("not fred.", " ", "Precondition failed: not fred.")] - [TestCase("not fred.", null, "Precondition failed: not fred.")] - [TestCase("not fred.", "", "Precondition failed: not fred.")] - [TestCase("", "", "Precondition failed.")] - [TestCase("", null, "Precondition failed.")] - [TestCase(" ", null, "Precondition failed.")] - [TestCase(null, null, "Precondition failed.")] - [TestCase(null, "", "Precondition failed.")] - [TestCase(null, " ", "Precondition failed.")] - [TestCase(null, "(arg==0)", "Precondition failed: (arg==0)")] - public void Requires_throws_exception_with_correct_message_on_precondition_failure(string conditionDescription, string? conditionText, string expectedExceptionMessage) - { - ContractException? ex = Assert.Throws(() => Precondition.Requires(false, conditionDescription,conditionText)); - Assert.That(ex, Is.Not.Null); - Assert.That(ex!.Message, Is.EqualTo(expectedExceptionMessage), "Exception message is incorrect"); - } - - [Test] - public void Requires_does_not_throw_exception_on_precondition_success() - { - Assert.DoesNotThrow(() => Precondition.Requires(true, "Message"), "Precondition success must not throw an Exception"); - } - } - - [TestFixture(typeof(ArgumentNullException))] - [TestFixture(typeof(ArgumentException))] - [TestFixture(typeof(DivideByZeroException))] - public sealed class ContractRequiresGenericTests where TException : Exception - { - [Test] - public void Requires_throws_specific_exception_on_precondition_failure() - { - TException? ex = Assert.Throws(() => Precondition.Requires(false, "msg")); - - Assert.That(ex, Is.Not.Null); - Assert.That(ex, Is.InstanceOf()); - } - } -} \ No newline at end of file diff --git a/DesignContracts/Tests/InvariantWeavingRewriterTests.cs b/DesignContracts/Tests/InvariantWeavingRewriterTests.cs deleted file mode 100644 index 3fa90ec..0000000 --- a/DesignContracts/Tests/InvariantWeavingRewriterTests.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System.Reflection; -using System.Runtime.Loader; -using Mono.Cecil; -using NUnit.Framework; -using Odin.DesignContracts; - -namespace Tests.Odin.DesignContracts; - -[TestFixture] -public sealed class InvariantWeavingRewriterTests -{ - [SetUp] - public void SetUp() - { - // Ensure invariants are enabled even if the test environment sets env vars. - ContractRuntime.Configure(new ContractSettings - { - EnableInvariants = true, - EnablePostconditions = true - }); - } - - [Test] - public void Public_constructor_runs_invariant_on_exit() - { - using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); - - Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); - - ContractException ex = Assert.Throws(() => - { - try - { - Activator.CreateInstance(t, -1); - } - catch (TargetInvocationException tie) when (tie.InnerException is not null) - { - throw tie.InnerException; - } - })!; - - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); - } - - [Test] - public void Public_method_runs_invariant_on_entry() - { - using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); - Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); - - object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); - - ContractException ex = Assert.Throws(() => - { - Invoke(t, instance, nameof(RewriterTargets.InvariantTarget.Increment)); - })!; - - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); - } - - [Test] - public void Public_method_runs_invariant_on_exit() - { - using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); - Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); - - object instance = Activator.CreateInstance(t, 1)!; - - ContractException ex = Assert.Throws(() => - { - Invoke(t, instance, nameof(RewriterTargets.InvariantTarget.MakeInvalid)); - })!; - - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); - } - - [Test] - public void Pure_method_is_excluded_from_invariant_weaving() - { - using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); - Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); - - object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); - - // If [Pure] is honoured, this returns the invalid value without invariant checks throwing. - object? result = Invoke(t, instance, nameof(RewriterTargets.InvariantTarget.PureGetValue)); - Assert.That(result, Is.EqualTo(-1)); - } - - [Test] - public void Pure_property_is_excluded_from_invariant_weaving() - { - using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); - Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); - - object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); - - PropertyInfo p = t.GetProperty(nameof(RewriterTargets.InvariantTarget.PureValue))!; - object? value = p.GetValue(instance); - Assert.That(value, Is.EqualTo(-1)); - } - - [Test] - public void Non_pure_property_is_woven_and_checks_invariants() - { - using var ctx = new RewrittenAssemblyContext(typeof(RewriterTargets.InvariantTarget).Assembly); - Type t = ctx.GetTypeOrThrow(typeof(RewriterTargets.InvariantTarget).FullName!); - - object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); - - PropertyInfo p = t.GetProperty(nameof(RewriterTargets.InvariantTarget.NonPureValue))!; - - ContractException ex = Assert.Throws(() => - { - try - { - _ = p.GetValue(instance); - } - catch (TargetInvocationException tie) when (tie.InnerException is not null) - { - throw tie.InnerException; - } - })!; - - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); - } - - [Test] - public void Multiple_invariant_methods_causes_rewrite_to_fail() - { - // Arrange: create a temp copy of this test assembly and inject a second [ContractInvariantMethod]. - string originalPath = typeof(RewriterTargets.InvariantTarget).Assembly.Location; - using var temp = new TempDir(); - - string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); - File.Copy(originalPath, inputPath, overwrite: true); - - using (AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) - { - TypeDefinition targetType = ad.MainModule.GetType(typeof(RewriterTargets.InvariantTarget).FullName!) - ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); - - MethodDefinition increment = targetType.Methods - .First(m => m.Name == nameof(RewriterTargets.InvariantTarget.Increment)); - - // Add a second invariant attribute to a different method. - var ctor = typeof(Odin.DesignContracts.ContractInvariantMethodAttribute).GetConstructor(Type.EmptyTypes) - ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); - var ctorRef = ad.MainModule.ImportReference(ctor); - increment.CustomAttributes.Add(new CustomAttribute(ctorRef)); - - ad.Write(inputPath); - } - - // Act + Assert - string outputPath = Path.Combine(temp.Path, "out.dll"); - Assert.Throws(() => Odin.DesignContracts.Rewriter.Program.RewriteAssembly(inputPath, outputPath)); - } - - private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) - { - FieldInfo f = declaringType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) - ?? throw new InvalidOperationException($"Missing field '{fieldName}'."); - f.SetValue(instance, value); - } - - private static object? Invoke(Type declaringType, object instance, string methodName) - { - MethodInfo m = declaringType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public) - ?? throw new InvalidOperationException($"Missing method '{methodName}'."); - - try - { - return m.Invoke(instance, null); - } - catch (TargetInvocationException tie) when (tie.InnerException is not null) - { - throw tie.InnerException; - } - } - - private sealed class RewrittenAssemblyContext : IDisposable - { - private readonly AssemblyLoadContext _alc; - private readonly string _tempDir; - - public Assembly RewrittenAssembly { get; } - - public RewrittenAssemblyContext(Assembly sourceAssembly) - { - _tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "odindc-rewriter-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - - string inputPath = Path.Combine(_tempDir, Path.GetFileName(sourceAssembly.Location)); - File.Copy(sourceAssembly.Location, inputPath, overwrite: true); - - CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".pdb"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".pdb")))); - CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); - - string outputPath = Path.Combine(_tempDir, "rewritten.dll"); - Odin.DesignContracts.Rewriter.Program.RewriteAssembly(inputPath, outputPath); - - _alc = new TestAssemblyLoadContext(outputPath); - RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); - } - - public Type GetTypeOrThrow(string fullName) - => RewrittenAssembly.GetType(fullName, throwOnError: true)! - ?? throw new InvalidOperationException($"Type not found in rewritten assembly: {fullName}"); - - public void Dispose() - { - _alc.Unload(); - - // Best-effort cleanup. Unload is async-ish; ignore IO failures. - try { Directory.Delete(_tempDir, recursive: true); } catch { /* ignore */ } - } - - private static void CopyIfExists(string from, string to) - { - if (File.Exists(from)) - { - File.Copy(from, to, overwrite: true); - } - } - - private sealed class TestAssemblyLoadContext : AssemblyLoadContext - { - private readonly AssemblyDependencyResolver _resolver; - - public TestAssemblyLoadContext(string mainAssemblyPath) - : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(mainAssemblyPath); - } - - protected override Assembly? Load(AssemblyName assemblyName) - { - string? path = _resolver.ResolveAssemblyToPath(assemblyName); - if (path is null) - return null; - - return LoadFromAssemblyPath(path); - } - } - } - - private sealed class TempDir : IDisposable - { - public string Path { get; } - - public TempDir() - { - Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "odindc-rewriter-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(Path); - } - - public void Dispose() - { - try { Directory.Delete(Path, recursive: true); } catch { /* ignore */ } - } - } -} - -namespace Tests.Odin.DesignContracts.RewriterTargets; - -/// -/// Target type used by . The rewriter is expected to inject -/// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. -/// -public sealed class InvariantTarget -{ - private int _value; - - public InvariantTarget(int value) - { - _value = value; - } - - [Odin.DesignContracts.ContractInvariantMethod] - private void ObjectInvariant() - { - Contract.Invariant(_value >= 0, "_value must be >= 0", "_value >= 0"); - } - - public void Increment() - { - _value++; - } - - public void MakeInvalid() - { - _value = -1; - } - - [System.Diagnostics.Contracts.Pure] - public int PureGetValue() => _value; - - [System.Diagnostics.Contracts.Pure] - public int PureValue => _value; - - public int NonPureValue => _value; -} diff --git a/Email/Office365/Office365EmailSender.cs b/Email/Office365/Office365EmailSender.cs index e8d8ad1..4c408ea 100644 --- a/Email/Office365/Office365EmailSender.cs +++ b/Email/Office365/Office365EmailSender.cs @@ -6,7 +6,6 @@ using Odin.DesignContracts; using Odin.Logging; using Odin.System; -using Contract = Odin.DesignContracts.Contract; namespace Odin.Email; diff --git a/Odin.sln b/Odin.sln index 2485eef..8fe8a70 100644 --- a/Odin.sln +++ b/Odin.sln @@ -89,7 +89,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.Templating", "Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.Utility", "Utility\Tests\Tests.Odin.Utility.csproj", "{52AF2C63-2247-42FD-B3DD-4AD2AFCE0C6D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts", "DesignContracts\Tests\Tests.Odin.DesignContracts.csproj", "{177EDB52-3174-4037-80DD-235222F9817B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts", "DesignContracts\CoreTests\Tests.Odin.DesignContracts.csproj", "{177EDB52-3174-4037-80DD-235222F9817B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.Messaging", "Messaging\Tests\Tests.Odin.Messaging.csproj", "{690312C5-1948-4530-832D-6332625D4E9C}" EndProject @@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Rewrit EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Tooling", "DesignContracts\Tooling\Odin.DesignContracts.Tooling.csproj", "{B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts.Rewriter", "DesignContracts\RewriterTests\Tests.Odin.DesignContracts.Rewriter.csproj", "{320907F6-F4BA-4A1D-AC23-145A5AD06C14}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -235,6 +237,10 @@ Global {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Debug|Any CPU.Build.0 = Debug|Any CPU {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Release|Any CPU.ActiveCfg = Release|Any CPU {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576}.Release|Any CPU.Build.0 = Release|Any CPU + {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE323D9C-635B-EFD3-5B3F-7CE371D8A86A} = {BF440C74-E223-3CBF-8FA7-83F7D164F7C3} @@ -270,5 +276,6 @@ Global {60A2A141-EAC8-4EAB-850D-AF534ABFE98C} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} {E450FC74-0DBE-320A-FE7A-87255CB4DFAB} = {73BA62BB-2B41-2DC4-C714-51B3D4C2A215} + {320907F6-F4BA-4A1D-AC23-145A5AD06C14} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} EndGlobalSection EndGlobal diff --git a/System/Tests/ResultOfTMessageTests.cs b/System/Tests/ResultOfTMessageTests.cs index f280d83..989945e 100644 --- a/System/Tests/ResultOfTMessageTests.cs +++ b/System/Tests/ResultOfTMessageTests.cs @@ -1,5 +1,4 @@ -using System.Text.Json; -using NUnit.Framework; +using NUnit.Framework; using Odin.System; namespace Tests.Odin.System diff --git a/Templating/Tests/Razor/RazorTemplateRendererTests.cs b/Templating/Tests/Razor/RazorTemplateRendererTests.cs index 95dbad3..a89c054 100644 --- a/Templating/Tests/Razor/RazorTemplateRendererTests.cs +++ b/Templating/Tests/Razor/RazorTemplateRendererTests.cs @@ -2,7 +2,6 @@ using NUnit.Framework; using Odin.System; using Odin.Templating; -using RazorLight.Caching; namespace Tests.Odin.Templating.Razor { From 0235de6ceccef001f2e0e6eb7c5fc95d4905289f Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Sat, 20 Dec 2025 14:00:38 +0200 Subject: [PATCH 05/27] Invariant weaving actually working, but unable to resolve test assertion anomaly with ContractException yet... --- DesignContracts/Core/Contract.cs | 71 ++++++++++++++++++- DesignContracts/Core/ContractAttributes.cs | 2 +- DesignContracts/Core/DesignContractOptions.cs | 21 ++++-- DesignContracts/Core/Postcondition.cs | 69 +----------------- DesignContracts/Rewriter/Program.cs | 26 ++++--- .../InvariantTarget.cs | 6 +- ...din.DesignContracts.RewriterTargets.csproj | 13 ++++ .../InvariantWeavingRewriterTests.cs | 27 +++---- .../RewriterTests/RewrittenAssemblyContext.cs | 8 +-- ...Tests.Odin.DesignContracts.Rewriter.csproj | 1 + Odin.sln | 7 ++ 11 files changed, 147 insertions(+), 104 deletions(-) rename DesignContracts/{RewriterTests/Targets => RewriterTestTargets}/InvariantTarget.cs (75%) create mode 100644 DesignContracts/RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj diff --git a/DesignContracts/Core/Contract.cs b/DesignContracts/Core/Contract.cs index 11e5591..5ae4773 100644 --- a/DesignContracts/Core/Contract.cs +++ b/DesignContracts/Core/Contract.cs @@ -9,7 +9,7 @@ namespace Odin.DesignContracts /// System.Diagnostics.Contracts.Contract from the classic .NET Framework, /// but it is implemented independently under the Odin.DesignContracts namespace. /// - internal static class Contract + public static class Contract { /// /// Occurs when a contract fails and before a is thrown. @@ -72,5 +72,74 @@ internal static string BuildFailureMessage(ContractFailureKind kind, string? use return $"{GetKindFailedText(kind)}."; } + + /// + /// Specifies an object invariant that must hold true whenever the object is in a valid state. + /// + /// The condition that must be true. + /// An optional message describing the invariant. + /// An optional text representation of the condition expression. + /// + /// Invariants are evaluated only when is true. + /// Calls to this method become no-ops when invariants are disabled. + /// It is expected that source-generated code will invoke invariant methods + /// (marked with ) at appropriate points. + /// + public static void Invariant(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!DesignContractOptions.Current.EnableInvariants) + { + return; + } + + if (!condition) + { + Contract.ReportFailure( + ContractFailureKind.Invariant, + userMessage, + conditionText); + } + } + + /// + /// Specifies an assertion that must hold true at the given point in the code. + /// + /// The condition that must be true. + /// An optional message describing the assertion. + /// An optional text representation of the condition expression. + /// + /// Assertions are always evaluated at runtime. + /// + public static void Assert(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!condition) + { + Contract.ReportFailure( + ContractFailureKind.Assertion, + userMessage, + conditionText); + } + } + + /// + /// Specifies an assumption that the analysis environment may rely on. + /// + /// The condition that is assumed to be true. + /// An optional message describing the assumption. + /// An optional text representation of the condition expression. + /// + /// At runtime, behaves identically to , + /// but analyzers may interpret assumptions differently. + /// + public static void Assume(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!condition) + { + Contract.ReportFailure( + ContractFailureKind.Assumption, + userMessage, + conditionText); + } + } } } \ No newline at end of file diff --git a/DesignContracts/Core/ContractAttributes.cs b/DesignContracts/Core/ContractAttributes.cs index 6f6d8f7..74d9893 100644 --- a/DesignContracts/Core/ContractAttributes.cs +++ b/DesignContracts/Core/ContractAttributes.cs @@ -5,7 +5,7 @@ namespace Odin.DesignContracts /// /// /// Methods marked with this attribute are expected to be private, parameterless, - /// and to invoke for each invariant. + /// and to invoke for each invariant. /// Source generators can use this attribute to discover and invoke invariant methods /// at appropriate points (for example, at the end of constructors and public methods). /// diff --git a/DesignContracts/Core/DesignContractOptions.cs b/DesignContracts/Core/DesignContractOptions.cs index b388f14..58888fe 100644 --- a/DesignContracts/Core/DesignContractOptions.cs +++ b/DesignContracts/Core/DesignContractOptions.cs @@ -16,8 +16,21 @@ public sealed class DesignContractOptions /// early on in application startup by calling Initialize. /// /// - public static DesignContractOptions Current => - _current ?? throw new InvalidOperationException("Current DesignContractOptions not initialized."); + public static DesignContractOptions Current + { + get + { + // Temporary hack so I can continue with testing the actual rewriting... + if (_current is null) + { + _current = new DesignContractOptions() { EnableInvariants = true, EnablePostconditions = true }; + } + return _current; + } + } + + // Unable to understand how to get around initializing 1 static instance of Current in NUnit test runs. + // throw new InvalidOperationException("Current DesignContractOptions not initialized."); /// /// @@ -32,7 +45,7 @@ public static void Initialize(DesignContractOptions options) /// /// When false, calls to become no-ops. /// - public bool EnablePostconditions { get; init; } = false; + public bool EnablePostconditions { get; init; } = true; /// /// Gets or sets a value indicating whether object invariants should be evaluated at runtime. @@ -40,6 +53,6 @@ public static void Initialize(DesignContractOptions options) /// /// When false, calls to become no-ops. /// - public bool EnableInvariants { get; init; } = false; + public bool EnableInvariants { get; init; } = true; } } \ No newline at end of file diff --git a/DesignContracts/Core/Postcondition.cs b/DesignContracts/Core/Postcondition.cs index 0de8380..1630b1b 100644 --- a/DesignContracts/Core/Postcondition.cs +++ b/DesignContracts/Core/Postcondition.cs @@ -70,74 +70,7 @@ public static void Ensures(bool condition, string? userMessage = null, string? c } } - /// - /// Specifies an object invariant that must hold true whenever the object is in a valid state. - /// - /// The condition that must be true. - /// An optional message describing the invariant. - /// An optional text representation of the condition expression. - /// - /// Invariants are evaluated only when is true. - /// Calls to this method become no-ops when invariants are disabled. - /// It is expected that source-generated code will invoke invariant methods - /// (marked with ) at appropriate points. - /// - public static void Invariant(bool condition, string? userMessage = null, string? conditionText = null) - { - if (!DesignContractOptions.Current.EnableInvariants) - { - return; - } - - if (!condition) - { - Contract.ReportFailure( - ContractFailureKind.Invariant, - userMessage, - conditionText); - } - } - - /// - /// Specifies an assertion that must hold true at the given point in the code. - /// - /// The condition that must be true. - /// An optional message describing the assertion. - /// An optional text representation of the condition expression. - /// - /// Assertions are always evaluated at runtime. - /// - public static void Assert(bool condition, string? userMessage = null, string? conditionText = null) - { - if (!condition) - { - Contract.ReportFailure( - ContractFailureKind.Assertion, - userMessage, - conditionText); - } - } - - /// - /// Specifies an assumption that the analysis environment may rely on. - /// - /// The condition that is assumed to be true. - /// An optional message describing the assumption. - /// An optional text representation of the condition expression. - /// - /// At runtime, behaves identically to , - /// but analyzers may interpret assumptions differently. - /// - public static void Assume(bool condition, string? userMessage = null, string? conditionText = null) - { - if (!condition) - { - Contract.ReportFailure( - ContractFailureKind.Assumption, - userMessage, - conditionText); - } - } + } } \ No newline at end of file diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 6f7e208..6064a9b 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -14,8 +14,11 @@ internal static class RewriterProgram private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ClassInvariantMethodAttribute"; - private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ClassInvariantMethodAttribute"; - private const string PureAttributeFullName = "System.Diagnostics.Contracts.PureAttribute"; + private const string OdinPureAttributeFullName = "Odin.DesignContracts.PureAttribute"; + private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ContractInvariantMethodAttribute"; + private const string BclPureAttributeFullName = "System.Diagnostics.Contracts.PureAttribute"; + private static readonly string[] PureAttributeFullNames = [OdinPureAttributeFullName, BclPureAttributeFullName]; + private static readonly string[] InvariantAttributeFullNames = [OdinInvariantAttributeFullName, BclInvariantAttributeFullName]; private static int Main(string[] args) { @@ -111,13 +114,13 @@ private static bool TryRewriteMethod(TypeDefinition declaringType, MethodDefinit bool isConstructor = method.IsConstructor && !method.IsStatic; bool weaveInvariantOnEntry = canWeaveInvariant && !isInvariantMethodItself && !isConstructor; - bool weaveInvariantOnExit = canWeaveInvariant && !isInvariantMethodItself; + bool weaveInvariantOnExit = canWeaveInvariant && !isInvariantMethodItself; // Exclude [Pure] methods and [Pure] properties (for accessors). if (!isConstructor && weaveInvariantOnEntry && IsPure(declaringType, method)) { weaveInvariantOnEntry = false; - weaveInvariantOnExit = false; + weaveInvariantOnExit = false; } // If we have no invariant weaving and no postconditions, skip quickly. @@ -220,27 +223,30 @@ private static bool IsPublicInstanceMethodOrAccessor(MethodDefinition method) private static bool IsPure(TypeDefinition declaringType, MethodDefinition method) { - if (HasAttribute(method, PureAttributeFullName)) + if (method.HasAnyAttributeIn(PureAttributeFullNames)) return true; // For accessors, also honour [Pure] on the property itself. if (method.IsGetter || method.IsSetter) { PropertyDefinition? prop = declaringType.Properties.FirstOrDefault(p => p.GetMethod == method || p.SetMethod == method); - if (prop is not null && HasAttribute(prop, PureAttributeFullName)) + if (prop is not null && prop.HasAnyAttributeIn(PureAttributeFullNames)) return true; } return false; } - private static bool HasAttribute(ICustomAttributeProvider provider, string attributeFullName) - => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => a.AttributeType.FullName == attributeFullName); + // private static bool HasAttribute(ICustomAttributeProvider provider, string attributeFullName) + // => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => a.AttributeType.FullName == attributeFullName); + + private static bool HasAnyAttributeIn(this ICustomAttributeProvider provider, string[] attributeFullNames) + => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => attributeFullNames.Contains(a.AttributeType.FullName)); private static MethodDefinition? FindInvariantMethodOrThrow(TypeDefinition type) { List candidates = type.Methods - .Where(m => HasAttribute(m, OdinInvariantAttributeFullName) || HasAttribute(m, BclInvariantAttributeFullName)) + .Where(m => m.HasAnyAttributeIn(InvariantAttributeFullNames)) .ToList(); if (candidates.Count == 0) @@ -369,4 +375,4 @@ private static Instruction CloneInstruction(Instruction inst) $"Unsupported operand type in contract block cloning: {inst.Operand.GetType().FullName} (opcode {inst.OpCode}).") }; } -} +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/Targets/InvariantTarget.cs b/DesignContracts/RewriterTestTargets/InvariantTarget.cs similarity index 75% rename from DesignContracts/RewriterTests/Targets/InvariantTarget.cs rename to DesignContracts/RewriterTestTargets/InvariantTarget.cs index 3c0b930..5c771a9 100644 --- a/DesignContracts/RewriterTests/Targets/InvariantTarget.cs +++ b/DesignContracts/RewriterTestTargets/InvariantTarget.cs @@ -1,9 +1,9 @@ using Odin.DesignContracts; -namespace Tests.Odin.DesignContracts.Rewriter.Targets +namespace Tests.Odin.DesignContracts.RewriterTargets { /// - /// Target type used by . The rewriter is expected to inject + /// The rewriter is expected to inject /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. /// public sealed class InvariantTarget @@ -18,7 +18,7 @@ public InvariantTarget(int value) [ClassInvariantMethod] private void ObjectInvariant() { - Postcondition.Invariant(_value >= 0, "_value must be >= 0", "_value >= 0"); + Contract.Invariant(_value >= 0, "_value must be >= 0", "_value >= 0"); } public void Increment() diff --git a/DesignContracts/RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj b/DesignContracts/RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj new file mode 100644 index 0000000..7520565 --- /dev/null +++ b/DesignContracts/RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj @@ -0,0 +1,13 @@ + + + net8.0;net9.0;net10.0 + true + enable + Target classes for the Design Contract rewriter to rewrite + + 1591;1573; + + + + + \ No newline at end of file diff --git a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs index c76ff9b..e249398 100644 --- a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs +++ b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs @@ -1,10 +1,9 @@ using System.Reflection; -using System.Runtime.Loader; using Mono.Cecil; using NUnit.Framework; using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; -using Tests.Odin.DesignContracts.Rewriter.Targets; +using Tests.Odin.DesignContracts.RewriterTargets; namespace Tests.Odin.DesignContracts.Rewriter; @@ -31,7 +30,8 @@ public void Public_constructor_runs_invariant_on_exit() Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); - ContractException ex = Assert.Throws(() => + // + ContractException? ex = Assert.Throws(() => { try { @@ -41,12 +41,12 @@ public void Public_constructor_runs_invariant_on_exit() { throw tie.InnerException; } - })!; - - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + }); + Assert.That(ex, Is.Not.Null); + Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } - [Test] + [Test][Description("Increment would only cause an Invariant exception at method entry, as at exit value = 0 would satisfy the invariant.")] public void Public_method_runs_invariant_on_entry() { using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); @@ -60,7 +60,7 @@ public void Public_method_runs_invariant_on_entry() Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } - [Test] + [Test][Description("MakeInvalid would only cause an Invariant exception at method exit, not entry.")] public void Public_method_runs_invariant_on_exit() { using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); @@ -137,9 +137,9 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail() string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); File.Copy(originalPath, inputPath, overwrite: true); - using (AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) + using (AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) { - TypeDefinition targetType = ad.MainModule.GetType(typeof(InvariantTarget).FullName!) + TypeDefinition targetType = assemblyDefinition.MainModule.GetType(typeof(InvariantTarget).FullName!) ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); MethodDefinition increment = targetType.Methods @@ -148,15 +148,16 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail() // Add a second invariant attribute to a different method. var ctor = typeof(ClassInvariantMethodAttribute).GetConstructor(Type.EmptyTypes) ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); - var ctorRef = ad.MainModule.ImportReference(ctor); + var ctorRef = assemblyDefinition.MainModule.ImportReference(ctor); increment.CustomAttributes.Add(new CustomAttribute(ctorRef)); - ad.Write(inputPath); + assemblyDefinition.Write(inputPath); } // Act + Assert string outputPath = Path.Combine(temp.Path, "out.dll"); - Assert.Throws(() => RewriterProgram.RewriteAssembly(inputPath, outputPath)); + InvalidOperationException? expectedError = Assert.Throws(() => RewriterProgram.RewriteAssembly(inputPath, outputPath)); + Assert.That(expectedError, Is.Not.Null); } private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) diff --git a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs index 6ade61b..4d7549a 100644 --- a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs +++ b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs @@ -5,6 +5,9 @@ namespace Tests.Odin.DesignContracts.Rewriter; +/// +/// Encapsulates explicit rewriting of an assembly for testing... +/// internal sealed class RewrittenAssemblyContext : IDisposable { private readonly AssemblyLoadContext _alc; @@ -12,7 +15,7 @@ internal sealed class RewrittenAssemblyContext : IDisposable public Assembly RewrittenAssembly { get; } - public RewrittenAssemblyContext(Assembly sourceAssembly, DesignContractOptions? initializeOptions = null) + public RewrittenAssemblyContext(Assembly sourceAssembly) { _tempDir = Path.Combine(Path.GetTempPath(), "rewrite", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); @@ -28,9 +31,6 @@ public RewrittenAssemblyContext(Assembly sourceAssembly, DesignContractOptions? _alc = new TestAssemblyLoadContext(outputPath); RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); - - // if (initializeOptions == null) return; - // GetTypeOrThrow("Odin.DesignContracts.") } public Type GetTypeOrThrow(string fullName) diff --git a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj index a74132e..dd25565 100644 --- a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj @@ -17,6 +17,7 @@ + diff --git a/Odin.sln b/Odin.sln index 202c6bf..e1feb16 100644 --- a/Odin.sln +++ b/Odin.sln @@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Toolin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts.Rewriter", "DesignContracts\RewriterTests\Tests.Odin.DesignContracts.Rewriter.csproj", "{320907F6-F4BA-4A1D-AC23-145A5AD06C14}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts.RewriterTargets", "DesignContracts\RewriterTestTargets\Tests.Odin.DesignContracts.RewriterTargets.csproj", "{7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -241,6 +243,10 @@ Global {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Debug|Any CPU.Build.0 = Debug|Any CPU {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Release|Any CPU.ActiveCfg = Release|Any CPU {320907F6-F4BA-4A1D-AC23-145A5AD06C14}.Release|Any CPU.Build.0 = Release|Any CPU + {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE323D9C-635B-EFD3-5B3F-7CE371D8A86A} = {BF440C74-E223-3CBF-8FA7-83F7D164F7C3} @@ -277,5 +283,6 @@ Global {B25A8EAA-5FD6-4A47-96A1-2C7108FC4576} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} {E450FC74-0DBE-320A-FE7A-87255CB4DFAB} = {73BA62BB-2B41-2DC4-C714-51B3D4C2A215} {320907F6-F4BA-4A1D-AC23-145A5AD06C14} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} + {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} EndGlobalSection EndGlobal From ea503f3daec71d89de50a346a8eb68171e4cd2e5 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Sat, 20 Dec 2025 14:50:02 +0200 Subject: [PATCH 06/27] Cater for both flavours of attributes in tests so far... --- DesignContracts/Rewriter/Program.cs | 1 - .../RewriterTestTargets/BclTarget.cs | 42 +++++++ .../{InvariantTarget.cs => OdinTarget.cs} | 12 +- .../RewriterTests/AttributeFlavour.cs | 7 ++ .../InvariantWeavingRewriterTests.cs | 104 ++++++++++++------ 5 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 DesignContracts/RewriterTestTargets/BclTarget.cs rename DesignContracts/RewriterTestTargets/{InvariantTarget.cs => OdinTarget.cs} (68%) create mode 100644 DesignContracts/RewriterTests/AttributeFlavour.cs diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 6064a9b..f8f0b72 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -12,7 +12,6 @@ namespace Odin.DesignContracts.Rewriter; internal static class RewriterProgram { private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; - private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ClassInvariantMethodAttribute"; private const string OdinPureAttributeFullName = "Odin.DesignContracts.PureAttribute"; private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ContractInvariantMethodAttribute"; diff --git a/DesignContracts/RewriterTestTargets/BclTarget.cs b/DesignContracts/RewriterTestTargets/BclTarget.cs new file mode 100644 index 0000000..7d9cb10 --- /dev/null +++ b/DesignContracts/RewriterTestTargets/BclTarget.cs @@ -0,0 +1,42 @@ +using Odin.DesignContracts; + +namespace Tests.Odin.DesignContracts.RewriterTargets +{ + /// + /// The rewriter is expected to inject + /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// + public sealed class BclTarget + { + private int _value; + + public BclTarget(int value) + { + _value = value; + } + + [System.Diagnostics.Contracts.ContractInvariantMethod] + private void ObjectInvariant() + { + Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); + } + + public void Increment() + { + _value++; + } + + public void MakeInvalid() + { + _value = -1; + } + + [System.Diagnostics.Contracts.Pure] + public int PureGetValue() => _value; + + [System.Diagnostics.Contracts.Pure] + public int PureProperty => _value; + + public int NonPureProperty => _value; + } +} diff --git a/DesignContracts/RewriterTestTargets/InvariantTarget.cs b/DesignContracts/RewriterTestTargets/OdinTarget.cs similarity index 68% rename from DesignContracts/RewriterTestTargets/InvariantTarget.cs rename to DesignContracts/RewriterTestTargets/OdinTarget.cs index 5c771a9..ae7bd99 100644 --- a/DesignContracts/RewriterTestTargets/InvariantTarget.cs +++ b/DesignContracts/RewriterTestTargets/OdinTarget.cs @@ -15,10 +15,10 @@ public InvariantTarget(int value) _value = value; } - [ClassInvariantMethod] + [global::Odin.DesignContracts.ClassInvariantMethod] private void ObjectInvariant() { - Contract.Invariant(_value >= 0, "_value must be >= 0", "_value >= 0"); + Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); } public void Increment() @@ -31,12 +31,12 @@ public void MakeInvalid() _value = -1; } - [Pure] + [global::Odin.DesignContracts.Pure] public int PureGetValue() => _value; - [Pure] - public int PureValue => _value; + [global::Odin.DesignContracts.Pure] + public int PureProperty => _value; - public int NonPureValue => _value; + public int NonPureProperty => _value; } } diff --git a/DesignContracts/RewriterTests/AttributeFlavour.cs b/DesignContracts/RewriterTests/AttributeFlavour.cs new file mode 100644 index 0000000..778b7ab --- /dev/null +++ b/DesignContracts/RewriterTests/AttributeFlavour.cs @@ -0,0 +1,7 @@ +namespace Tests.Odin.DesignContracts.Rewriter; + +public enum AttributeFlavour +{ + Odin, + BaseClassLibrary +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs index e249398..3d8bddb 100644 --- a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs +++ b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.Contracts; using System.Reflection; using Mono.Cecil; using NUnit.Framework; using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; using Tests.Odin.DesignContracts.RewriterTargets; +using ContractFailureKind = Odin.DesignContracts.ContractFailureKind; namespace Tests.Odin.DesignContracts.Rewriter; @@ -21,14 +23,41 @@ public void SetUp() }); } + private Type GetTargetTestTypeFor(AttributeFlavour testCase) + { + if (testCase == AttributeFlavour.Odin) + { + return typeof(InvariantTarget); + } + if (testCase == AttributeFlavour.BaseClassLibrary) + { + return typeof(BclTarget); + } + throw new NotSupportedException(testCase.ToString()); + } + + private Type GetClassInvariantAttributeTypeFor(AttributeFlavour testCase) + { + if (testCase == AttributeFlavour.Odin) + { + return typeof(ClassInvariantMethodAttribute); + } + if (testCase == AttributeFlavour.BaseClassLibrary) + { + return typeof(ContractInvariantMethodAttribute); + } + throw new NotSupportedException(testCase.ToString()); + } + [Test] - public void Public_constructor_runs_invariant_on_exit() + public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour testCase) { - using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); + Type targetType = GetTargetTestTypeFor(testCase); + using var context = new RewrittenAssemblyContext(targetType.Assembly); Assert.That(DesignContractOptions.Current.EnableInvariants, Is.True); Assert.That(DesignContractOptions.Current.EnablePostconditions, Is.True); - - Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + + Type t = context.GetTypeOrThrow(targetType.FullName!); // ContractException? ex = Assert.Throws(() => @@ -46,11 +75,13 @@ public void Public_constructor_runs_invariant_on_exit() Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } - [Test][Description("Increment would only cause an Invariant exception at method entry, as at exit value = 0 would satisfy the invariant.")] - public void Public_method_runs_invariant_on_entry() + [Test] + [Description("Increment would only cause an Invariant exception at method entry, as at exit value = 0 would satisfy the invariant.")] + public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour testCase) { - using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); - Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + Type targetType = GetTargetTestTypeFor(testCase); + using var context = new RewrittenAssemblyContext(targetType.Assembly); + Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); @@ -60,11 +91,13 @@ public void Public_method_runs_invariant_on_entry() Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } - [Test][Description("MakeInvalid would only cause an Invariant exception at method exit, not entry.")] - public void Public_method_runs_invariant_on_exit() + [Test] + [Description("MakeInvalid would only cause an Invariant exception at method exit, not entry.")] + public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testCase) { - using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); - Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + Type targetType = GetTargetTestTypeFor(testCase); + using var context = new RewrittenAssemblyContext(targetType.Assembly); + Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; @@ -74,43 +107,47 @@ public void Public_method_runs_invariant_on_exit() } [Test] - public void Pure_method_is_excluded_from_invariant_weaving() + public void Pure_method_is_excluded_from_invariant_weaving([Values] AttributeFlavour testCase) { - using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); - Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + Type targetType = GetTargetTestTypeFor(testCase); + using var context = new RewrittenAssemblyContext(targetType.Assembly); + Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); // If [Pure] is honoured, this returns the invalid value without invariant checks throwing. - object? result = Invoke(t, instance, nameof(InvariantTarget.PureGetValue)); + object? result = Invoke(t, instance, "PureGetValue"); + Assert.That(result, Is.EqualTo(-1)); } [Test] - public void Pure_property_is_excluded_from_invariant_weaving() + public void Pure_property_is_excluded_from_invariant_weaving([Values] AttributeFlavour testCase) { - using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); - Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + Type targetType = GetTargetTestTypeFor(testCase); + using var context = new RewrittenAssemblyContext(targetType.Assembly); + Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); - PropertyInfo p = t.GetProperty(nameof(InvariantTarget.PureValue))!; - object? value = p.GetValue(instance); + object? value = Invoke(t, instance, "PureProperty"); + Assert.That(value, Is.EqualTo(-1)); } [Test] - public void Non_pure_property_is_woven_and_checks_invariants() + public void Non_pure_property_is_woven_and_checks_invariants([Values] AttributeFlavour testCase) { - using var context = new RewrittenAssemblyContext(typeof(InvariantTarget).Assembly); - Type t = context.GetTypeOrThrow(typeof(InvariantTarget).FullName!); + Type targetType = GetTargetTestTypeFor(testCase); + using var context = new RewrittenAssemblyContext(targetType.Assembly); + Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); - PropertyInfo p = t.GetProperty(nameof(InvariantTarget.NonPureValue))!; + PropertyInfo p = t.GetProperty("NonPureProperty")!; ContractException ex = Assert.Throws(() => { @@ -127,11 +164,15 @@ public void Non_pure_property_is_woven_and_checks_invariants() Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } - [Test] - public void Multiple_invariant_methods_causes_rewrite_to_fail() + [Test][Description("Test all combinations of having 2 Invariant methods")] + public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] AttributeFlavour firstInvariantFlavour, + [Values] AttributeFlavour secondInvariantFlavour) { // Arrange: create a temp copy of this test assembly and inject a second [ClassInvariantMethod]. - string originalPath = typeof(InvariantTarget).Assembly.Location; + Type firstType = GetTargetTestTypeFor(firstInvariantFlavour); + Type invariantAttributeTestCaseType = GetClassInvariantAttributeTypeFor(firstInvariantFlavour); + + string originalPath = firstType.Assembly.Location; using var temp = new TempDirectory(); string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); @@ -139,14 +180,14 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail() using (AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) { - TypeDefinition targetType = assemblyDefinition.MainModule.GetType(typeof(InvariantTarget).FullName!) + TypeDefinition targetType = assemblyDefinition.MainModule.GetType(firstType.FullName!) ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); MethodDefinition increment = targetType.Methods .First(m => m.Name == nameof(InvariantTarget.Increment)); // Add a second invariant attribute to a different method. - var ctor = typeof(ClassInvariantMethodAttribute).GetConstructor(Type.EmptyTypes) + var ctor = invariantAttributeTestCaseType.GetConstructor(Type.EmptyTypes) ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); var ctorRef = assemblyDefinition.MainModule.ImportReference(ctor); increment.CustomAttributes.Add(new CustomAttribute(ctorRef)); @@ -181,5 +222,4 @@ private static void SetPrivateField(Type declaringType, object instance, string throw tie.InnerException; } } -} - +} \ No newline at end of file From 2ce4c6e048eb39342ddf1eacc0c5c9bb599a9ab1 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Sun, 21 Dec 2025 22:00:36 +0200 Subject: [PATCH 07/27] Exclude analyzers for now --- DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj index 212d2f1..5c73585 100644 --- a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj +++ b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj @@ -6,19 +6,17 @@ true Odin.DesignContracts.Tooling - Design-by-Contract tooling for Odin.DesignContracts: analyzers + IL rewriter integration. + Design-by-Contract tooling for Odin.DesignContracts: Analyzers + IL rewriter integration. icon.png 1591;1573; - - - + From f010691627b5a7e32006b9922d2a3a5544699673 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 22 Dec 2025 07:10:47 +0200 Subject: [PATCH 08/27] Icon mod --- Assets/icon320.png | Bin 113978 -> 96384 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Assets/icon320.png b/Assets/icon320.png index 9bdad527b3318d788a04a607b39d545639b92332..6858f9e54c0034200c450cb11e0081bb33eb5990 100644 GIT binary patch literal 96384 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

n3HJFop9sAj|UUZJnsmh02 zuoK$EHgGMjgpP^L#F2yuB>NlRFA^YzV1CN}cI%3W2j6scg}>+SPj%IOv*&fN>5_jN z&<*ay#edOuAbIGPQ};9!XmC9`2waMxZ#sj$_p7;hADm7!va}V<`pd9}cL$1Lvcs{l z;)n6E_6~*U6mN>M8KbY!;Ll7(%M8msAFxp{aX$_84!!}|7JM^-9k>ZEm^J@Vd*~d5 z!$Ap-V2U`-g> zc4j*89pPwmEj@j=;0zr9Q73tuj-4Nx@BSFJx^r-1+ra;~3&_z_n5V{v4g1bzRu~;= zevu0UVpSjXt&0Gn4alH}G*TB^>%ioiCmR>Rhjn2~PX1tg6V{E1PsTZ`#K00Yd39fZ zNxc)`>5_Uq4!R}oE#`#53a+2YV0i?y}55!BDw+Y!NiTyaC3(TPdO z48I6HmG@wvs;Q89{u)hbqZ9x^^Igj-*R#2{=Uov*p6&5P%rcg7TW}rgQR?3+@%FHJ zuC2>Z0eg&KMGQi{?WU+|@t;iceJwR=z}`!}$W##%%NFaetjsIRc@>QleV*PAPWbsX zUASm4i}33*;$L9yiyyri>yJQwH#^paX2NrRzm+U9&Jbocfz&LoUm&lueIoqQ+$M%er06Q=-RbYkfp^ef-Jhcl=iIJSz!)f3j>7Qq z3fE((4gMgr0Rn?0NRp4xH!><2hi%=H|Gxqqs7OmL)pu)D0oV!rA;^?ZHp z@eNs|rv_T&^9}FBmueB#9z+n=!PLFeOwr4uehXUK#8~W-7*{vY3!5Sg1(z);CQl*o zHvGlOK@%si+23n^o{U!$n;Bdy^UlaC7+>5uA++hm9|; znSvw z^rS$M1vIPeemw0w#^Q=O&To@YevMIU}SDoER=3W1J@Xz=U*2KH!>l}HHB5Hea>r-1SO`uKA;c%^k zp8GM#LbuCYkczBNG9`1O^zZI07yFEox>PrsZ5$F!>vIA23_GvLmwVB}!yl8Qc4W-> zrw@Hk_^!dL#o`9`JvM#ZWIW;X_1e9iJAx6MO}>*^4@(9oJ*yAqP>8QYfmeYBuLsi= z^g;$(?c3}f-4L?$hBX>exz$p&y`iv9kaazH3KNvPxohsX-8&?-$hJYHA_)WEj!oS| zXZu{?Fbm7$tMPLNdRovE7G~&gGWP}9jtVmyzD|f~THAZ~xhN8cYg)eWN4;gEM))w9 zX(Y3Y6{X_u;{EqruZNBWIYrEIy!_F>kc_CO;deNP)*r z>KUB8#Hk->5+X{7WLvvcbwS}5Gh32L-sOd+eX zJO{|^ag&PQuy>-TBnkLOOjHJe%t_Yy$DjX5B&g)3<_0V6K^h=C`%Y^N5nCO$8+OVS zX%OhTOJDuOh-4O$ucca`a@HQ(*c4Km>rx%h_x_D1@TSvg3%UO@t5d@cJON+$0(0A< zL_D1&V@6t~DV671n%92Qsug%^HjT2j_8)?E z>L&h~NpR$VHnIe8>LF=6TG?xlNrs2hx$$=cA|v6d_!n60-%=y#u<bg6RZ4!-Qgi;J4S7BG2teHAAKkc};OcI`VxWIEbR`W$m$nUgW59+kRd zA#-+qvpeWRN>xXExPIfr;sc z-tnVqPW=@(d@;CUDT^!ku6@jOY+=?j`y67v`wcDHPu|VW{rnDky*yP0F@kS(ds>q$`=%MO(S}(ZiXJQ)q$b#Mx zDk}3ApK+$pO*+(*qpuot#`JvkGIC4JS6_nyZ)rgO5b~Z?kbU(jRQm}s#y!wTiOk?m zO^=FUd>ODafbJ?`EgF`Q~q4?8F6Pe zdupo~QLIeb_&(=V)n{HqxVJISyM7iA;!D;{E~I#%DDZHD_wYbu&=n0=L`P~T zQwWK?gWj0GlBRh4K*F@Szf(11l0>k_*s@fKhkR|zX!T5zEUPT0R$I#s{ln|F^NwV5 zdRVjCZqbSU6yO5vk0@xxxysDVR^V#p;k9x9e%by)MCftr#|C?nyS7^(A99YA=L)iE z<2+I}3U-frTs^ugucenVl+Jz&{;4y<<=LlQww*2e{PH+ocZ{+m&^O9nNdqEOODYgU zV?I#?@3TS+6e?JaacoRy@3W>eAS9Ici_t1C3Au5>DeL@gmcZhpWBGh+_pKj7&-nCUl_sjS9#z#^xPpLr`1n3g3(8nL#@Z< zRW5>>dLIbI>(Mkt%2|9~B6fqVkGWCAtpOW; zN~J2L=itT7`~2cuHtsJLyJk@c2$DTS`jHbpJgM<#Kx%fZwhGPD?C%?iMw(T|e%*MR z`s39(DyoMCtM_O7fc(&ada-H#8-m$Q@@gpJT8R`dRyP*R{)sSMqw@j0JH&VvM-IZ9 zG&qOldL6yiGj*S@aiVHEKg(Go(wVod_CLN}J9qY842)bKo5crC&uw~LB!+zf#C1DP zp=lZ`D9I>$>Nc_&|ACHXs$|fzaEZr2g2pu!jFm`@s&#n(ykaUOy@4t~{!8$?U@@OP zB?L#6#qtdH%}vveeb`r1@YaaB=PuX(%L-m!=lzxY14Fr*FDCG2%u z8j{_@2C)XI?=H<*z^lIhnmd_z7Uc_zqcPRpZyCY7&x^EprYJTy7}M4BMtQ!Of9@QD z2IN&A*UtcW7RM~b$C^~l_+O`}Z!gTIs)G$~_#C@s(v1dge}M=LUPsAhPFW*{jcWIV z?j=U%Y`K9Fgo$A-HZfv1tX5cbgXGB(3t~W>zZh(T z=9R_}^HgenEy|_#J%Yr|SPG?Z^URxXfcl+DH%;ejucN%QfIYfFk?BO$)XanDw-a)7 z$Qho7oAu!nmMAbbpyBCw zrR4ZDOB$QPDV9y4w!gOro?tuTt3X-+06+jqL_t*kfl+22c+p3eYRz${IC@=kzBT?- zB9UJgAecewv1IvjC014Hjb|8|UcFdxL~VOEuP5EeRWbF%9os^D|-qjcl^ zfbzC&d$bOb_$aTbyg1L{5stHQ%zefVl#``m_>f8(OB zYW0F5{q$cyt)8w{g_zuS*omLglehj7`?Ut;mzFzGoXzJoODZ!dP07f2%t@^ok%*Z` zggT#xW=F7ji<2TpX@b=7`|@0NDs9_(@Gj|%7!4#LN_SL@sb9KSu4U+ROF8?HZVpN?&i8}eCpVQ-aTm`6MGD@jrfD%v~ z#-_RcS<+7C@cOcqm{|^)9}@$z8JjW~hr#a*;~gfDc_!u7I?RWjT^p4<{}9bEuGP|D zix6{gZXOKdXwLYCmtT09hKVRC!E2bLiQ(bSp=t8Si#wEs_#hKSiPgEy`P^PZduOjx zwi(9vGASipcinZDPTJ4lXIJW=i2$9*kI3$)Km4Bl`0M}FvdSzSyQ)gz35*Ni&gEnU zXc#cRm)b)WQP&|0%CzpS-5MC3bUM90J|E%YnBhPGAl$fRH?Fq5T2PfsI=~R3%oIEX zJpfw2Gs((_O@1<`*bs$Yi_oS?5sDaoo|) zg5%n!O4J*no>qWvRQX;G_emSY!0c!;h%xiSqBwZ0N$-TytxW#R<;8{b`xH{f;|z0vucmR`&Pfpa8(VY45kcez9)8_G+!EEzrVJzlNf- zXc*JEK-AqJX$BHwlmShorm9R0&E^G&V+FN8nzkC|I12zcef=~ts1w>K%9idy>Vi1D zpv_IBKA1qnXcO(tHYkb4!zkU_uJ#Vnff=nnWCbT+tiuBw7$Bv^3fzFoTyf1-D?KyY znQCQxKO;w*=CT(;wMCWb*o&_`;!vhv-KwjuzEeF&fr^Rxl4-CE@NtNA!e$5;F%T(} zT4rxB?AUc>rh1fKSFG2!GH*2@nJ9B)fA1&v_rF^!@VChChO7Um%P+k|D;E~%^3NR& zd!bDe{#u>%k#qI@gV(F4ZI9{}FV!oLKdd66!?;QI1W{fzxl)rDc*G={#=Q@&Um@uz zF3prvV&J%@l$a$xRIT>iH2)^i;!da};878KcQ`Ui)aK*!LwfVg?bQGJR9jUDjb;>U zHsh~O=xOTI#psRalR1SVQ)DH`=!Ux0-HV6-@UyP3m)iNAcipYyjyv{%06)7_@B1c* zf!`=M{x>NE-$qU3$c05(NG+mkn6#2wpr(=a0B%`+kLFbrX!Y_M)z>#W)RUq-W)N^u z(-UU27+qpIvbp?Bzf%XCaMI&g4It2de%)qB)GG=ysh-?{O1<-@rv zC_|}4&2MDJBEL8#XP5IV+zU`tGLUP=flO2I@P?;gDoo=nP864pD$3~kts|tgNq(ms zWZ|1x-Z$g!($m(4vpQ)pk=;0Xv`u$=<+Y7uSZM$*>ZEC0pJ!v$)zhbrF3Lab@gb;Z zMwq^X$nQV`KieViYaXC&j8JGm|AN)epYOXvYpS!fpc2b?=KCfR#T8}w9DA!oQp{ZH zaubk>w=`kVy?muIl z_rLjLZ@|9RiwD_77kox9KK-~>GT-2^x-6vx0u*x-=+2_8O~kj1`Mec*Sk?!!XcUBX z+*xO9`Qj3_?A*+p0Y?6D@rJo*!)%t^oPb(5wG8ykyU#kqHrK#T%^(|~zM%sE^boP- zI8*KiF*da-hRr!03FBq~A}=^M4R5) zrawROthP1vJ9WrxW+C)2Bcu-(Tyx(jz$(ZK2q&Tl36S~*h)}IzVv3Wp@)BjE8!n*( zZR0U>GUA-Z&TH#;>+#1QX9FLG9I(ii%6+_FSAfIL_W2#R-l*?T-YL%U>-!gBDBA=a+LZ_TmqmWTjyB~aASIp0NJ0%){y(|w2OI#}1vgUf*3EA@ za<-@b`h?c6Uk}*z*lwV+mF`eJcJ0`quYK-Q+VIMYIrQA$l5SERQYFzR=#uZ*}kOo!DGZd2&(r3RRdho4E21pWrW>Jfz@o_2q-?+!V?FP-;LkelJu`Aj69MEW~PM{Sa* z)P&}2Ry~o{^z@j5gUN&JVN)}W&@8d$91|Usn#4%U z7-Lm(OonQ`XCJ-y)d5vFS**8d7SZRN!_iU3SK(PSDUE6_Fe>#b+*BT9rX$AqbAY88jCS**t9ZX8}H^b&Pd z6>+)}+V_V58r)>eMaH;+78P2z)grn_pL*SC^Ou(uvq_kPI*Y?Co5rX%zEo0P2+(F# zZ$_xG&<7Hr#6%}hmym83B5T|H`m0*|(soUfJQx9ciZ|EX34s-WOM|qUY6h$PZX1AZ zHOWk!-PfqtQs0UVZ8z znri*3Te?bDUwyMy(Gd=_?~>ATxDR}4YkX71{5=32CJ1$^IJ3g><{RYjwQ&)-WESlx zn9LA$ayqP5R8gg6rIcqTIkdxiYx6EBU2!^O#X=`Fo=D1|6qBHxTX*U8&GpLmlc5x5 zv!~I+bHK;UOgOBG9=LuHc!EjPwr{=djsqI}Y>f|?2+%J2b8DZ`=Rbaq+IDQzkxNVH zn)wAP#tCIp`zSmMRa9KyNPpU4 zvU}+U1BFgRXdNT_*yt^dP3@|I^clnJ7^eXegP}n@1CT=f18Ul}om9hvV#M^riKN+3 z0LY=AMC`?@@jpm6x%nTBQQfjg(R=m6(i#i1u#=i}SsVVB{8+KB4tI62F zCYvKXz~CA4QL8L3fS1_CKx^hW;K5dqnF!Sd)KfDocgjAt!&{qMkzOM2 zB%-S43@2tP$aZCum?bLA2J;+@(d;MGB!2 zwpwB$dP<6W^5C><;AatBS7#@m!i)jnYwzsS{g1Cx|HzaoOLF*ngtQv4<6)8Ck}4+F zQTHn;$bwVI1iG#esfRhjpi-gBX2uO>A2dd7G8s*CvNs-@!-V<2B~yPEiJ_npdIDrrl2SW12l_ z6AQz=9b_yxJu{WfG8%hq+qwf+@_A%eUWc+`hwHYj^^B~X)bgrqWtUbt^r}5O_h>V7 zd(8}?dSRXVxW7iDQ%*^yv8fHo=71AKc&RJi4axbGQ%^bI!O!0Reg{Ci;IFKES(jXJ zo?l@{vdzBUN zJCV=HYpS$p$s+22_y97wt*gCVySaE<>N`~kk1kL=Ps)yAL0bRj{*3O}BE%SM z@nf(8GTMq+4$-%ucG#vjw!+yWdR#Cs-_b+{kpv}x^y$?9aCTo{#3x)EOSB7cK!kPc)RI$*X_6H-@bLBN};j*@M~vjq^Ct28*pObKut(5 zROw<;VY6!s^->Q{Q|*F9WENrgq<*B6xCAkIt#)3K`nuYQ2*zcAGzwkDluB0no*ihW zQ5U7oKA|@@?V?2EQ+airTH86O;lr95DVcqiIXk49!c-;YmpRS4H#tcC9MZ+*6;9yM zKR5)y&0xTRr(bZu(b=1G%x$;bu2WAv^?d<;wgvVp02=tc@Z4IbnK6aQ=qbl7SDK%x z@I+!JxHdVNag}URyvfOqJmX^;??D?o(5;%)r|6iI&tOV?9rZ$t0*JB}quLlJrS+g= zEUm54NFRJZVnj0|9T-UY4Q#`(&*M1aMmR+WP+mB; zL8O~$fS{$DrnU7_12kYXHw|k>BmrLC$b4q0Cmw(JGPN`|Yl_NpoTUV$r#ttwVI|ZL zh;?bh&Mt@hm!Ii#((9J4x6Kp+$dxRb9HVZMhl@rQY4HfZkJgDZ)1;`Z1OO(g>usQeOT&%P zlx9mm1m&t9a}Kj$t0>D?EVfqW_ZLM|J_`|gY$%Al5;X?Rvp@>9>}kqAP^~6Y$PyN> zpm~8k9L+XkGbD4s+o1CzNYL3S(pwEdJ^Az-)T$Z!RgmNGVPk0yS|hHDrb#CfHydb_ z*y+fy+e%kv6{PrsV+67Uj2;M&6bQ z&+e#G&r;$Pn#FJnyrN8^HVg?GHz^OQ$LVLC0RgyPe|_;S`5<_vLRJoBrmA4xQuP3S zQ}~}PsH?=lF^lki3*EElod{}}IlO(C7ua(o;8oV#g55lkW`vZ}QiO4i#CQNWnW$_4 z0g7E~zWk(i*IVkRVM`#|%TI0g*LJR9stErPeOGb)2|a84O3F7Mc=2 zsgleDlq^uUp!-zKgP0Ay;f+mAe1D||I+~nma~W_E6XIrd>@kOEGTg5}UVDe*YGan; zb`9Le{#^mP2KMLb-~Sqt^Cis3&DU2yc7!}g*ZLwb5i+t>TC>=7DP5soaB zIjrGk6qfp(J5)BWR%`(0S`RS7)x7*>*rR556=H0H2SWl1M2-=`bc_deNT&P+)f}qv z$l$shjk?L(4E$#KxxvV!nuFtdo&A&>NLE>CKFz}%1OzDxA?uz=CdzG)m z<2n@@n`YyK7>pxKP*+i6*M8`O1d9=S?73YQotSC~l)LpMC*0Hu)gDVB%^zjD+XT)| z;s$LfwTQB&lA#mP(lScaVF1blLlR5%pwBjLu}IGcGtB4*$==WRqsZ12{CX&nqY*z< z`6?c;fXy9bW&s4zL?V&~C&9uF&}KPS23nu2Qn5w@jxGYJL?HcHc$z9JlgtGCe8V*> z!E1$qTO2e0@OWrM11P}6U|z$3hXw2j{2-VK@FeAR2pr%T9|R?V;fQ85iC=InQ#?Np zrq%Q>PnWe?c4iU_AL~toU1u&_&iP8TY%MzxBkC-LGNuCcz?e7 zxee$i5B$i!{#So#b8-{yGq*0cK*Nf%88kz5I$3;@3vR7-Wo**I@NvyiUG@dHH$>Z| z=;%I_#Is<4q*yzC*YQYK^%5vOpt<=2eNiRThtbf;1nSO!@liVE`;pcU^h_gXo=%NdL zT6O0=0P3Q@`pS!T*KN0$_gKGu@?)FbfT6m&#WnhU2nwM{NJWzAgrdT{XyF{!%OLqe zw=9W3wtz`jyhWxK*w`R`0!UKHda~m5ac=09HrCs2+jbnX)AamGDw+{s1O-p9k?d7< z9iAEip4L!POT(Am9RX2gw)l`ut!FWK>VvfR0&9VxkrE$Hyu+`6{+mP+TvkV^ARV*- z0-VE9F~|^nMFSgAeq@p)Xh1(f@d&CjDrePG5u8VlE~aFW(2ax#RF>yM1j}cTBC4AQ zfmgpLe}x9bPNSi`#l%Mwq~I_k)s=^iVgDG4H$+z93un)WXHpaZ*}Rmcj6& zn-adLqRBEiew9qp5sZjU0lyk)3umWVz-X8K=)NbwY*pF1jT_nh$q1$&=lKjM^E12u z-v6?DzVzn|z~b!RzH%dK5N8v6%z^>-$%1?Q6NR}!mufQ%_yVeCiI&K8R z7}RQW9j8#FY7bI>T~Hq~M_e{sAmp1C6&_$q*KV>GUU`$j3DJz*Qd(#O&~xfrFf(Ez zEH4jFeplsa3)9&SM);ca6348r106GrlLEQ;2`H}Aqs}r&FcUpa5R?ddbfN)iTCD7F zfL&C*6WL*^mnUZ{Pcp+g0&EXEZyY_k5GHIxdJgwI0H1jd1z&PEC!60(KPDO>1~98o z`zVi@=t;`xt#^)FW@ai2JI!qf5~N(Tc!BV|_nUZ(JsYPgaQd2*#V3y5aIM;Y)4EX#GZX_yX`+*h%n`q&g*b;ZGZh$dVU{a5;bTyZC-Bv_!JD2jfmotO)W+fr?Tky!QNIxj{?Z$}F|Uy-x?+rMX@#rh)x z38CT;eMMRB4H0C>`)ZD_|_lpF2#l>QjhC&}!K{^W%ir3-@GK#^pDvofRYdHBe0?Al`+ zueg$PLS&qNOxYRGz3|YFegMe%b2cw4(mr?dLX^H?_aNOSp^V&RSJ{lY%V90mxL%(( z0|sIJ_51CyMWnwNg zxBU4Fx!{w|DJnKGncY(Uq$Ac^b(#`#!gjMmX&mpV--%1L4oW+zvUrk`Yiirr*hqPR z4qt1H4b#v~N{Yv&s>KntgW>DSltujBK|Tuz-OuN7cGeJn7D5IpDQ|(VCEr~Sg(!^i ze?G7THwn>kB=^R%RdD63$>sAN`Y9Yg@^>Q$NV?MhG$ zPZTtLzz@nFK|#W9jw917=xJ}0IK-8URK~JM%!L~$8Ng=6%ZElJ_39bP@(1P zHLkz zQvSy0EU*YR{o0dnn14tV?H-s$l)%D$YVa}GC&m%g2J5D^q1G0z+`t;iXNb{GCfGz4 ziAPK*86G&nQ8v)_=6W{eArrG>0C<>xU?8+ODHwrqMbKB^d^rk+!&yIs^#aS(^64`- z_s|PO$Uyg!5DBde;o7_C8)EEr@4ffhZ6EvCB@z5|g{CwB>Vj{5_G$avr*1c_#q9&D zXPOT|u$`%^%F1ZK#8Rd3!Ekxe>NR+0%(texYUhZSkvALpDx`Gq3zadmC+0-$1ckw# zE@Y${9b-aezyW%S6(wo_r_$R4qQdj3&w)#ZMIuPh1O_h{B6kfyVP%36cT~VqW4EcJ z-yMS=dV$K;TF~B;Q&khePXQ|)+f0cPXj-dgavaCo%5+TGnx`9}coM)QQpJOStX`lG znO$;1H1a}th7ceX00pCw#Femm>RX;=m5|2qNS5LYa^m{A+})%%I7X({${?e@76lHn zy5$S=oN4>^UGLbE#q$W3-gao;ejq}_mX?XqG>!A{n0PBEI}|55DJj-1Y-tIeC~tOC zXMxI_P_&U!P0^_$4XAC_oC22(2m_e~2YTza9X7-x78jnB&6Jll*w(-xkYUA8kApaQv$>Dct`QGi^>YWh ze>roOSrotD)>LH!C_u|fL9HQWd?WOSTN}#V#@fv=qqU`#%^4XtHp`IEFt>4!P!;qL z2>q#iPMxW+b{r08F^sE3gK}`(gK~M;sW}S+TgQNnBFTPq?~m;EkAM7<34Xe|Qx*Vq z!4DlgXm@?=R_q{0Y~wO|6qI4Yy$vx$5Wi5rGfp5Wm_HvWswm1-6jR_ZMsqX!a?u08 zC8=N5gSm@5IegjKCVFb^#2J8~P{dB464Tz^jcRf)wtJy079UrV1|b6J59hH7dfOmb zV>E0Ce}Y^&f&-ueTxO<#*G(pu6oaP*X}(I`ZlEXK;Gld7F!9ji*mw~i2qdr)$T;>M zvcn3P846tiOmTJm`niIjJUa9dN+zjZV{KEL>z#N4_#0#KD)SVZM1LbQ8uc0V{3@lB z(fkwpLlB*KL{7!;74t=dlS;~u*)TjjK<>AKxGyQGu>9Hi7LMO>Jw`Puo8pra$>lI_ z3JzlT_XF%4%f{kErX0unf#GI}v+5X^(H&Wrt2y0QknJ zniA=wciicI_m6k`zdxVJv4V(LI`2>8;qmn^ectktgY9!S%(nqHqH%wiNm5^7N*kMA zz`B`Wg9oYLhh}7F9ewU9WU7Kdu7o--UtcU0h6!9TNf~F$Vb>e%=bnjR)<+rL-pEGT zimO|_%lLYL!&Fz*B7ho8UWHK`|jXs;iN1=tG)DjXIM=Bp}BYo{|EZ;9h3#7WSp~v6_;nLg;O5Q*#Ia3Tx zj{e{{=yu`i2zr_X>n4|^mimiQV+p6PQVG-f3MC_-$j-YoJLr+iTtnh<+L=u?P&uj)( zBWR2uGH&F;<^9tYtKGD-mMKq;i63S1a;IZr_4J%}_!d5+i+yZ$NQ zK~+%yx_GTgG7iC!W@N?_z%f`yBx22`%iVQy>4tk(`j1-o(~sZh4Csuhgnjm@$LuR# z_^hSJdf6v8q0#6GdcZT1scf`0)G=6KgYueSvdX&g^O>~%+CvzUhp~au$Y3HYN~PLF zN(w^h<*6JN6Qhev(U$<}2R2s0vx{u6qXlO;`sc#)1u*1LGB83JEIHkn_ynqiQcB)1 zH-vi=;y|?Dq^(~*%bJH#Kj&Uj_9$9tHx-aJ;SuEW@4x>6`{-@AT~e9f+3V;*+Au|r zS6_U=jvm@?m(5Bc7!Gm+i~8klMOAI6o*++;1bq&HFCrQz`C~`j0A#l8BMC@!WZk4o zntYOxSC1?NFWuVGA`A0pQ4F+L#fb_xeH|i=%F4^JoLPD7XhvTHPA^m00ZLtEd;XNy zlB*Akin1juvMoBk#D4nRZp%T8U`SkEf|_U<3P{TOs9@=lumdvc4+lO|Q`2C@RV~)u zH)0v7$@X7A`Z32EXdh=m)KH=-P%0bLYf7*{{TeyIsmvWF=t)OV-&3?xFw_f05tQ1I z6xXCqo5e8wfCd7OFVCfEw1T%L%W{z8-Mez!gTNlZ&$CkF0e7PT$KqZ%Ki}OLCr=hz zSxFUuYJ5bX5~`o7^AHCpdyw5cg&-b9@2~7ck?pM>#Kam|^kCfU!u@T-f_Tf!&a;%P zOl%HY>|6J~VK-mB*}g}kd&8AiaaK5fMQV43rf9KTqED;fw*x)PzxdO;EGfdnzVwmB zTpxsT$HOhbmo^8M3B^?y7-a{M5q4W#8sO$`1P(k&4(Q=c`uLL#hM?nzu{eLHjkT97 zPkT`tpl22I_%?4XOm$B5NdR@xZyQ|+UEmYF*ff_A!V^7m4{Y8oa9Di1z}DM=%vV;pWzeaA4$ z%xto|MzDR--*Z!1;O&9PP$dBx9;b&WsEq=y;43mBIt8G(3&rToS6yX~KK8gRU9uE^ z)HGYaVT0}6v)9g)R*?Z>u0($@9?D)HnWe(1B(5~&^;0<=6Y84E8h(gSu8lx&81;XM ze=dVmRyfEMS+B4DPau6kmB7j(6(A+Wuc1n8EEzJ>@-wI9S_fQ)9%UK^N)2R!>K}%% zyOSO`QCP7(vh#Aeae{0&li`!qU6f0Kwh)z%Rk@LtpGGEt*U6$)3&EK6ScFfvty+?9 zg-6Qlkw>1ggybYEm^0S}=}R<5P1)Ph#@61}2JGu2Y}l=K=jL2ncl{^LA_{D%wZcvL zCjw9~XPwQ=&vw0A_4>MjcWM3jAmkwophF2H=7M(z%L=GnEu?NTzfoE$o@9c4l*MO` zAGT@`5KirxO}2y0RY8>Lp>rq{+sN59)Krsg4p|}4sxUV9Jm{0*P$hc?z^^hHK6Sdx z0rz_eeiIldJ^aWcKwWOQWP_hB{vSO6I+PPP^?g73A-herVv80 zCZ>dd!qs4(UVHPXHB?vI;>%XJ$>$*HyDp{zOLJv06dlA7=wl4DH(SZc(==%r8h8X- zVpf4=5CAnnRYP}(DXqju$JhaV!8Fy>fW;&5j)1XK#wFj!74io;AsfK)x?x$(j4uCq#l@u4HfT!yFe z*N;A=RCGJ>T-9?5O|P9;y%c44g3|~BLnqMn4hMLCd6o!Tqs+~Nayln7g+9!nn@ErI zBnqyA()80C^dh)*($`DFIH{_pf!+^t{}M-F`rF#Zd*rp+$^KgLHd%kgI;$wHw8x)( z*{ZSGj3wh+w=m5XWrd?<7-B;lBk)QqM5QJXgD5|raSQevwAD*ekwI^~8Tw4t9oa)c={^f|mgRN1_J2(M{+#aS4H^|JZ(v;Kq+tgOJ~9Kf`I{1e`R zPSxdu*Fi6J+5hkUzAv`l{G$gzT^!wnPtf#jtgf((#3-`8Hogsg3|mU6ngjzaQP0bV zV4;BsOj%0gHrw^u>o}stkO5}f_HA3q{`QmFmpCW1W=8g-1kFj9AN5^iLYeuN0RddS zkpTJ}5;h$~7AlHB5W-kr0}1s(s;ld*xVX%LjVlo<7@_goNahq18}5RF3&x<_JE5o% zmk}uqbiwLaNh6!6zkT;R-?e+bc8|+qwHRE0|M^uGX6{2D`jDHL?RaA+nrQ6~HWNb7 zn_%%N)9a=b7fnsmw^gfF*^y(X*#JD4412K|NL+y_F)aa6+cZTMR7MSG88UfsA|s%j4S69o??WHY z!#B*LlJn{9)v@_|vyppSeN_?bl1>#^Sf*z#mi(DW^*r*f4MqWT6$J!wVd9S%r7$T{%j57)2G14}t*&u)2@K}5!T10{LuRU z{&)YwcD($WeQw&|CIBcZbRrN0CrPUe5xx|y(we;;QC6+UMvrPboDAL zt69fmT#sbijRJc0@x7K-umJU;Y{1 z^wZBdF;Dfr}BgY6Jax!dhNRQ?LADZgF$0}lcdXa@IG1AIj0VK zKBK6tX{(MWIvf}ZgAWZqRuO!kdg^IgvUKsg7Q%17X|>>I&6@I8TDJA6G zmTBMk+rP1IfBQd~bZ5F=-|E$?>G{mGmtT3Eo1u%$4*DC|J@rUcDhsEmh3`+Gmb2G5 zWzqn|b-`U!`i|1i5jeY-lE0ULK2ETYj%32F9w(j+GnkM+&XDKe(Q*q%D*Z2Ro@G~G zvDmWb%(e-i2!j2n#lgQ$#cp5_BkcBeSSgfVRkiIXQO8(ub+?5P9DOia?`0~!0K=(J zj&0jZJmo*V{*IUS1mLiA-0eXd@pYc-D|0G_bk0 zkPP7GU_bfEqjv2FFa5Ij-RsYT3-CuzbhQYYkegbuY%vn|ome^|0?G|6Jw0#wW6gkx zReFV?ge)yQ1xEorogW5GT7i~>fyK;5(tSow7MY@lrNZS6g%>PoDFuWmCayv78rukV zf{iE$cHz4rd8$(SUdp26Ox209i_q@<(C!pyB?073DWQySfFXg?=c^No;%pD_BOkln zzWtr=fU^sKSMYoHDf`V|pS!u6TbjvOTP!6d)v5OCFW$YQ>i6E>^R|8D)?2I@J;HR# zei=(Uni9|y9s3Z+F7;{Y59(ZbQ^u%d_9a71CQyyBfSbttATeY8oM;#4c5%x_>Am6ZbRiR`?ll>trw zL6XfWJ2L@YMnKw#G6asb*`9qB_Ol%o*vzKeckX-G<}O_FOYiA7pOyF5Pu#VAbC-Se z+D*2qAl~L>MA+^#ttf|g*u4+kPvB!+_O!UbC4mnGqAsv|;=E6=IVm_QqYK7PvZDB? z?S1`q`qq_p!$)tlSXvaFR6Okj!05y@HwlhL=FEe&(NEB=C@Ep0ob8AMJ)LdzajU2T z_SoFH1q?jWTsvr(>`*3L4d`Tw+gk@GUc;A19{Gt~_o4Ss=67yYiV*Nef9Ph9kB_%o zZ@Zo0N56AEQ#ltxuS*$NKaZxu2@RiJ0@aZ+n)zf& zE7z{4gzy9NRzu&e4$?Nl1mgb!WF8Q3liwX80CrO{cec0jHP}F2qX+o*jIawUo02MwASsDVX+^X9>es(+_doF9|0?+DB8t@+aZbUto4T!WZPh;V_G*MPil>VRwJIV&TnVOfA6%)qE{6Z*!C!F@* zhRfI4%dhUm*R|RWRTg6s|M43Z+pgoCw)-Ib;ekdA^y$TWWYp&7#@eYPCFl*tk@d34v>o!5WBw!gH^wK~oP z{dd2=_xsle_U?8);3R}=WuwrBqPP0X%Ft3~&EJOKL- zQpJqXKW=NRv=(HiHe7$3-E_w%a2}*jUR`6QC1q^nw3B!)QQM^E+}YlaD*8KIkFy)( z2oM_ezF&N8ADG)%*g_dxLnPr;01Ayt>h92!37)eq?^{$Ka=RIwb+{+zoK| z-Jgb~kl+R!3PdV_;~0J@>rxPiL;^~15d}Lbt0*y6 zBW0N;!c|PHI{CXErdyg|wZUo_rX+4BSV~M);Np_XCUPU*AWtjkfFGHs8pU3Gh^e7* zhJ;LeC?h3HuS7T_Bc0!Yd&#l=A&+xG@3%dVeztJoB75$+=S-#Yp%dl2q=qB@fSki= zR!8zRHk|xVSVKA1c>sH$Vq&)+bdp4qOb4X^vvR{a`x%qoGU#`DUvtu;V3&jfy!Ex~ z)@Io|r}|L$owV5aAYe2!$-PEw;f7_FI5XGsDD%^2&nDgiqJ&wHpAdw~w5U+PC5~6v z_BT&~UE^B#gjsS@gnj)_ms%A3;Xl9a7JKC0@43@79Rz=|T6A3t3lFo=a;?~~Dxj7L z&pIPH!b%F?Vd>F!A;?u%mP6^RwdAxcOU;?>*hfAHgv2F+n{98hO2iIS4vRIUHimrxn0A;HtnY~yBTszs1d2Z$l4 z6Al$*MZ=JKc(y{GrvvSf5ULu%y6|#7W9=|O!*z}Ee>j*aVVv|?D0(T^4uW4B>F=`o zx_Wo>BWCZWuRh3EDeKf=VT2$#$-)pIS=cz0cnWxY%k?2MQsW(Sql(7!M+UA zaElpgq%e5$q12L5Q#2|u2~51VA8fMxoCs)C3G|2{)&uzoqL(VEqp-w8`c{M1*@Tb; z0W};t-ypk47+~oRdbC|b9=3YbJSR92956u!$uyeD{u7UF0pHkXOM%IFpT~1{zOK#h z`#RbPx(F`c`qzK9D1QbR^k9n+Hx-!wqgT#i(?WqKEe~|V9v8@DR~iP!wWGNb0n$dS zQ=9C-+q)T_p;{i7#_v!r_rb%Zuie|#Zi&gsR8xSa`JPxKMaNG&)ug zx0hac-QL-Q10ucPw6tWdBket|jVPs!l)X|8Z-toPjfv934-4ma@k?6$zSrs9zdxlJ zpkCpw5@DavTc7PJJ28EMBdSAl6NrFC-oi8&X z3?T40H*gWY7_GK^(JY`e3<%J;3nmx{(8wWr#6lV zaD@tZQlBPmb;qK?h&Bhw@2k|W1H#bR+Uk0&2@!slO#5KTj7Vqb9tG}C=R7^m&lcvz zzlHuz&=mEy! zsMpfv%iv|wzVhem?doMo_Q=f13!4c-k}tpTaaa0S?Ne@!KhNZ2slg~?V}D&_i?3`g zHq_ruNF{Y8fzJ?nc-P%<9T1srH}Fu8P+7{^zdNh8Ea3s6=q0ewf;*drAsU3}29<)d zrq9td_%(cxQnF;h;}J3yI65vq&epBp@T=!(%Ksf36KfkcZeTk6nw^2FluD*1>3jtw z1#Ff23f`jDDZq;|7#~N_C(9cKODR01wA+G#8>uNSIFGWrsBn6U1jYd-eNrDOE^p#} z2ZARHcG`@Ml_#C=#2_0}FD(UK7oSOJP?T?@yymQVSq_@h1ze~T8`8B`Zvr<)ua)z* zVGUDxj}a>iIDnbL*#rp-BUM=F)~PhP}Zq z^LsyloPU>PGUWkK2a%SR&Q0WRFTM0CYAGlp^FNiDL6pV1afb*_5)2h6Sc zAe6ymi7MHZ14>0k6ZQPubnC;vLArrDhY`WPmP+4G(_>Gvx+(@tlh6gLYdXveA&5Y- z;ndV9rr~2`DK(ask!qnaiL?)}S*$p10g&*etvE2^#U>C5uQ`O?YNsWq#&Ha&-2iKf zQQLtHWzn)cS`PS-H1^o4%hH%Qd)sTT?6+56-C+xsE~VF)aGuZHh46CPcs%U>AAHwZ zs*7#o@|g~(+|V&-3-S_?up(na3B9hIAc#f5>@`evle6sTzBiZxcbRW+JZz_AxbbmJ zhG8|);~QZ(RbNx(U@z)zt11`*dHH%VRh%0ZI!@7 z?E~0BLf{tui7ZXS4>28_=LT>XY4xQb4^>&$=7|4EOov4cVl=@A@q#65002M$Nkl{$@hD0QN$)1!s1rSuan5GK% zCo0eo*^Yu{6F0mc`e?U*{1c9m@m^e*Sb(cyjBMTZJknRCmYJFWI|Xexj*r9SIMtie zkSN4XoUU?xKZz&=`O(cFTNH;_u%Pt#Kzd1d7^9_F*CH?BU^v4OOt={&A#T~$+6MCk z<8;cgP9RHlWp$RsRCtVDk%S~7paUi{K|E0i%_=S!2vQ7{7Pw09AUkua#Fi2Shf(rA zcD$B0Lae34_}cz`MfTj5Z8me>JWEZ_d`~u!_u`uAHMPcjdb;h~|N4&>hk4+voJ8Am zqQ)w*NL@2G$vj7ZVbMyM42s3&EzM3G+|7_97LRIrg1}YeuI5x*!1HOV8=$w^)X?ZM zxHhUBtoLZmiQKiI(f`ewJuOK4*f zH@N#Bc)+fQxilU8ez8iFPF;NFObWPx1_29OG3prYYH2Ut4wzMANH zbW_%a5fCM0s@_>9Dlj#5O+ao27>p2rfqZD{9EJ>5E7_#CZ;;)2*Inmz5b}%5arUc( zgali)dX;T^{zd$bD{KZBzhK^%Ch4O4&B;xJ!qjJV?CMHXg9zx#5aXjU2_`^}uvH*l zW1AKwlMVE^b5dS|-vl?7IKQGKc5==J@CgnenEKNY9~&b3Zf&+~i1`B3_P{pq<@aLQ zj6Cr@$pDnDzF{;y#LEekcPeQxscCo1DHr$$1whP=mC@ zq(r35I-Dk^v@45>E1lp%9sS%11;6_rw3~0fc{=$0$^tlV0Z^|iuZ=b9){))3;C8nJ z6qlXj8o^GG7~P=aL5rb(dila6JHm~UNFXZ$SJ?~xaZVl(8(1(CXuauX3uNjX;El)v@?ajc zCKAXx$LKwIvAc}&J2IP}L}h0T%$d+i(sCTaGvf5&leS{@95ypQ+rH}z{i`%vJ}1ID zpjO>`-&6D^2ko-eYv1#8c|IhhaQ>^e!pQ6oYw%TV-PfE@#4ky z)U(ezZwdv$W-h4G{tSdt{V8oV3>hZtTQD;fmEKJ8IXG;%aCm>*l@J)XT`7Y{Ds6Pf+rEkcl8&Mm9N4xfY$A$gg=h zBc1jpq{!$W4HIPBn%kgcaf1Ut8uthUhgNAzSInl*8)rKRf)a4OY8mIfYY6$Umuc8n zIUu_qQ|n9hp|vI6`q#huWsCM5w$*c!t!oSx57Vm~R^zV5h7gsM$~pG2_5lFyBOcC^ zLuxSc_~_|uw<>Ud88a6#qzJXc`}c9K@us1a6$f2D)oS6WofDsuZp)Wl=Bz6{8E7n= zlWP@hklNs-WTdiJ12d_gkD>AR;~)RnZn@=_3t~NAh}ZwT2SA;KN9-mT>N728Fj4-_09x|*4mLuja;We$ z0k7MVVk4}&p_RaRcF-ZtrFK$rYJ-_1tF#tYPM~agb!^{r9JoTLeg6kPwCL#Q3+7sB zfRvk;XHRghg!s>c(5@_@0t%bj0YPjS0innS0?|}oZ{Shjy z!b|L-;5SBA{<%N>wC#Iyr(L-aOeLF8anpchMbl#O8b^96#S-%7Iz{n8+CI{EY^Q`3 zU%s*KtP)EjEtyhaFzt_15X&_gmK7dlCS3JbB#INy#bh#`fSM&(A~2g`2RQ z-bosnpmg<0xXHT&0_OKM=~TljihiLcT*OQMjTAmvibP3uW1cuwO!jxSugI^`MEi)>yFju+5sk1Qi!})7~NYTvr0|`Z+z5 z^;uQZNPm0C8FVRw%b!(XcinNbty#a$a=7Mw1V~{)gM&kG0c67>yW6tu6+|q?*`(pY z)AQ>B4?E1-m&BXyqYAQ3xwqYS-+guqT=NTUEnjGt{yhtT3Q%+B&SN?mVb5%R(d}Gu zOm#(zlWdkUIQ`B_dt;lamWvt|sD{`xClv9I29&xLk<{`+|5F^%#BIzE*TOO`Bke!P!8wT&Bk7$JlZb}g*`2!fi-c1W~< zU>8W^U7%o5!1N39@fD^}a#mg}Mg_tGbzO+0Zd!jcph$anoudE?CVF zl&(O8Wxe1Vdr55s8Foo5bpXw%C~bgK9Bv+*+h+QVy$r5$vSYy{_E|OfMdB#gU^Fx= z4?9`fiYp(y=NS<|Hkx1{JPD->yxK)9wXfhOL4&{k%g@`x_kP!w&-HxkY$$buj^eUP=h_xRY1>U}LIEo#0XZxtwEA}gis|p{FJyz;X)o+J zV8@HV|FJQ7!Fpo%cKAQxW1B%X2(6&t*F#TR{qAs-lg?C<0fJz79YCKdI=aBd z1k+QUfzi9ax2!(Llp{06^@4^7=eK{{Vv&Ur`Cglh1|4#l6AVVSi!y0ysjB7t0WRm~ z>Y;W6^sRyKI>7KLhS$%YnGWlV;Ymoi1xyT3Mh9Uf+GDwCAOPmap=jN14?p@0vh6{( znD?RUe=$9DyfT{l^5;KoPd@mdEyz!>4X{3HyBItHR|)YRwOLEo+M+dA+Wze?I_zW( zQ{PEM3}lJmi2y-s_n7rrAe--~HG(}=2B>Q$Gx9!qdcw5|nIQO!oB@>psIpO+tg1$A zognTJ!M-NwY0FqC2#rRk=ZCTV#juVqlxzN;2SA;Ob3rgZA>N*U@f8ACpUd!clL;!I z3`UJ%1v^bXg^AOGke`M$>onYu*g+@Y<|Q(6E@s00jUdN#qO^tc%Fu)$7)J1IB@m8Kl84aa8=)*~tEeQK4B(uf6?P~gGYasT zuq@PQl)XRr=yPQ2J+^%HS~tM@pYQL&`;OMBz|Nol^d0v6laJfxH8bpr6*E{%2#k{T zh54eRIA<{>eum@k#wI2?#g`^T5M-~=&AKi^rN|HQQz^5Q;J`M(I#i!q)ki%Oc0r0n zX_Np)7iF#>FUs=tJyk(k&)Uqw;1{<5a4tv4HV(AF6Hh#8n=aop9sGXJWphCUKpnFd z*MfzMEGsA5w!QE&>MSS}$49sZeg_$y$^b!M#1odVs)ivFR+Vr-gfN7piNXqofN4uh zP2@&bsn7*h%AZoukH2G~Q8FAkQOXo<(*E(E{?Y#CZ@=kI*V!HO;>tugWA4}m=h?XF zaw~;R@X8zSfc^8cw3uLed;RWaO2zF?S)h8Nov;W15z#|sQvm*>38K_@bnXSjjVDmn z!d&P_H8T~Xs9*wB9|D|{P`A8zodBl6J(~U^RTe`TtKMKBeC0T(56T9Qm3Cp;gV-(1 z9S_d;xPx(MkO_7XiLaw)+Hf25v(4)gVLFT;0I|bmuxmDKblHx3Ll@|O&IZ38`|PJa zVXto6VmGYMwQClmB1FcmN+mkf#}=->!saf&5(**8%uw0XvzGo|9m5N)$79Ef9PnP3 zu0`vUbuEQxm9lEr=voT@C$(~^n7j2%m6II9=FKoE2*iTZx8!=2Gazj39Et#P2+STdA$sBJxzzcP6dwQZ1oVK`70 z(Z1U06SCHvR8VV22R*JhCo(GFwK&D2l*iSWJ)b;NNv7yy|M}hT*%$ueFWgIBe3{?5 zGxdwlx^XsLez~1Iea5!Gb$|>Hfmwzl1C+j#9$s!b91QnaH?bz@8kWSz;YmSIs;ldO zDdOX@!FHe+5kQNY5z1_#{}k#^^8he;GbLOIn~V>*Guaz(5iqVIh~Z6AUfRrcq3m0j zZJDqh4jidg0j~*ivT!WAT zrX?5LgQAnowS#r_*}LwrH(z+-zllDc9@>~Bfu`(SfmBj$n&>N-1v9-@TSq}p zNkBaU2hAM=Cd5KgN}B!k*S^kL{*I*rE1M2}|GR~A!399Qjf~7pyYkAb?AWp6Ov8$t zplJ+KOkaYZ7DEsVL#2X<;Tm$NFQNvq%3)1L)z@l6H&9Yiz~sQaHKBluVLDz3_x|W! zyW^90y0dk0W`4i^Li=$(F291l(}9Dw_uw(IhG0vD8Yf#%$s#FZ_Vqc-rEVp-b#i_b zD5J^d+JMxgq{KR^mU!9dib25drfh~ugJYaFolb7PHV zQc}Xmv?v8VZwYw5s=8J_TCf}~b3?vxHjo5>?5E*e7nL_StYk1OrR2moH=*xD*rJ=k zpR5`rc%gMJKyw3(o;CuWT0tQZF}8I1W%l_$`@Hj+x#Eh;UFJL;{QmcgV@fykv`;R<^`uDrdsD7~~&i7Y3pN$O-cIT}(*~x=@?TXbiY~K6~1Y!d? zkJu@M`Ptn0^DF`3h^D%7D=DpaCgakz>>=3uFsVJkuuVW{ZQhM+G==z5t2Y|QT8@sv z6Bs3~5>UUK31`iog*n(iwU)&_8+P5&Op1zDh~a$mG}Cc?!!=sgtr;&<~Zw8hB_yWFmE?eo8fqkfE956DQzL z@{>m&by8Dj7mb$L#n$ut-ByIoSsjz%58rsbm7grM<@w1>&;6bGjR!ZRR75(+09&EP zi6WOsPz*zEECMVcZ9JPlH^nCK3qD@l#&oySs+bNhCjidO#FQ!!Ekb&C8?IUGmtUef z!(?>056H))J-)Mo%qqnG<45<~hRd&fPj7IY$6CB~@BHY^R(|}jZQf8|8OWyfjjErF zAT0x(wQH9#eaGm_C&b=*V<)W`6kKQ@bTQ49376PST4{1FEUUnT2v$-Mw9AB8z~)SP z>(J=yKrfJl+7gnJY|-K+wsPePTd-h}!+gf0@FED;FF%BA(0BBoOLGnX^820Ie_hZ4 zP;dQQ5G*e*w{QOK-`G=6K0#|~++rv_L=W;O*r@~-BwB87nik6_>A2gCN5G?{+s__* z4Ed&I@97OSwU_aipIzsythCIo$LpcKyvR1q$3vM7qZLl6jM|f90X#EdYyyaz7zq`Q z;MYgbPRi4nDNz;|&EN=}+QAd`Xe1&$3$}3Oym-q?3v+zpKwqZ0^YSUvP~xGz5J;n5 zS>Xsn5#iHFVYuPj2?RiI09_YkN81H6z$utiIjLJMlb z*$Q+$lTLxB1(WfmaV$|&2`N-T2kPR6Ub1+xJ^A$0lr8h$H^J}Rxr>U5vbF2hVT<@G zJ*R3EVz3VYuF?(mv6Cz=fz2b5%&(ova3}$6c7764_y`=3Ui46oX-|>Qa4)|v%Yrk& zSmaTHh71Cm-QfSaF{h8kCNtGe94>Z0^T)J)0{(Dp?pzy0PD|=C$!SSe*U)Z}@iB0R zL#?s8837ASz%g1HMDZ{m^$HmzsSG7La`b=iM!g6H1b)O|iX z@Hlz$xZQpGt=3#sWS6hZ!?Y;J@(O0M$4-#3b=h^-U(1?WNNR^7&dE}H^|c+i9d@%Z zV`~WoQ3DOZTIAnR!TZ(PjbV@{8&=_3D{28P1B>~W|MTxyuznXt)EovR;$FMdZtC9N z^fUjDGjP!aKpnYuX7xdC{m4gb4n2`R8tXNb#dS<6YmvCB;RY7XDD{Oiudxw2S4D=WhmaQ{BklZ%}kP%`iUl@n4>(c}HTxOA@G z-u{Pu@|VuHw)!TkVB*;fAJ~&gZ{8d={^4=c7prO< zLU=U{x4KZPEUK`ydGk@34#W?mgfiKzhr!G^keE(-WZq1wmoH0UO6p}V?5wh_JIn2+ zPkze&?pxoXL=T*@>#6hf4)&6Ff9%8NKh|l>7o?%qoC#ZF%<^zp6D4iYqD4&2BP<{! z#tt)SZ-qB~gyBfxvD220#elRbPjC*TXDLw*AIjwg^9td+D zA4gD=!nQco1!)nM5sxx9;9k+Qgb|!g|1L5NIvt$plhyrXg$OT<4q9GH2rw6aH0qG! zB2z4wlSmfWgP{}U9oWN=0Gjn=in;lKg*!*UVPdlG8)&oV03pGiF@!QW8r7e@+upPR zPk%c_K#t*W^5G1(b)l)Zu7Kkjvn?-|*i)~S*s80pwg3BrA2T_P{eyzwZ(1|YJ@b@( z6>e{=-;k|el4bEZxo*IwX>DL=gr#NWTWV$&CA+U3K2T^yXNs*B?~lFvj+!^{szg{! z?SS{y+tUPCVx{6#kI}O9rjbcv0&)>tjl6N`?zI(l%&#y1468IM< zW_rQ=;#~Z0U!|hJGHdo6rNKF!81o9wG=zsU$NC!yST;FDcwH z0-8T+#p%i6h^zV`q18tQ6ar|s*RqnsED58f(`OLjMA|(f8ofUx&BP5(gdH+QPivT9 zkdY16EiuJT9x0_1Y_WJ)CX*Qc_yc?nMLA&&>_9Yk&0xiwPRH zb@NlqD`rTk6!fJz%(0;=e3Km%_pWe4K=Gh+;P9qmQ9d4Pg6zhCvf- z9-^*^0V7^-IU7kvb`JgME$<3`dIws^(`)&6xt3G)I~QpHoT}4tX+LjfR<^S!ee9`c zol15r6G|yKx4<%x;gmmtu#Fz7FfP$RHPp`%!=}24p$MJARLWKwfCRsO3xQv)uR}mS zhrumwgP|aQk%WYBn*S)A^bI?TVs|g(JNk&h$n=kc1*~ryb0Pp?*pGU^Gmc5XpSZjV z)(AF_7*b961X^MWdX3N^Cmk4g%+gaMko^jFfay23zk&1IGJc-$TQ8OZpYp5P>MEz! ztii;8e(T@u%U}4sJ@eG#_Q=Bz*|sMhu~P@%v>mU#<|t`%(xrunNf#)BG5VEjHlWLw zJBMKoGE+QRLrsGnI&ut3TcMSfl`%;l1>=`N*^Z)4V++qQVNp>L4)Z9 zYtKCM?5_mB-+K7dKRi!oU^)PvrxSglyiDHAnHCQ&@+rJVMhQAONukb>ZwN42I71Ps z8OxU?pDa*%bv5?=y$WuPg;0RR;3!0=LDEr2^C~%mSm~bNlz*JzOhL5X#iqewjf-!&{c&JAKlyPM1xfq)5AR(u%{R5b(1@g1||q;SXP z=|T`7iL5aJ;PE>Li)`z2+i*b3w@e0NzwV*ncjoj-`?F8qjb7i^?7{nfY)?J$Gkg8{ zXDufh2=`zI0Obn1{_^?uk?Ysm;(0SE*N3g}OdU$g-A-I%^{S)S02+2;j zWq#e&dT+ls9RT0k^Z%Z_`uqh8kjxKs$~#h0%#06l^7%5EiVO?DP-y@Hf4j}ef$GLY zSU0kj6oU;{&6?{7C{Ux{J{N*`x}q>cS&iR(+A8*i?p=NSnDOwg~K8V z4?{x_N}e}Ds_6s+X2@2M5nv4!wT_PG1%}g;;OaSy#vS6{?fodMpoo%)Rc9cXX&=hg z-H2PvN)N*hw9eUvre3iY7Uji3FT;zYjZ&V^fnXgW zkYfy$YD)-wsd)=*sHcnmBUDPTrTt^x&OuQ8YWZsfP|Efb=-=FP$V!V(S`i*73WlD?k9FBs7 zG#KTjvgP@go%c#i7+QabA&jB~6Am!Cj^1D=Qd)}&5~y@>;6uw!4|#wjfgz-z1@w!^ zMC&k;SX9IOEQn!F3fvhbq#-G=~oShQ6t7nwYfi|}wnX(tzGBUqm zgi{BR*h-0CHV}%8zo)k!O3O@`lkkJC>$JyEm<~e+Z$2tQ zdQbXz=BX#_GoSjT1$zwG$8TIoKtoCm9m7R4Vr})pbZhJLw1cN>9Pd_kta(6ihI?DB ztF6gq!&3SDH~t>Tiw7bNyD%I-PGex)4&XCW&cu2QvnXjQdiez+0E@Tp$r9&Z9gdiR zxYg3~3nEyT<7bnel|$yYjlsu^>EP#9&V~7(=>T|PUY999Dg}$nmaiaic{ra5d8>qw zA)Pf+LgNqeejS-$b4!mUVT>*@PQg4RMMk6{pfUYrV4?4I0u@0x(2@5g!!P^MR9xcP znI@}ODE_v1u}(e<*o~g+czM62XT-ZQJP4A17*o{Z(k2Ulvn^lZlF~+-hlfd9b0@&z zUa~VEXTnt9&`TEQW%;S_Z^0D)?1`-olQ-W}X;N^uV+&B#1j zZb}fkhl7;zuiI-cZL=+8e`%;ve_#V8epH;3kB^B6WRe_dvr(!}owv$T^3a^jNVcW3 zQvqoE*>l?uQR4PtloesKS8Q@$x3@Nu$-?UJ7`DU5OWnCpD?t7oQPGibse6FCAn=N) zf%>3+v;o2>t;=c&eu-)J)YH${f<+6bgWpAPdrSwwi{K(nN z@(wAUD~)A6FwXC^&<6~Lwx%ISJ2F|LVEp78EYxhwasZudQ(QrK2-s)oA;tJz72m4FM5O$n^4a$CMgxD%QJ`?ESe5}Q@LpWh|fJ8 z0RM<5^J0A}*=MdpmHBAlVftM~7ERwyu5s<%{Z8FwjI2($wpt|fqu>V*BaWbtAl%@5 z5u=dPo`BL3{9SD$((q(>F$_KeXbz6Re^npN2UYGq0%r(nG-1e=c_Ms3CFz3bv0~J} znfM0N&+CSpoDmmbyAM`5Jl)0>>5gXB)81iu*-^H)uo-C1gr%j#z*y;{lna=z!ru$$z0A5U2YYLCm-#@@MB0+Wd;66JA!9N;ki>A9$F_D_I1Ue;=UyIyR zN>m_Fk#Q!(lc@6eJJ~D^4m42?B*3-7Fp+VR0Kwsr0c1n+u??gog%Q%>X)|*Z9Hge7 z^4fO-GbV;F2Tu_M!5K~xY$xF4=4FK28}C#*#qSyE5%$ua27B(UN&w2^_QsJ0Ce!>4 zl3fQ0ewVLV0Lx{wojiVqsdERx&l7xMv@Kn|!3q{G{doYbH=ltfYa`S34h^%cC95nd zEfcltdaErfu}^*aQ+DJ~q3zmt${Nv-9Hf`o0l(RcX>}mnatZAAGL??SiLZ_FU6W}E z@D^7!S~xw-r=Qtk3#WtM6t2*zdFqRl3O6+;$B*6vSx<@P{H4M%%=1B38 zR#)C^9RmzQJp63^qA2tShY?2TwP4h~51;C=;_4y0adSR^XnKms$&Zuiy?wCJ&fqF{ z6nM@0l?5&Uc{2?b*6!4q60*`R%gsoz2&Tm0L0%X_4Z{P^v=!^t*o;{V+?4tB!M8CK zLf>!i%N8DwNbKxY78V&z5Ji%T{^#DEd&~=v_J%7r+Bg5Zvecgsl z_QuX#7@JoS;QWlX9@(9_{$96WAe78C3`V5$*UFHipAs<)hdE!=q^0Oohg{YW0tXTB zj61tVTmTLA!yt&@6yQyUXYgq$d!vKB$gX^CXdH0`dV+0CoL}Etjm{%{V@!pPoNTl` zr(5krO}BmQ`qg&R^&1$7cp5@BmYbPqSImvG)w7fMH$B0oK9s*3ofYVuf()G5k`W$& zyBip2UfuK*Ct8q|NB1Tq-u$CtorrD+l3l{?$yT+rtlUl=KV`X41ew%WX=4`(%@Y8l z@#$a#khy$VXfOkiKE7tUjDLquK&U@_e}do;)8s>^5kQCvMsLvD1)(Ttb@F{Kq^yTP-%7 zpSpQ1gN&J0TGQ+-Pe%s`POTL-($j%Ti?4m?rW@^JpZQD5^iex}_?XqyHlozhX31&E z1Y8d*-MtOy3$(q=CHR^wu=0{3%JWI8!fcD;Sb4jwJB26(_7ZH;*H`k6(~ zw3xJ+w&SJe=~=cgj0rTE7s+Z>I)CbW24krjh|^#LLJ!Gsp_TPEK<5i(IvtNH%qUnu ziJA&9{QvQO8@|(}-^iM9tGcX+hFT|^IfgDq6TACHinq@D%@FG;Q zE=$5{QxZ>gz*^$T?D`36y<}`MtDgZoqqw2pQqT#Ei}16fRb37*=!e)scmFu-3151C z_}Rh(PNKw>inj+DqcjQMJW$UdWZ04-eC%{(w{1P#%;>?}K7H#(Te9*p0LKhem?R5A zBP-NkPgo_yw5Y^PK-oUF`tr?Wm?+iYfiio}Y`gi!YcXGHwdY^mZKbHUj7)etKi&|g z%(dtidUGu5WdXjknKZItX4M%l7v=jOhUQd!4TL-!JFPm|-&sg3oMu-a$WdfUAh| zCPQ#}i^ye80t#b@DI~>(S_!&;3XVaT2Gw>9JEiC3BzUp}8DC0YmC*I=qYM?o`L}v& z-juO;hyb4%gogL*p%#NJXJ7vOE!bqPu!_=BD0f{Bn-?17YvDw`RIyI70}a8U@(+nH{SR`OTZuc?b^Nii~@!Ksa)YIl=$2eeg7Xk(0NMW@Cdl{u1ROj)lE*qY8;zTad}S`)#8UN zL0G0a#I_jNFk$Kk;^jMtOxD-F{it_hpS&PL15s3iv*VQ+QTo#dnk2Ykq#0E*g@8c9SHc4pBPYbb=@OqoE z;v6M_RFWY!H?&y@Q{f|L+O4dvk72`r?K%aMA}YlG>h9}p`KlFGdg6c$^`gdtnbQc* z83=Z7#KW7pLh=$0E?sY6lMW=Zpz+ zESO1pep14j4t~FN$y_u)oGyVcnoITGAE;AZp?A0C`PUFXK$RsW+zF_*69mJ6v#89J zYRnMu5wSIzk+#nO0~pB!`2;;d1#GBKmjuSm_7r)`x*H1JzvM` zx66L;t^a^4>+PsZ3sRUb|2(#ocan_-gR?Hz#|T?tAq3WD+q@nEb` z+dgC+3`oTLO+c~box(DHZcK>%(AvwkfPpGYrsVD&XZVqjgkno3py>!))nW)h!U_&p z;m&8Qr={A`XD+bO-ZrGDDlGu3&i>XGYh&8$9~x;#jucuvip~G<&2QNE<6`XLho8Wd zDatai^%Rsx437ZsaH5yOag6$g!RQw9_Zr+tPG1n3>BpsU2BrhxrEwk4 z%ezq7r+(ny|I@!(H(c1C-hV&S-(btfJ+7j;pRDgJ&Mzzw&kg#2euxJ&cMn-R6|zJF z+QGG?W4b;gIm7~kf}DcP(Aa=uZ@m7_X?#S0p5UVqAv5vn2G@gIGB?5&&O=ScHvnar za4bjsnap-s`SHWfkUbO0`oR%z6s9|^WRQ~BC&)lyGo8km97c&s)_~X~>{5Fus|)SZ zci&-^#V2gvk#c?w8Ga_h`Wm5`>zCO46&tLk{EY2->OqUeXep5ZScWfeRb>S(bBPQ_kgICLj2y^| zOw7IM4Gw`F9H*4+Y-^{)^|A%?bM1*|x7!gEU}B>~9S~Z0z#)D|agE_H@?TM~RM6a_ z)bFKVc)YaP79hO34M6+mtM7*34xY? z_Ffmxa--=Wz6w2gb`g=vyopz$EnNqpe@-t!)TL=dz-D25b6aW6(-?7>Q+wAohpEry; z%|AREj}idVh?EBFBLt=h2Jn+QM(BKFIJAk1S6)_ar%LH9#z)(X^myB`_XOqfAlaxq zd`I|xl!X9#c0MNnuPf_Xjp4REiQvKIS6ne2{4TL&Jskiqu?u>>-;RP{X-TnNM-Z$i zt*{LU6nHS`c;`$jFc?p}ZB?2r$c?tsl#u;U;8IiLZOz7|mXMoo)g{1CEX?}aYptc^ zn6)r9Zs{`zu=W~5Qi}jOgw^DrXCTi9zqhN~)-241N(W$hBES|cD6p2A67xi^Dlj+{ z#b#dunU6(=hdC9PE+ni*N1(2?b|A+U?UZg>`MM23gSw`6@QDdFD?7=aczK`8Sk-$> z$LOdVK(xpIXYW1WBR!~d@1w36Nu$w>G^5_jN~^8jZm-wffFU+G28^j80YVKBhZcH5 zAY4Ln;S$1yTpYjzgN+Nuy+QSJs`uWdkw#@yzyI?t8{oSk*ABLK!882UyQoZg zXWr*M=RD;QXCsSTTlc85jQxlIPUr!_vC-=9#r}l7u#Xo7z!&!Tz9jBlAb9NPVf)j! zzQy|62oiox`-Ps}WM6kN(2C zu&f2x5gaJ3q!LWE6pzHvT5-m>%W z2@3#oa$#B(H^2W$%O)9mCwYNsWCcEatj`X$OxPq^!Ik(FE8DcyM%(MH`&6AJtj(eM zJ8Ao$IKhjGvCTWTSXv^LS6{ca);F+cY0+}?b6KgGYJ)AUmH`+g(#9S|SUtkVIv+qe z%BoB)a4%(US6EmGX|m#w8f~3jwApD+bpITLjpmg-eRzz?H&k_}|4FylH#lyUrFjrH z%Kc+Y9X3B73!&PH)>XJf#2-xtbOpk@3A|6aElcaXpo6}BZa(s<>^HrMfyljsV)jQ z>n4*(R+(b~`aHhc^(-|iB1vYHsQq-@Bk6vh?b@72E?~%JiJm{)G=}XiUnG&={zeo4&+qr=ync29zoDT)`*Xqp?!V_}_L{8$ zyK+mR%lq$NDXf78SXjRkQFsndp2i zOs^`-wUWvbl39?|ut{!cVAPsN7T7m33HYqo=3Uz?IcC}FPPf^8CwfS4Su;OXXF4M3 z_rCk>b_%^;gxD%od2*$+w|8M*OJvTs=)`wPpgl~WsSB}oVSa$ha0I_^3fYAj>_lng zn4j5-mrP5n^czN+MB|&4X1THOX|1PO`{=y&(2j4sV6%PihqqZpMMV?@&+AX?fBxj6 z0QjFDm*`jjA4fpo*WBD}Z@cjZYdl$JyUV>!OnzEMhQ%ej9V=V{KU1E=Qu2KpY<95K zmg94+pKWf3o;u8|EyEUEa%nUXwn|crJJlMx$6=1At$?cVE!S)${@!oh9Rt?TK5B7P zYF)Ie7gl5$$ywIW#*Pn2mO1o$PqmNRJXX1KcKeRe5^vdDO(hp=)1etB?InQ_g(WJ# z2^Jek?>7rLCiAtd3=e?`=mQ};T-<+QZopdF`)qJzoT?Eqw2~Uo(f9~>$fn(10~iIO zal9_Iz6-R?$2+I&N;Hc1+>bqwTXZJ)|M=jXXWx#_0MD~Giy!#_VlxxWRfG zj$0l9sUB(R$k7`n{PcK3FF^-`R^K&Y4SmF1Ev#99>MAw6%6$37R$5hR!Ga=lM@G>O z5^=@u-|OCRz3twDNZPgRw690&dx*EkriPp*3ubm|j*V>S0JCtJtPox>#GZ~2?e!=Y zyq%Rn(rM-_f?!&aS*M%205q55M2P@=dcLE0edlR;wbD}SF7vS7$=xB(GL~Q@GV%N& zb%uXW5Pg+GHlS>M+V`8nPJk7FQzVI;BZ~lE(3fhPOEdXitQ`Qv)g}u(j@dW?1`*iSWl$%tay{U;XsHV$2&l2k z6$SAS$JuQU)HA1J`UX%yF4a5bw3WDiQ)p)qE-)WJxv8|W3Ig4fI zVF(Z{F<-=g9HH9NvUkk}mE%0i_XLVs@}#{WtEA4T6&@U(Fdsmwyud`huD!or%{2L0 zg}`dAn2&>GmHU*#MSF^Gzw_i=-=ZmVDaHAN`2E??WHvWosQM--zDM>mv{a!1Ma;IX|A9z)!n4KJ}3g+mCMf zwr$Ewv1%%EzWdew`OsEq~j_{WS7S*m8P?wgNp4@WH+-2sFggE>GN9c`bs-a43^L5u|%Ga zWHVxeV*pR~*x8o#-#MfEB69}s66~IStFWeD`0^kdGZ2utcS>U(C!lDoU z`JdW7Kl;AyuFSG^fkcv)L79DwV3L!!I?CH8L z{JP1Om4>eH^pK@{J(iKQhNEMSijYbS0dykI!Ov>WBxZKUJ0`4;$o>U}d&{(`lDfD_ zgU*6@I%+H9002M$NklbOW(HY?R``(=MXLh#j{inbGf_?jc{iAIw^V>#350;ZXmYJ7lxn%I=;GN9} zu+s4P$^jCl6^>n+0}zp+EYDlg3gTjd4~7SZ&5cE^nYT$}@k`ip3eUtN)B zokVh-=nc`Xu33rSZ3CoGtIAWgr;I^Ga%tVBKvDC+b(O_dhBa?+c-)>j*#cPu!wc96 z7`?Q>e&&Dy>~UTQ5f6X|_5p@x(xQEQfA5ZJ8y_EMNAk3_vktUn2sW9Y)ehm~M~>58 zFW9A5Ug^vP&mPNj{-#Esz;7IZC;;Q=hOJw!YN_Q&8!} zIIZ3wfOL%2lWomy*4WTygW)&}6qK^;D9_^OrfnrOVU6|ejs5n($k`XNM94uO zZDM3(02Y=C3gL2gQK|j(?kCaXjagBCwjDos(i#at(7xde2#7RDhT7^<8-mH5BOzu1 z@?xBn8LY*YM71VTB8k?ve*(stR#fe2Ngy52inEsCX-CVqBI1H%=P;t}dFGKd)m3b; z%Vha`x79ayJ1w+cvvhw!G5~{AlU~Rd3Aq)?B`tI^Rb+;T3jp^e5~a0&+@5L~0caOV zm@&wbc%F;qp9XNc?7oMd;Q5d!O|_eW$otGORbw5!oi9fq+5(?1@5FiZJanQ?O-T?9JQ45&N882~hDs5n%-(x#UeU|5qb0Ve^ z2^Jj%EYkoJS5l0Xl;>Du&#aZ_rBju;97jf&SzbT$#QL@&1kS7Gi-TRI>RU#OCq|di zn{C`sX`Nj?cBpxnDvhvJK(PxUvZOcH?y0bDo>SL2QW#oXdQ+!ey`U^;4>b%~2a*|) zA}P!YbAlO`9mujgD%gR6ersszut7x8alf3kWv3^zp3@B>!onwJd+~{hXb2Zr_>{() zPiBr(eqk!bqc~HhV7$xxF3W}Sj)!mwX1L7D>#u9)dob2xVQs;iCz&SAu6V`E?F(P| znq{$XIocARKOgoe0G>bZ#d+~e1b#|)dGFiaXvYs7v2=P+Hz#*Gk@jjq-ORh-7uXG~ zm60g`6|q-ClTPNJtZ#Imw22S|fQQ!|XE_9=Dq5<%q7d@HV1Adaxqsd+-dSm$^mH4; zCilPt2duEP*kO5}Jl2Uvc-bzk@!Rm^Jhrziw1snq{0~57%NyX+%h$!yc2h})Sf!Z> zI3)m_6XA%n9y3)fD}UBl#IB8Q4=`(#okJ3qt+nM=RZ(n1w7sWcna3tV!b|6t?@Mvy zFRCux0Bl*d*Kz|HcDSL(ag^wORljwOh0IT7ddZe@8}06ewN7PMNufRbbfXQUOY|hN zhn*@?+CoWZmN_U*y~DQW>TB)aaE&Aas!bUXo;_Q10m+uqRWhb_&YcSa&UTNq-e`s#C(8TE>++XKi>(+rclLZ zsDh2fN6h{cFH)Mlo5~Pm&)X#Xg~uUdRDC9b{2TM!4p4cM`!NxSCC zo!G_}?a?PsS>MEhg{YF$x=N5-g`6gca9XP-^V#UOMBDqou`U~gz}Q!jX`2iE=J99Z zBTlqtK&y{xQ>^UhOoW}naa4!%%pVtRjNQTkK&mD`o%=-y4G6E2{5-*+Fb?*jePq_z zF_Hn$73Br&#OZbigw(pr)+Sw_xZkW_N?TO*YCGJ9QX>0VoFrfIg%zaFFz-w8<&ZqR zVl7SW&MdH|g2WmYwR>3x-`6!}5lLs*8xlv0pW^Y@GP_DNN4)pwfXyTa?cN6;vf|=m z=Pi#`g6Hy+kBY12BOufw7$}sJAtP^f94RDFYmESiL zLSbZL+Cu08hgh<-21sgOuZV{bR=I+pm-cr7ZFH=a#6PHFi-Ehpd%~SmCZcz5RCJ>8Gu_k;vk+KiZW~3>0z}TH3J*BJCF= zR$iDv2O4MjFtn%Bf&W1H)DPC4cCa;FA1jcy7mTHTzsjatX z!cODs9iLgW-4|{II#z9{f5=KU?Xhmzf*<)2Y*jfX2#M4mAQ(jVFe`X9-mpvo0P}3E3(?Qxn#xAkX-i=hx!x zW!JsN{`P--)v~j5qAl?`Ivb(@c#aPIxqR5O;Md#JW8eDPSMAns{v#)RD*o0qhQf=a zqtNED5zPexrLbRvv)-v>l-Cz#?gge0P*80B^~Z5*gdHo{8fYXj@N!JN)v?Q?hsrJk zS;(fsbSLs^6+oLsQnHQ;uonGaEN+W=)=jnoFcV7&Sl?oh$AplrglrTbROOZ?(t%Pe z$Lq2p+GFYY)LQSZ&PR$f<1Ad7;XWmDj^zg<=y~GY8Za#RV*+$t< zQdp2}?|kR$thuSp7KXd5u(*KPXciKkkR3kR328ED9sQG3n5)QfXoCUQ`~XA^2{nrg z5l4hQLZrG!_r8kCFm(Y#b|5W)rlQ=srkjUbZ}6 zB7m~S+Q~)yw23w_x?m@n~O4a6OFo)FlS73<` zb&VrmA-n)k!<6DwU+KhI2{>Dr1FGVS;YCE$8dI4`!Xl}S6RMy#AJKNupJIK|9$o%5!nqZuX$0 zF%!%Lyj?t>IQAMZ!3ZxgbJVVpzTPosLDU1xN&oETPuT}={)khpMrVRA?g<$Mz!&#; zpVLPZ_%*Xb_b=c3ZhPj@U)Z&~aALrC7BDx<;Upd=1!bQ0u69GYEzM3^Zdr{b`txmd zZj=^r&6W{3PYw@QKWi-$QvKxZA$UgEoPp#e4jWr~VTHw~=M!IFPj!bAgo>zraN0V@ z=I!Xf8IiKAaui;;N;2$n!c&!`;>yUfrw^R4hW@anrn)Si%IT6#`GDYxJ#ezm4tI{5 zBq~8dPt#~;Csq3%ZZL0=kmSV?L>W>Rh1Xo_~;lh zSZlU<$5sMiN9@qaR=f4#Q#Mab*X0{>>=Ih`Wm2rCCMGSAon?NKV>;P)+&wU5160P- z0QkTJJ5dO97^2#a(EitMxxn7_zW2irzc$(qzldi^6aZhugMH2(3gA=qcK1(jw~xL5 zy|z3(YOlMZ)~a$+>}W&3sg3j#KI#jwmzN}z0e~>umuwzyh832VIaSOW)lMt3t~mfq z?Qu8u_JWKs!#jc% zZjc$HfU~y1i{1`t3vBKvH5XrH|%<%#k*Z|_eN9+3RR9}d2Rd)SSLAj{zMj%YG z2{bLv!d^E%gXfkenv|G6TH{etUUDEWyok0n;}rREvUkdoXju?Yqv=C(($5U+$<7dRk~N$rZU}^Q z!05~zFbrE-Rgh_P$UFvZE-BG7kQ(iyb5;u>kqyx?h`S>m=}az_pIF;^^oMS$LnXaT z0HSV)oT>sOEL4ll=mr2~MX0#mz&XDpw8OZPVq_@^=}T$k%3%?1)B;Io(SD7J6k zbprk1qE!^<(w0x!mSTt?2%$to+9hR$mS0nAU-;I&W^?0q-HvPmR?}=Hj?2L!tTmhE z`tUvvg%<6;10C!)IYY)w0`Nifg}Yf8Rh*M*wc9pYAG*syp1Ul3`y0mXXz#q;{`33n z(#xVNIA4r&CJKNr#!)^;k8u|KzVVO$V1NIyn{88Znq9mh=u|>oeG^#25;5|N(TsNl zy9yQ<=cJW>O+lqogb{=nk&ke4ise;>MPbPPT>(wRQUE6pZD}9zRpV4tVLZQ?h>`O= z%Sxl#tT)!6|E|BI-aJ7-Uw4Q_c#r1%zwXOSDvZ zkeMKr;dxA`_O@AT6j8D*>pEZnn{}dQ(+ys`EVsC;+Umxu=8O?f?vfJjVdD$S}a9W0b~yR>CaTcMbD1Vv!T-@Z)EHB`0>vNErEIY|9jI*@7%P3A@IGE4Dkfu-(<=|@_ zMgO*f2GH*!gp_u2Bkm1vGJrT8vXTlkY*co{1@MdUHaRqGt)#I$(L^;PjUFfVMO#ZO zWiezGxn7$^NS&3P>5wW2#JOBI4;1B97?(-`E2s=pnTxiGVd z*3dC%K7t4a0q998(^O`E5oUzh5GpOCKX(D50?vHWTO?-*MU+ir*yh5H&o__zoe?WS zOI*ovBW3=H2#ElcN`amQr0039`BZ3Phm{aL&G%lp1INeAxLtPXP8%PYw0n;B+M&8u zd*!x(?c0=Nf#NdDt=eRZv(xsIyZ767Zhs1mB6CJREIOdSr7{NzQ4&DyhYWFBds`pz z{2}Iq*&LG)K#td$;8pA=j^A^`wci*TT ze&V3Dazgh(G=yQ6E1C0c3X;xDTU$u$*w@iPW^|0TL2g7yW|>Cz5?WZb#(`rz@QH4#p`ud7q_pN4BrgI$=@+pCJ5l_7RF2vRClE|V zhN4hYEl$t#Nb;g|=>))@1}2V~C?||0;`GKnlnR)| z8wYs;V%s>6C9*~{#U$EK?f0FdD)r)4nV!cFoQY15?<);@jjAyX;F+eXUB)`+gAJC| zFNNbE0 zyBZc90r;sV+#+s^ab@zn5mB#3909ee<}mZo0`!EPosD+S{$6Z=ZamVN*4933Ip`+^ z=3oEf=k30G?zVS-@I&_cH@wkFhyE1^3fd{5qqpDb2t+Hu-|6t3Kff&S6I1!&_q@v< zz55Qk7#YdBfSZ;N&7P{goD5b^x-Gvbmx>QP9H3~fTq?i?>&M9vS)8}DtV{>c(m0Vl z0NOaLZVbdlc77I>4N1sUGE-R6rWX_J{sV2+$?Q!QIZ7?d&+%Jqgy4XcIcG2MDv!z8 z4&Ww1B7_YynVo1I0{qsTg-I@eX&!LYQXy&MCUJ@&QMNrbIgkN^#O*KHwi2Hsj$T1r z4G&`*inDAGA+n@3V=$@(%;g4nK5l@m6N03W>S+j2k082M#i#gsrCbRdB}SeE7;AC> zS=eX3Wz$r@K8PDXz`q2rEz-6JaI=UUis1*||Hr!scbK&A+;hyXzIZ*L2#7L2+5R!BC?n)W{Ek(fW-lX zX@tWj^M2g(P&*_EdchR(1Yx{~hKS~(ikx4HA>eh|8W~aaOf1^qHpeQmJ$O%=k!Mz^UTC2c5f@9!5+r?vDHkX9xMMS$V!qW1SS7eq zz-6jATJjkZT;vidN9eqFd#Q!V(i=j!thT%;fUS(*(Q90yVjRZqrWx51V5Rm~JA1)U zhp5SbaGB!uDcesXX+=>9D+GSpQYUsf4pu_oD}S>@(aG#ER{JgcomOV1;oTj=#+F2j zJUlsTw>{cmSMMybRK(dcgt6|~UTKqrKQuM=+M}l@t*w9Bc9o>r5=q#V`5tV8ewHc? zK_t0tEiu&^30-jO@!4EgSm?EUNE_Wk%wRjn4V+sgc_v6T&W!YQcZjI11#2CPAeVtC z;Wf*K7a&sXo`()HUg)(~{n52fC3sFhaDPA#9<2a>&)-Y|!vpvI%>ME{?=-}B_-0G2 zG{@@zf%I}ysCvg>Xxqu7Tci^5!Oq64M63s}5;&w%J*5F6T3nP!+ohmWEdUBo1**yl zFgLugI@7%6>uks6m)QN^`nF91LU90uBCgtCJ9DX;T6#tu#&rmA60uPZ0KfW*&E`S@ zSW&{f43ea8VA9zY+=B>skVqg8}Jq3S%`JP5fFvK_?mOWQZg zb9d9H4-lLnc2wF$ia0CDOtpUIeRDh)$zkT`(G`B6_EuHk5|wBwV3^MD4pU{a-Gj<9 z69R;algD3LiFIT%NxbGv#smUw36w{8jcfew1y$K-3ghiiBRh;44P3r0$GWKsAE=uk zgK)+6mAb47eP9Z6x~!Z+s^uIiTe1op8tjo@9CPvxH`nCZrrp~tZlT|NnLaytyxC5j z9bv!69fSakk)dH0E7;zs0>-cs8513`t+O~@>au&t6)YaRGx^6qv)b9{ZiqxvC z$Vc#;V9o6;j6dD!TwGGbK2yHt=jT#6`fP1s&PGNjc@0GCQ@y6dMr?)GGe_%A*$&G- zZ(YbJuY28%_Wloj$f~MqoDZFR%?^d`{J-dbp34!40^oBwTIbQv3j9u=I$<|nbESD= z7jRQ}5CbzCqvBfxXmql39#btr0^r7QVL8)0E-VQNg1Z$?dd`1ld(=@>J(8}BhSh9 z159JI(IpJ{FI^XO;^ddn@AVQvFOjt7e>S|BzCD2nKiPgpTmOMtnSvPbw^t0h$d`l;@cI1a)JRh}_0 z!D?|;MGEv?RKj}U^GINl0nfcPxy0sY*@I6vlQ5IYOfV4{Oo(>XP0GxIz4ppVE5aY0 z9w@Zdrgrn?mf6(Us5RF$T0c?!hnhy1?X6fDBZTW-eVIkbSiJkrU)TgB%n06M#risXClvenx?XeJlxS2p(EMDV-CwysU`1;-ZrT?e(SGD4BRu0E^rYG4X45dV~}f z;`0Sym#zyq-drWK3;=RjRETRxQ95afwO2%>MH#LVksgy1QvmS1)3y#FrzyuSHw?f^ z#v_2%_O%57My;^gUq#?20cs)IX#sm4t@{ui6p+v@H=ywJM#RL_^dz# zwZ6Izq*GzWt1adNl8wbqUg81fqN{jylVewH9kF0nZOtG!BEhP+Z6awV(PGn!thfHC z^%3zEqE&5Z?y-|yXTn&wZz!@WcU3~sofIoHPW$ufD9(LbRd1qq9~<+ZCbO zyI^Z6&W!~7?oW;xQb5~U?y`)ap9MtI5X7*e6@ELy9*)BDT&li!$K*dtg)#xybRu7w zVzEybCz|GKA}|I4BLRbe#m~=gV7Yl&VGyUtn)MA%SV;Q2`6cH$RhDEqAUXsf9wZ`* zQU!mnk0CyZ()|1!K&bLE8Q>YF%26g?28+z4L!1~L2e?RB1}HU)RB(YTc?*b>!7&GX z2PcW7hnUdjI52~+$MLgT&dzo>fNhn>gS>WJAClK7f~$>|Hdfz<=$QntH2V{{CUgHQ zRDU%kL0&@$nawm?=7_nc@+-zZSeoUsD|ZxFWO@dGiswG!Y>3$;sSu0<#_ifyU&;o& zDf>U)e-Nu6+QD6=wz;~{?t18?ohCC;vYZT@CHwYlu)Uk}l?F@2O|^;;dbp{MR33t7 z8Orvf|0{(osV>W+>SHbonKDP!7@p+9Lbcq=%7AzPP?ki^Q6DM-MU6N_AUJWrm$3XdoSz#3Hk;B$(SgWHO47UkXoI236C} z?d8k^HG5+bIi6faOQq9Z4wGWi#~e>qwPsFmv8V}v$}sHlD8Nz(d+Vnn9OQT0lve_` zFl}oGkyP2VlNEjK$A^PI=uy0UJ5zH$TPIFoZ*1ukmBbdBnPX~BxuBULk0J2Wddmb-p|$TLoi;L--*3eOJOwx>GYl0OqKGcVXldVv zLKajJ>UmD`Glx19WJ*d4RbNR)g6*K13lJC)14z!$_V=;F#O+D9W36MhrMApzVQ;0=XiPmyStxkvJ>4?wD4}b?&{rkwu|m1Mp_35OWzj6tKg18PZ0w8Hu<(B=n^7+>ae=uxAcFZLfOGYY2)! zmK41`>mv{az-N7^&gBoPwV{*&_ym3@jvltpee^?U_Cog7m)F?U7nWOnFX5sjo|Li^ zqX1+7t{qjxFNN%``|9kHodtH$jzS2BM7!^a9xKWB**Zc$>l%kGmy_yb<2Y$B>9!TX zjCZ91fbp=gtg`gr6k%3({PYN5h@C4Rhs5lvLqce7rg@>}Y+<#PSjm#{x0VyFwR1;} zGn=F6Im6ZhNMm^Iv=C_$*M}AvX`crH^bVM6o15e*0p~}wZrF}km zy2A=fN^Hvo+s@?&_4n?vqfU_Di?{3y@ad`EdE2e_*_%Je%xKv@`DYh8GM2}W^|Glf z4f%@)aP!-a9hJ8KP`f?!=qY>M^;=15DZv*!XpcN_+%~MwwW?yTJ#nDZHeXO-$4~Xb zg09+yyUQ)vi$8S=$;lMeNjQmEEcWSAeQnxQN##eCLS+^}57 zrWet4A<~w1Jh97EW$FCP%q*-gb4D#ribHxLi`=$t+w7bF{4W;F4md#kh0lNXOw1Jb zBlm~&g-Vnb*)su%O9RNmo1fs!OL>RJK& zC1(|=09<5;OMwvlTMW8(GmF9Inq`kZ{Gh$<9q)23l2KCR*`3%?0DN}G>74$Qs=kH! zdHct|`IO!A^{?9Y4SDv~S8uXG2!})U)3%*>tM>LGPR0@|FU_`}-G9QKI^1kGURP`7 zU;!HPlkGChmfnaWBV*K)BhHbsRqNxLRe zLY0;T$c2GS3o_e!=wvs)lZYH9%&{d2hPhrQP6)NJbi>T(&@e*61Zlrlkfdff9UlaUG^Sjk+Ag9gjK(t+uv~&P z>1<~=3k-EK4xH%i*&Tr>06x3pbk2V24B^pfpZ>tR?dZez+jUn~*(>=oPP^4MxL{Xc z(R%!e2HS##EGv+1KYyZyuz@za;hHilf`nKmJV5%m3wIZh^_O7x-gg>zMY8QbI>OLC zY&YGo#o8NKWywq`n?e6%XxVa#gRq_?!K_f7u-PmEv${q#)`{ceL@UGy6_r}A3z$8Q z0vfr1pjz7%N+W^X5QN6^P}sHtxB-Buhn82%j?w@;wY{p&3SmO)iR2PFG0WVJTGK8? z5~76aql6@+@_1TMBz4LX?XpsxN11Wftgo>j-1_fUSy}NP1AiT(_r3oE_U(W97n|ts z0VJgC5eRBQInEB4(-lzIS$xFvS?1rG_lbq37z9KXs644wX+t%y%mtve6S=cz1liEC zxoB|{Mn;_sMagqq0NoTVXxH$Rv#4E_SP4YRTdu|>f{%He_ncXid(QB-SxB`ICL6$3}XYzXGQO4VeRt~3L(L^ z#Oh^8kR(!%BX@9|xuDW!ddC(CPne)eX2ih#Xjl@xJ^LdNodG`kWA&o`q)ux6Ir!w` z_TD#LPi1z@-tormcG+Iy)|pxL4lP)9WuYB>rq!xSuuBmEf2w^P@N2iLs6w)EgD~bN zcaP9f!bE4N;7(EP?b@6RyE_Jnuxf9cuYShgO38AV@QWLeUT3PeX#(;@a+68GL#(MhyDnyR6Fi<~c$>CtvAgcM_cy@r z?3(*aBz%a#&;mUm8N`WZFqdUHV9Y5!0;w zBZ}X0@dJ~*Hif(fi3aP&#d4x)kO0KqcRn=IS=>7-qdceMW`;+#>u?`EtaLWf=#3H5Y z{8Zajn`wJ@3-H^xKFEo*Xl=a^6mC{F zo@lp=F35p^ShV`O9@xSuyM#*5hh;0lMPdsl=rWqN*6uK}nNi!hB|soTqP6wR*)>K0ljzgPt0UumWMIq=H%>Xy7`49}NIY3&_M}yp za%yPpq{P-*pfn4x`Cu`{vi1Xln!Wj{V#Lhm1v6oRxgSnuxljZ~^4x+;h^5n<(A7n( zaB6pD-_x=q0c1b6G-6uq`?ua|8#iwJkIL`Y-yi{zAJ6l@-}-$*QDYpy4I#5xq*a%v zc8UAV#cC#DxKe1GB~8p6l`7q-iv-nc5i$54N=~uhbHv@3=4CovhS#cICcpmRDTPY_Hzx>pPq_zMTb80qkxuE|24NUcbJ?GRZ`YPa-D& z#A*BPw{Nop$J^M260!tF2xxBNTW0{@l+bFRKD@C&oG z?JJ-Axc&J5e9N{0dau7`D=|%R*3U`U(al;$Sl+(AA-n7%W`BfePBY`HD)U+SI+9)( z-ZSpESprAH%%tvVvq8mB-s_N@3H>?2ey4VBv`| zM}l*-mCQsEUs9;ZiYv9nERs)6TtSv`veEjt1m0Fe6DDBJkn-= z{lO0)vl+CTKk+GNSIYlQh*17Un`P(o5Ul{6!*G85M^#K$M~8jrPj9jZe*7c5;=)3E z+aGTvvdM+RVB8wpNLMb(vf0^L+X(Sdk31!bCFQ{YCnLLZRX8fOSnDArf^OJ^zcWviJ?=>TB-S)o zQP&#mY=Y%L04z~eNw*iF0#OwwYngWXdRcp^t!oMyU0|-~1yGV`iw9`WrfG*&iAlSs zwpQ(^4*|4xg2;=jnpZ+!eecZ9UbV)g)u0=U<37IjjeoX^ipu}A`aApXDdbd@-ktaS z9Ge_+8lH=@kcqF!5u@3la-t=UR>h~+A+fW5Ry$E90YbI#Dg3PV3Wp$QM3SgV6f>+` zz!oGjs&+4~$s(wg48O9VlSs3`@faH(aYhIAY_}^eDZ)0GhIK9-*G+&qqSpybIR5w( zcHhsRwx?PUHgk+>3OrVo=O!Kt-}IhZ%OkdIhy_Y1Jg%fgIqVu~r`=Y?r%1B(SPJU^ z!M3)3+qyoFfQD2qL~C{&NmE5pI@1*V0X4aF9-G)?v$1N2~#rRP^# zBs6T@#~!BwTLc`iiXmgs!lOl|@%s-CB5PR!oQSH*O{NM-GdFEx2V30)XcO4$B%bbv z3=oU!M=R*_Qk5_>3?MB*SjW7Ks4Kpp@B&5Xi;!`gBWXdSVWphQqiZTfTql!#MPr1k(_P`@g+R@{!_F(-m?JE})NRHja>R)_m zjqRfg^MuDYQaN{782YZMAVavMRFaS1yyqM5Vdt@sNB>V!0}>%I9|hOp;6D-RYK|Q)Ek_UK?&c$qri$^Kp6*XFrb) zZE-2i9y>f_!^lqP0qtViXOS!skYs?0Y!YjlK#q|UEM}M%mbn=xU}X`lG~ZCppFF#x zfJhHg4pnCQzA_1e0HXj-8o&sECk9vmxlEN7L*)qmIRa)e$Ma|iKk=zg+DAVA*F3*x z_XfEjKDHtTe(=Hj@h6Y-JUIaTt`cW-7Xm*YE*QyP1j-_3M0`*XsyyU)oe7E1bCy_I zWY91dod(3mB4#ec&s@filO>21xj{Vq{tV*=(aH7fByJd-wH%0&Ed{BLJ0vzC$=X}H zZJf<=M~=7Ky>&xQ)vHMVJzHsQ39-n}_3&6MPvUB(IIaX_ICaOGsQ!|ztSsLS9P4uy zSWV-P?k6X3JIOhP!DO4JvL1)PDIqAH8r$3`IdyG zNz1i451o@1F$-0}G$$cUWeS6Rc6Q{D)Et3n3?d@2>u^{=(nPi#%sYOy=2pUyuDPlor z%=wo~yO&6ND?zkuZfZ+a`DIdhX)$>h8{w8>6YcN5^d^VSR;pc`QNFa(2mjL_n$HTL2B`c}=Qd85$Bg zFy#3(;Md>VZ6AK;pV;lUd>8Y4vAzCfOqg`z_+;ZBcVPuzA8Tu2mzQ}G5I1+pK;E=@ zFgCCSzyV3i42jEagJH+Ml}pR3S)Wp2{8T;DFt?iLO`%EC^6&s(Q_{2Jgy6)~O%fNi zXG&<1l}v4^+DcW7iM(cgO=~hgMBd#e5H{=fT)@xy1^(mn`Pr>|8MDlw-TSk9VQax| zKv;nZsyao0aEXA3Np+HF)S9ONIs$X0->5Q_qonen^_AbmdzP z76hHp*Z?3s$}x=N`Q)QdtiYM__~S?I=l9-kn=aT+EPviF57SE<{B#gBdyd{-#3LZ! z)1MPZ4%(l*`Wk!e=RdR8zHGhS_^N8>!yO{3NQt2&QJ{U`u131dgPm zO=~*Ez77D*%LzO@!B#g~$bA=;@OxcRt^RE1Nz)!z;_V?a!(@j=cQ4afzB#5~i@kg}7Zk|iLBd(sKl~t7| zSq@KyN|g94Jtv5l8I;ueJumh-wX6~(4-f!_hve)ci9+glT~i`NHig7&_N$gFUPDjORcbtYjF$+i%hWaTIzM|f=P zDoHdPm)yd9sydGE*raVMWd{nDL+%O*Q-U>;)>FM>2UR7{mE$(Yh$J;59W7vz)mCQ# zt_xW00Mm>_d*c-q78&WZx8HD`?SK4X=QHRWVCO$CT_B8BfWP__yx{*5@aguGAAaBd z>K$*f`Kb~6Gv<9eYJ;4d_+GuK=EZ=&f)vF?Rp!sjp$(2$lG}yDfmStz`H(!Ocj&KmEN1f)t*vib%|`f)<&y8*6LK3vbU{L zZK@fXU_Zt2_Bq(kWh=~L**Pt-fN)xQfTWLjXcMOZGR49Vz@{iIdX~@uXKu!8(sS4S zoSFMAaKgdNP|37YDXR6=%NEIyO=UX?xgoI0fh<{tpm1h@RAX{ssQRObv@d+=%l7hD zyy8DVOvmiMdvoTGczkca`(CF4l)zc5EA?1%p(u)9PL3I5Ija1U0X_k{_K9>->FG6= zBkd7T$Kb#i<9Uu?fy+QIm`?k9v}+PyvDqFXta#(#P*%CGhz(K!&oDD>qh0O5aZ||q zl@IB$0nz#efYTyNj~bE6WKdc9S?(0!|0i;v3GyTN3?kJ5TzMZ8yjifb2btNF`O_gQsyHkAxA4NkH`=2CgcEo9_R z3zGQ4?G790|JCyXerMm$&0Dro zMGx5{PaUF#C##TiU9nj{j<>936wd(mnOUp$lX%G^l*>awuNlk^OOc|80FkUl8bX1i z!V>ys4OQ2xVFeB4z0yOCXTSiS-xBBe_gQO01Qz9c~2e zH9!tw|gCIQ^! z$b|I_OxPAyc6K1Bju5M#&)(r=UR#72vNp{PpdTHBoz@(ATWvn_m{I$|zu#fm1%(7< zY;Z16XOGoO?e8c6z6kHQz^}Kb$KKD@wx8i<*jnzhSM1Jre55WwE+q-~2d$G@$t0NH zWU4=D&z3mpB5WH|`P9G?o#C;0t7YSx=7VbI3i0B)Iq~L@ns@@aHXA}O#+I`TcJFHI zS}gOmrMX!f=;^!-`y4teQ%jsUlYv zW?|FGD&%qElW@}ToR+5tk=A4~=bN?8(@nU4s6vyu=y0)9j9D@S*V-!nUor|iNTR;t zReR85E&!ebmPb%+GW(lX7^B2;U-E&Ao!82NtkgK`fx*7*CqG4hhyZ)rj+ZP5Mgj15 zKfiwKJqY;pXa6rAv-jL|gEbvJVlUrQU|T9OoQR{%n@jMVGGC)i68NPsxKHzEQdPxR zd&>~58lQClV*g`xfF;4Fh;Ji0Ndc5V0YJ7q4d{`k67MoMu7gD(ryE| zNGfQtb~3c@=^b;dZ)#mtT`CtZOw}gVbP^EuvGix08Q}zaM9u&Fsfoxn(%Iy8MrI=^ z$^iGTax06t`M;c&+{#g3(WQ%JMG3B-++#8+Md|| zv;$~zp~%B5ZK5=TN`@W+a8x0RmEKaCMVmS6RISn}&huI|Clpa5K+K^UQae!3RJ? z+LzER3V<)rn=Rne?T4f`f9#!a!(KLG*I!} z3d$*KX&j(BSR!I+f|QpOE2B-D0AfI$zh_p9X-)$ZGB-!ucs==#m_6BPcL(j)>cG`M8yg!l}FZt z%R{NqT2wAen|5^cAa9ZOPONVLTb*WZa(ZlLkpFMQTSnU64nWPf#mWxSN@9$LVI`RAN~V z_pPLsLKbxg{45DaKR88%y<(FddxrbvMBiNHwH!`zX$TXUGle;s=V5vufBdu)dV1x) zbykYVR^h6{#JB5Y4l>ABE4`_<$odE^ZSBW5j4)d9Q(By)S(-BUl+GMh7%HHsMM@GF zOWdp~D#%<4xWxgoNNc_3EcK~n7WG1d-`TfXVre->Zu`kkSQ`p?!1ER0`KeG9_8?)l z(yj|K(;OC9t2W($pkz7`+Rav8cP%aEGP+5TCM9JB_Q-(-Rs)Q&^quKHN!U0yO2 zOUi6|c#z+VFm518J2TVL05b(EOm{c&`yNN1Dfdfga-0f&*jZ^Xjz-bT#Yfej(rF~D zPvLb=1EPxF4^XwIak0AV-bbvrzu)#?FDw(?lL=qiN3lx-D-=|oT@1e7BqvFCbOuNwl@e<7i(>Y4lx(PGfU+*Nj0Zg zUQz<{K3XMxX#LD@5@Bi=uuRQjeT&c*Pmp9XizGufGgSrsBXbrDal*(BvP2nySkqtu zLx9f!^P$H{6d`Ti_K+fz%So(VyJKS^9s~IbJ2M_8Pq2r|@7k+sts0a61CP`>C*v@4 zI%WEu|Og1Evgw63RT~bKJ0NIENkw{GgBs*qUIm#BhSTb<NX1m$M3TELd8KZp&u_tfFi^eB8(=;RA`}VKv@zivT}9 zup%{57*r)@#&e6Mk~p*qMA-3MT@!Hp_SxpG5Pjr)%%ZYj?8Y%Z`uW`<9}8c$F`C}nt&-~+Ls zxq$EzPLETp(NwEA50I&15_kltG;;lCz)uY7I_`twiaFo1HBu@10D^MjRT@7Le%3$V;(5*_u(gAb9yA_+>YQ>n?ZB6c{Dd8B|$k`y6(3ykhE z7n(vU+8$U{wT~h?bVZYUMH)zjzA7a<7k#FdyK9qOMjBi2cvIvwNkq~jVC@0?RV@bt zUNnA;5TK$d3L!82jVJ!VF zgK~mKspgh&dT71lBn}Z}+by$KLh~ZIF)GlSX%EL5&I0KO$Ho@yk!J>3?-*yhcjN(n znPdgB;{#1zH!05|GY*{?Vwj)CP(J7Y;ymqeZ|{iBQ}Rs%VDrrV?hbsiOP5yvuzO1G|k42Ox9wgY}hej*%4J}iXT2CWy0%4SO(pr+u26ppQyySFs;6Se9kXD59i7Gco(95qPO4S2_QjE1<%9#~*LtsxC8sob6%< zhbxZCfH|8ReVsQe+X6-9&XxV(qlc}prOQgQ@uEVKDEz?f4dB%1zx(eoiP=6fL=B4VpG z6O{vJns#3F_9#miDNW^8Jil!N$XJlZXp#Gds(|?bMuiw`gq7EPu$x%> zc=Ux?0Ovfd>|p;G`aQSpC~@2UkF}Dsh;9-hL?AlZ+hXD24qJd&D9Bq4?i zp%)^p8bU^7)B;srJW{9>0uaJ@$hGkCL{|;;F{^<&%=FUs zP#v(~W@fm}<|oF_uz$>_;z)y5Te^n)MG~4+=81zt^HxP`J4`#Q7HE}|c7(Z<=037b zr6aG%%JK7aA;ixUT=m0Fj$Bt?$nlG&pj8_jW!6Mr){MofnG8Bv!Q^?>9^gVG6{>pL z080s?Ll78XpEDOMLF%H7Wm?@>fIg5yhm#f$Y&Xlz5oN#Th8t|#&K-8s+umkh`0`im z)i=DBbd!{GR{8zOVa)M&&X3^+5wc*ZLNbyc&9Ps^k|4pZGr3z^GBtdr&S3R#FzlEkP1v=2OR!WC zM})lL@*M?a(JAy33lcLgUoZ!kMxHa%n;0FlUp&!h)m3@sNx)Ou-f4M-X!ueCd@#9R zwzI7+&N{Lfw=aOSrJor%t^|G$G;!P&qeRrF(7GhT3J>>&(GepTvnS zVqk>XV<)q#F#nb>HWx-%QkNK5&7R?DTh9`v5dPLh0HlsKSuF7XV#asVo8I&r$M;uk zuReNbj^%Isz>D~Q^j;1O4A^U4d9|HBUXS#MbQ_+(7-0pt3P7}4Gm#68epa646k26) z3i4;gI+(*M{z{S_ol1%Zt6+K2GVQ7^QbhnD_y~ZC8OI{XMa{+Wl#?R`0pKj726=tw zxY(%FDu+}FW3`*0noj2Xn`-lDl@YTuYuv4Fg8A_@6?mpU#j>bKmFxorMS}N~^|f|d zQE{f@0$EG)*a%uc0jgXf{%kCf5HSm+eM`rwT_uxLp=rzkC5`Fp7+{2wXiaTMWRNkb zdL83)@V!U6ZE!x;{^vKpW!GJQgY(Y(>iv5W->4Vpp`sPwb3Cv*2^JRS?Mt8iq82^{-Vm_p4fKeV{0Y0waQrf4nt3oLr4JYdD;K4TAOqz2r9rA#g zjE|iWvj}skG0^rguOaAimBbfMjy|wBHrVsg-_q&)dzF@jk)AU&=b~?YO^;#sf_tML)y`!C39+frf;Tevbw%uu-si-V%s8h;|7Y^mO9OFdiqPU3musAP`3m$JIp8*fCS-lW4sV+2$gRs3+b*KF4ip6e7E^6?E(B+fosPwsKfn6Q@6*4c72tDtw*-D&o$dD5fAMa6 z`jH2%8m-qx{tV)laPx`VSnT109piX)<+b(Z7IXEDXQ|MHJ^f^z12lX0uH$E^0PuFM zq4AOcHkMYF0T16_uyoQ}JOHMo4GU-qds(ofY&Q=(c$Z;^L(?&K@<2TwawG=98BQer^z<@`-^B>j=@R&ibhQsy<@!QC4>^oP#w{{FOlP2x2@WFp zzPSKRpz+g+RJG|5s_+E>*297c5gu@8Ox6ZX+he#Qa8GZ(e97cpH5e*bRK3h-IHXF56b z=Sf-G-uyaiI(FFhkl?bX#&6G@9`$NW0fX9MZ!-KjE~Q z`z|cOmkHZPn>js1OG_IWhqqH~@Z#b;S7)#J@=E|kytjk>Hq_B)tF)>iO!q#P@+gA6 z;Z%=}a1!~MHBCV*^o&FRJS=dWG|TLQ$Yp28D7$GVm_g~JRJGUCjg^b`+5-cvEpIxB z#YQgE-YJ)FUMDsuf(HLeOpb?GNrJHHVO}V$Mi-KmVNUEgDh-|3Vq4#gRR~e#Dvcp?@nNB;bmxud=6}dI~~fz{&w1T{sl( zpxq%ZDx+?WFV_iYR#=2zI1G5{_jA$rDQi%BlHHIblDvrMnBtVuJK$6`o5O#CZHpym5vU@lEZZ>sy zNl0&y1|b9pJ#=F_7z4I(x7B-{8A+qwb^p&Roa^7-WV1=YmaK31fsHg8&Ai|Ho%fvQ zJm-1z{UHKhG|yKiCyY=fZaX;O*c|4==M0g_g}LSspSd<|h#AafG9)f3*}~{MCa`}d zIk<#pvcVw8N7k z+fRP*9sA66e~*Vo&@Mka4<+IGw!6IBEFC)Xk3O-OG4(zG4!+7z^ z$Y$;8EXV%u7Jm#_aSI7M1!QetC?D@hA5qy=tg%ij;2P2@TOsiy%PEq=EObf zN;%zty7_!;dTHDRL~o&s*hl%-$4%md4Y;`_9aRjyL!)7}Ohe=#$t04mNX`3@gO{km z|M|Bs*k?cgf86`|{bjHF*0ldmDjC&+(h?_KmPUChWL2eYHr;jwxOT=06%-|ID!!gF za!LN{KuMcB6gz)DiXb}8rTZqcqj;Jsb+u3x$QsJ{T^g-Yz=8+s zvO3M?szeoVpTZ3%?p_UDy&-N4Vi$pm6C5ttIBARCujWc4$u=ZkG}pa^?8b=a^vUI85I6>Jou#$5H&isx4Od`Q9t?gXFp?K_|li%iq7QVrw^cO z9}Pw34%B-_2YW4>sf{Ss2nwJzatI9)v?Svn2lSDI7^)5yWqeY*QMsH#_N{TldJa4; zH639B5kaqmb;_nOB4rkJX>*t-?&fO)bkW-|zk=N9%1&`Q{bZ9q;D%&6!3ku4hjEgV zXUmF}g_zf?anVRtK0yNn!+LP!Az%+_8)`&SR;js}QFic13lPWv21=clQCLDJI>M5( za%t`$c6+#j4~m$>(&fBH(9v_c(1x5uXD}sI^X5K>Pqv~sEha-L`)&!PyfVQD9(=~i zF``!xjHRPLQy*Rc`~z1`H$Zh|h5aKgZ4cdZkF8mhX;+_L3XaytM8Ke3u`$n((xh21 z#|lgH(e(3KcQA+>SHDrg^v`NOsm|#RVx~ zymJx02qKe(;tcO~l&oHz|29;6x*%s4urGbQb`8ot`Ix*HmgZVJ7-Fct$xR?d!XKQZ z;TFMYoaVvn=mh7{MQ%q-L!KypC^5PZGT;WP%1|IoPD!>b%3;krKl<1!H1&4crfuh2 zVxqi8X5PaKfdA#1>4NLu9gjU|AHMb~t2lVTF5g^gZ{1X6&+o29>~g+cd0r9I7-96* zVr|*VVgeD}yY7G;LHgVokFU>0pfa0AM~@BCO;kf93T59-TNm4crAz=EJY>BD#bHkK zVQ!Wr0$UuxrKu`lhhC|%R=TL&P!#sR)^10!I-E-;vKTsqTr6ICt<_pPhsi1vZ7JEM zR6|Apx}(TGqG`Y!WAdN{j8LVNnjovn5KK*rCA&lOjsf8~%5Tkx7D3ez;a42CFac1F zi!pBQWK^d+$Rs;B<%RR<#$HHgFdnKzEe(%ovYK4z7mbY9l{cb3Ly7&-kA4(EgBx7e zbEap0CvFGr1C@C9-*cDyvYH{%0h~in>P0wJW=fL$(x_Z7Yp85c0ZfpI6s{!@F-mas zlMU8@qs9@OWFA#TH+`H^vJ}!fiw>5^s2sGId1QT>$>vgV82T)YrirLE!%X$W@ftDE zS+e0{R>0Am2bL@tWQ=h}$!5heL)-;zWggcjiH>|Ksx!(GBV!QTjir)dKlR~T90a$2RXO9_B9WvB=e(-Kjd!C?^hy2jdedvPx&OyJDfz&i2lF`tiv5^iTJdms&Q z+Z9Vv$#BQ*8Jc@Lp4ee$p0m-?GZ@F4VGl0=o^Zjm;q|!XN8ho}fAsI+&hf$?+`fEwx0iY=Q&_Msv8 z2Bgqmt59hZk5m&~Val+%WPEBsXtb@7fqIF1@?}-DwutwU$n2+TV7(uS6HM?KmmuER z&rKzf%n(x{EtI|bJh22&nUl-YLnW|$I|||XC6XyNk`+WzZVB@ePp|>3v5BAi$VWe7 z*MIBVe1qK7Bpu8`%Go{8U05<(vN)angI6&`>H)5y z*aSr=$TYM7UvT+-97s;43`sMpW-@#P4rqEbkd3u8B67eriwBCBaHQ=>^OKp!L6{yN zf$l`JEeYz%U?1KiG^z$rzL`4*cqLH^KrvQ%xYn|Zvm8%wxSvuTADU=H7skO46X8R? zhK1++$zHS~kqJ|frPQ5AiQbGdIopm*Fp{yq1ZKvM+5PuDU@O+HfrnW12Cn{_|59E6 ze0_~|6AjP>``TweW#9kmzrol~w(H(=uB}{{VbATX;zGwTO6I5YO7Nm+8ct@~^I3>Pp^GK7(GkOE3$6Ph(`K*0J_ob>q5kofHTAm69=|a zaiqgu+}%l;5>FY8hXdJz#?Z9salmds4U`;iC@-9*HPG?bpHm)VkQW(4bU)}P%!Bydr3v23RuzVAUekb zc&juQ#--q?CP*eU3Uxm_3HH+bt4GK0+V{N2zV-bdxZioIGQT%7_-SkC_0QdW9%l4; zG$A@2nYe7}3j3#j`6L-zguSw7AJ>F&Ix=M)J|c{(>ESFx>ADMx6;`Y1m}DA8RgB50 z38e_q1FSJZ&?`WgqOpmfLT27Sf>T{Cq5;Z1-5==b7EGZ)u=mhZ#(n~cr=JInXR0abWx@Rw5 z$tY&Gje~XeA*H1ND7JzK_3c4noAYp~%SUAh$cCyyA{vc2)D_@aT!xmUpYI>8Y_jQA_y*H_&nfYiFyYT_MGnhUCi-P z4mBNZvZlk$WQ6ICKpXBGw^~Y?#VfOsY>%_OFEz4J=@gSSh&=7*w8d z)RiPUy!`~B#yX}z(1w!$LNr(&O-dKcvf!1e)RkqW%H0-%oJQA30_i9cl+o%_7SqIZ zK}$!E)5DVlza~AT7^1$nG7-7931d|0rz~cO8&6&zM_Gfb-hGYT_|u8V_}ozIM&;V^0f~k()5ZIVCo2_9bMSCCqk6GcLL(y; zDpSGAGALUKHpJfSYi*=lrbP{1W&r-_TxF!}k^L3@9Pq=;hbKV~V#b^DSqHAxBnTef z%ibHH`7#Hn;O-|XXoij27g6@U_5&YsJ7T);jai)m>LTjj7oU69uDkZHt!Dpjd*@}# z?5ejeC)+{&gA+Ix%ta@%0P8NWv6V}6jd>z_Zg(Tpg+BaU*+>LH;b}548is0O<8pFC zog=-_V*Hub)l|pkMm+_=%%=T~;5?LM3{DU(buO)E+`G@$S&`2z`iIVClJ{VWL{R+G3 zXE#$;F)ntR2fq{Jx+XiKAe!r1>vKmlr@alYkyl@R)%NW>Ocss#JSA?BY+O?G`6#mJ zdEy3kkbUX;NB~t9m^yq|W*1B(XCy;_L<Hp)KKx#bo6}<-|Ih_?{^l}3 z;UTBdSCH$6vCpQYob3vUgM}x_kPV*Rt&B-J?hRjXUMw$oJ#%D|f zULm7$%)kk=(`FkXfOdir&LKO~yl#-dtPZ3cz@i|;3MAJ=f|?p4KdV8~$xT6wbiuES zQ(MFO^LP((h7)Ee6_i2bkyHvxAy`;XB*8L~>;n|a`TLfGVZH5&x7p8bzSXG_&!*sa zVjw*6?-CB(dcg(u^s~=FIjICAjbY3YS3R;RnXF4iN7;8KBB>o5IKmEP7a%dycIYX> z_u@E6BB+tI-a6nS2$GW=Ork1@J|pKm=___{%@#6m-9i&gnC&>@Y<1v(Ma+30Io9S_ z0sY{PIW(;_ZWqJv5!FdtK`H$d=KAR5$DkS{;Z~?Q1m>=mdS|?py>PJ&;U6;z8$bs! z+sZX%WUQ3$4pbUrVU&;B+Q=fwK>3WC{E?upaAv)p8ZZTTji~JZ&E1bsne^GY=WSu1 zQJPQF$E*y13VwY6!~X`q@2A&))mA`B_~`q#Fr!;YP@HQ=(dbJ?VmtxWT&4E|;CqSC z8zu=74?fob#)AkzMhujJbVo{^k8IUE826EM2Zg7^`EzjU8lvk~Z~dL9Et7GDz}7n0 zcsenonI=&1(#anNQ;XuXR(YkNdl}rwtY(;v>`P07=cQ?lF*lRi1J$J!Gady(su0HB zA_z?2k92nxsF%=Htc3njiQ$eYF!7YF-Q0XS^%dmAQ4@(*N-YY$${r=+sR;%dgeNco z;O7RBw7sSjQh7~fjg54bD`~)7dikaH^ILD@=92J$N87Z9bVI;n4>YLC4 zOoZAZ9}$hg4Kg}NV}y)6iy+v}0VqmI9C)g_x)ac-&RmpEwhk5xHk(a{Jptl#2hSTM z7|vBD1|%U`R5bf#kj}`m(sbxB0Wv@^MyilPs4E5V37erP>1Rup6mY2e=@JjQDG_P} z6o`7+mncfJpEMP6;3%d^Kq*oZ*DSLHSCg`e2!0St!T;h@Gu&9Enp+WI*(s+S=jW{gN^7m-$}nr zzxNl;x&WvRP{+{w$Km%q``f$i%8M4;-(9m2Mn0kh&{B5oDu;z%NaG=swtqJ`M>ilc z{u-Rt5pjvwK}O?^3+N5rBLx;I?~1yRoJM1!vvTR%P>a?EFH*94Z<2+ zKtrVqZMG!n1sR;uj2F;Qq4Wg3Q?S)2n=-Nx0Jdf~MP(_kXtl$TS?6#wQ3cuf+$e%B z&0$IriD@d!ShV3IvIg(Fp+R<5_6DCnV$!>I&zfdW&QKqLG$ z0G6l>U+!}gl*KhnY^-0JX*DSBIHevAQ6GC$jgrb5njZdCgbz|J+}1KBsn4Do9>nws z+%J|4UsFF4J2-r-9rG=Tb|AQm?0yshb*L{y`AXLpA%!Sp*Q7y+S}eFHW?az-W6+#y z3XEDk`hjy`IQYO6^;yz*olb&rZ$Go;Ja^N&Tr%58yZ62)*~1mKdD}JuKIHys_LwyR zP=QbX?t1Y#`!E)jt(6Dt@7}rEF1lbjO#x)dJ9}O5D-j?p)?l077H159v$Au82ELRjAMf;C&1cJ{cY*Q{_w048|-dF zjX9kFrO92&8iJ8P?J{shl|ll*gG`D@qFkr2JU+s7cBRIlGJ^ZYy&nl{255-Ha5E_* z&~%3sel=wf>OKv_Sts-Zy&@$w9QdO66mO_`P4&T zP=M>;x=CS2CQE`Q)L|Vb_(^$Ma#^w%m9AnY0W%s5uMmAdRLYzNV}L9%3tCb=tpuyc?mh)97*92SDLeD)IEV^H(?9r#|vt zn?K%zPwzTgk8egKo2(sD@)J)VwvFpcn9oeMKufDN!KPQM-$%eBo`B2I6-CnC4`Fm- z2>M7v*eP!aIG)c&mU&IIFNN%0bcx%fjM6z`YgXql&eo0kM%dwrVY+G>eUf0o zvNgphd!YQm$vTcEpH6;dcY|!?V0gmHVeyMXp&%>&+BUKXeI`jZg~=}E$R?*UQB*Vw zJde%YN2;iydNoHR=OpSx2d8rfjSgjg3cfx7_f$%=GGkQlI$7^ z)9N}OsOU!jF_w~B(?>z{0ufYztEm~~0;WJztw?b?fqkqdodjTqc~7q$4w1ou7tUpf zlS6z2u>&bh$0Vk~OYJm&{!)iUtEvzj17gGeie%b|1OZAFdi)eTlv6=LHdYX#pfnk*}TVNg<967!Jh zuAAFIr)w^h11YMcLtTkM#U(j!iR1Z&8mnR5w>XEjU~t%0EYIX!u=zQ?M`&b7yRW9c z$CaKXoZwR4=%s5oItsuHTVK=_nHZ_tw~y{#G-FEhIDu0!*=Zvv4O7PIbX6D&ksXSN zt6-WyIWBA|M7Anl4tZ3l+pDZYxMM3#kT6|ib?Y>dAg#9ox|H!uv(!SWRgf*F#1iXY z<8J$zN?Ccv8v8ZDFE=mG%l!U?JG8K{$QCYHYPa8YFAXZpp9plKR%+H-hVj{8Zwg8> zhsuOAI0V+GQMdv2llEjT%H7?{7&!=q&naLQaBW)ILn5;G(%_Ovg0Mk}Ur1X}qiL!H zUOd`mIY2bbl%NVk(*)Qh11r^W(nwjT$|10WtSycxmJH&@&%Ru5jrBo0TobmdFJHk1GIZcttf3X}TD&|quFZeFQ2;K-KN<#CYrpnb z13+;S${v02q7tyGgh}tu<%EWfuf|896~Zb33}XGt)g^S+`q^+Hh^c_YbZh(RkY#gf zr!x>A16Cq`j&ZQR4oJx=f#V@MH+78IvvitMm;#97jcL=VMC#~5$P`RQ1nU->A7?C1 zC%8kHraYE{a|`9K6i5Uvs{1!a&8*;C&y64(OO;z9H^*{nOUXF}Zdyfs04!%5E@LvU zgRwU6^#GaQiq$LaR?6OjLN$hTN_&q#Wd$@rk_yG-H+SF90W{)Nqm$H>Ld|)U>`qIn zEKf};t#61XiRd+gFZ{qM!UiXxxafJZHE0WjIW&>VB4bl`cbpFFJm@NFj8r2-CVuF$ zQa|&;adwbBt-z^{ZXEk55$}*lGP~aHUK$!Qf2SeFVXQ-4q-Q^+cQjz z!v55mjwkSigY9&SYZ#ZyvV_cH1R>C|#Fb9O?d~vK)6o$tg{)v!8%Z)od5ad>{G=RcfdSVQ>mm3xRYO=N_?1F*9)x8t(a||>GfQ=u;6u<$hO{b^ z>OSP-V<-oF{xqwrY;oCLJ)8L`0VoL#wh~Oqg|A3OM6WTxD3fibe5a@rA+o_RA4>R=tSjRP;?A})-EXa^1uKB&5e!AwsDl)Jkj!t< zvSoJLJ@?}SQS1djx0C+7|EylS)^$)HeCRR48kwIT`7_e@b_v(=rz;w{WF++tj~ydB zj02JxBPb0*z2eN{z(bV=JXW+8QDg*j2*cHW5VufdXG!C8O5!Dq(RmV34X<5ZFTq`? zQShVtA#BIsDXjV82&xH{vZ*;4t|X2?4LaD=;pEZ^3o~f?QH>DVl7M`I$Y)8SD;cS_ zaC;L|B3Lo_usVgH@90_t>JF+SAL(s0T7wd6P`A1PTZnPQeRWXFX5r=CO$21KbJnC= zE)Brf4mBXD^{~U1^YTeFPT}LU3V_G6zGt6)%Kn8}-k*H$20QTTOSXP_j$OW`)M}WO zJxrIZhtpPOFqfZK>@bX%_cmK)6FwO8$L-=x3)r;iyr7=jC9z37U4?V(r9>zw(rJhq z3WMS4xtYLyQ3SsbnIDY>f@KqC)V(n8mn_P5qNcs@zU28j#Aar{!Vyfh+>!#P@GM=W zC?CXAMx74rE+<$ISOu8MzRD2g(u6Y~FD%KSQ^#Zo0dUZjOv7{uv1UYcRiB{}8GPX@ zQg(53zT9S-3!cYo%g|e%93e_7QCmpv+`kep)yC|jtWp_P#)zHpz`-drtzBelh%CscNw%$# zbYa!#P#G>LWG)=jZUhqI{TakrbOEVW;+ZWzQXT+bo@a%HSuhe>z@BOR(d8bdJnrub zK%Qp02@G)Vq`0pWR)tb@YDNkT4+3iv!Vh$(o5%!f>pNg5V9^NtA7H>bV&|TQ-T1MKCsme&1Z&zFZ*vkbQ0rS|mzm_>jy0rZ4p1p0hdP|4AgS_u#0CjQ2 z`q1qgp;nr&P05WjKz1nyw{~#91Tszyw{gz^j>nNnKw9N@tMe#aNzIk|_Ac9dR8mNV zb{m-AQZhg<`27WU)d@eUYkcnJtzf@>_Sp8P2^cU5C@poZ`U!q|NHJzU0ZAEOB;V88 zi_a$yX1>K9zN2Yz|vmX-4{sMH5iM>`_R)C@rGr~0S zQghkc1kx}JiD)F%#AOvBI+mdJ+$&9(x~E~<9t8ymp#}J;VfL`@A8}}NQI*+qG{C($ z4kj3HYnEi$uGbpuz6Tz%vo@T=to5nM1W$_qs7)rHgRg(>OZMf@d=grK&;I(I=iAmT z8)4)#<4QMf6o6TN38T7t0~6ssw>+ z$3|MQIMwpN?dC?tx-2d_0iG5#m45I=5v7F-mLKb|RV$Fd8tNygwXi91)}pDB%^Y#z zq9V+XQXy2csR&GS884HgTT^YD)w5BW@r6BxO=UC~U3!A35=fC#wn|M#x_nJA|Fu!I zksJ7X4W#O9=6yNJT}hloP*aeXI*bg=2l!L=jq%L=svAt@ZHp;yQt4O^&|P+2LBtfq z2diuCw7o}KEw`|M;CH{RShdOvekTO5H{!3_pPRRCg;mgO+n;^e1-V4BZa+kGHLaWl zCzKN5d2-YnQhM)5o47SdOwsQ>aEH$SSNb5~|BQ_Q?c}b+PCIMsi^LIb5r% z+kt>aZP|(_JxqH@S>t(>U&(*ES#e1QSX~A|ivY>*k|(Ql{Ce1U?X5j_7*_qd zH97nYB_~;&FDcPU*ozaDnuX{An>o5Z=Ih*>e=WT3(+0@;7(esx@o9;P)oob>eZJxAlB``Q?}F zm4lT|aVL$eP1jpyRLb1c4OTg`J@}k!hHTn&mStl~UWd`pLQX}U zFuN<@FVVq@z+*$62sQPc_WZ6I=iiXTCRw{I-SW}Yn;*}=WNZVFLxauy49$po=6ZX< zUiKbnvSm1UB`_+abIYHbZMi7%w$n}Rq(Pye+E`u-5G|Q2N&@h@Drhh=E9xRkli#bF z7AbVzq6m1Bsgl};WgpI7`scS|FnmEp@WWB)c2pwD3Y{ z#wqxTb5_PwfwI658&{NtEJ~?(PU!|39=i#Csae@jUGB3r>(+U}?@bDR$6q#z=E3GI zTkWYGJM0(@wG^(8CU}Oq-b1>E>}QQWiX5*IG?m3_e58@}sSXx7U0;(jk!Fe2_3xp{ zracfxHYnAYI1ao~u5%RQnaTLY2ttYGv|Gri+)Ml05LhrPB51}#PZ=gS%CDG?ic7fE zlu@BEN;Rb>iyANaIZ5dK)xcgDW>hrK+8BwfJW`7xJ0&p(aeQJjkP%Deu=F<_hPV;etYSe9c+kbTd*L{VQ0q( zLK;M0zI-90X+4mkA$xKP9&72h-N;j^qk615#DDvP_|4hnQ$wUFitCGH`nGbGF@ zF;vZ%5+k9xFmKFc$`sEpl1&w3#k|%Wx_)8Gc&S6@pyVsCya_1a6+Ux39q3!h{LW;0 z!NdHHhp9LB&qTO&o6bAWp4{;SO@bzZ8NwJ`KPklYlA+B3Bh(j^Aql&Urs_~e=mQgM zit2LxD7nLv(`s zf&W1a0Si?2)dbI1w_4Aa%iSp7_tRA_$WE|V_qDppWXY0j4*ntAwX@n$w-znQW$(gq zWJ*LBwJI5@5$)KC)#jpHC+C$znZC3*$@bTv9s~PglHauuJK3jpH`6p6#Ak#qHc7za zFQSx=VDLqkUH;}?gcDwU<^@1Di-O->civ&|zxo>M47Awgmz-r6UAoOyoV5j277RlqQn(DY{)_6XLq_~IGbaw~g3;L6wPT_I0|8S>kz%5;jPy#_$iqR7N z0D>o$U1ZIj(G1V?;CXrHDA}#YQ4XVo-?2 z;Sh=7OnCFBdq_-f4H=wZ~o9vo>Ii)Z7iD{Q%NGTr@UpC%mo-OpmI{+w8!R z4tLQdnjoFMZt%G##3^M_C>g70X!j}ylm)6;6Gc#3fW(oc%{ApQ1kq1QBI4sUu(370 zF~Lk`67#t&kg266%&#Qdqs*b?QGg4P;YsX3L|NS=;`PbHLvzBRu3jr<{!?or#gXUt z*Te0Kx1ZhmYrE)@OT6HBQf{;V|CeNBX4=|y>+Sygf6GWD>dowf6b>#m&lH%1Bya!# zHJV98K~zNg*|Va-3@I}sh)IcCT6}7rRDm69x`*3=gEtC!tq9ljQZq!hsS>Hp%}I9j zEY3;G!ge7ZM0=^pWYe*oL#aB1cTH@P9}!e3y2zh|y^8c(AN#Z$ z{7#4bip9`Zuz9USZY_`MLgjidhqFl3BUCx^rG6yDs&jL87Cs1gkL_b>jd zef3}e6=R-wyZpk{w&miBZTXrrtn%Oup1HG~G78?P@z#QH^b2t1V+4U1NA9O%uJz#Z6v9qylZqPI&!J!#E$NVQ_d zG8PcDwXfDN&ytoUmQf@xjYdR6q2Ib$227k%nhGNY)hSnncRW|#{y}-iOf)duo^tsBoMp{zhFlV6_D1fJrZNdZQD}_$T%AjgNax1TZ2J@u` ztuj!bc-snrkoI=tnb9d1lsF2w`wnT^Z$r6kl!{0KWkuF`3>!w(Km1({P{;776(tlaIRS8G50h zCUDsfOI1o?q6&=dg#R~v0Y6_r(pcQCaot2b$15ilvUc<7i zpLQoASL#uBq}Me5GfsLnR3eaG(?CcSa=eorn5P?9vY|qXMnyw7@y$54dWL%li_!2y zYW0T6Z!i`H6k?e+pZ48!%&sB#z2>);P%O6E8=6?!ZT;rf6)e4QprO=YURLQTRVN(r z5gSm7$s8*xFAPmeG6-#;VN2Yz`S;)(4B_8Z$Z1={|0DhDv5UFz479eSZ62rYVSVHT z8)FJWgRy@w3Iuf2iqVL8P89RSC;u`>kuLnzjNBA-dkP7n?4lSedaT!(;qJ6PxP#qF zt7*?!gzGp3Z#K^%Up7|o97{{~yOdp&;cN)rVqxuL-rliNeA5KCB*?Zwh2uyzzAgb? z3G--%chx+ujUMJ%&^6$Ftg(hrefLC=p^%-!vt#(5Et|5?xn*Jt>Y*cO}#UuBmBrJ!b zI7OBGGnM}bkRo;v2gRH+)p>2!u)3Zc)_99GCfXDu-bxI8#$k#Cdk^;Ru2-^AzKM+D z6BFXGQ@||du2Dq?|DYF#5-@qbFYN_$tMNqk`~&3{VVX%zRUY8cS0qiNM`rgIE9O}yOLHS?KcudzEjR4xmWPDLlP#qg7w9)7s6 z+jO5FcATU8+E33!1ygqXy3c>xeLB4MUtD3BX>nOeU%z9Up=cl09^U_q|H7gQSv-(m*gvyZY%45B$&oI|-*8hS+IOgz-)R7jx3m$Ysz{2Yk`4$3gC*I15!yd|l8?IZ-) zU4LX(gMN!5t|Qd$a$78G4*O$%T_y-Ba!QmTd*#~y6?ZIsu9&&N2~t2riRT|1A#jBd z)N!&=6?ZwXX_R>Pg-EHK`S}CwEx7?xe9Z`o=W89i+TbxUqSzQY7Gi%n3Sv~gp=UeN zVT!WWp0n{pWfIH& z;%ZIP(>X&wX8Vbh`2p0Qo)@znn!jG|ur!KssWPiyM=`2vtDFZMNan=Z0@WfV z!PPbt<7LaM00&`{GY#C_z6KR+ZFDc+wU02A#F1|#3R^dKO?rzs98P^cd9v-dGi@&l z37Lw(c&z9r8tswWcWhnd+0e(}jBJlWCE5^aZ_VMGNJO65pq5NojEPM!43gOvY7-Xa zRG&m-|1grR18k;wm%)LJbRm?c92|92qg>PBGl#(uwQ-f`N%|>!PM4P6W{iE7loNdv zKEQzxqm2TitT*FLroWg$Fa^O*`z!T;vawQjFOZBU$O>rnh zCi(~eAS?TG21C$H*2M`kEKvjx7ffsBBY}e_5O%IY{O4@q&uc$<1$xq(1nMakqjYKE z{JGQJ(4PU?smx!TJMqOSr6F!@?gdbY*UI9y3et#R@{;7moV?ib2|d@v4EYdWa^-xv zYCu~M3r{YIXdv3NPK7zLfRXj6)gC4KUZbawDq(bi%>wDpw~ztkOL^YN_3p2<_ySBO zeTQ_T*C!!Uj<06o%oQ+Y{Nm0|yp!6#7SFXmawyO!Bo`;HF((kQ(WdP_K+Z3xE(O4a zR6%%7D0l=#_h<)>^*X|Rh*wm*zL6V_a)!>v3X)!T`gllDZ09+T3fjOSU|i z^R~|43@ER#6DYgG&M-~GRfpLgUAaUSK3k2#+#b60D;H|8_*?b{G~B6*kmWI|d0^Pc zq%Ntr8lpCNV2kW3L@-+f_ZRE6%%Zrd9PVD=vM2B9d(lxzGVK-pwNs`UCq{%gYY-CP zShGDio_X@DrZP}lx=F@&7VN|o*-J;u0iImm%m9c zv1I>1v}g<2yvpSuh>RtiU>|UxgCse!x`w`B0rcvrpdIubM#eB?kk>MG!OKMdQj4hD zf+!}Z${~ww;1D67ASebCBC`s_ND(Fo1S46b2%tOPMcFq$Wn@Z*z{60*ryD3$#QDM3 z)a~#VDes8GDP~dCUQTWnlPi5T+G(dM{)x9VcnA|pwr)v^-S*h1kfxV|yj-)I=XaGM zfM6gA?F|RknP{gJ`)wN=&i_r#ya(Ap6!melGO_5z7g3)2yHVQ!WA8;H*C(06JHT@> zPN;cQdCdUniaJ^f;UUn!GyB!GseNz--44t{xQ`4q3+B%`MI5L$p^pqL&zyD|3ndy= zn2@o(tMibte|0*xj<+HhcHBJsP2%q<@W6FDS;#S8i1L3S-Y&<)YAzyu@-k&yU6ddWOFmx^gHIr6=qm zgxvZmlGn$>Q+1nUZQ?Lngw3ra_qC4JBycn%UcBZtfse4)Dg{9PH2o3wCo0yO{-3Er zGFvdW?4|%n!lb9tq3;lVChvn>u?n{@F8|!9?a^0$&G) zQrb!o-`rWXF9`QLc=wjTR$B7PrxLy?L&+Gp2Fkz;K&$vzFd?XVRs0~4&aVu;?T8EfNM0#ni=vt(W{X`?1{Q z*DNrC^gB~wA$(X!<5`Kk|8(s@3b_qs`=^$<0hX+Pey;ZZuvc)aC^l0i8~KSwxiF&i zd$bw$r|Q`FIITX-$X5G@LvzFe2OU%_1u9DV6cg6K>??ySQu)e4sd=l}Hr53_F2l9m z@F+R7AVF^h&7S0Fd4FC)i7E^KHHj_$Y z4E2qte1@cpvG~KnXrKbDGoxz z4MuJ+QCoFo*^RUk&4<3qUdZr*Dtl8QpL5_($Cls}LWI7FurzVA=4E1*Q88_0#|3Q0 zsw-L&#YNEeLSC^5H*Yqb%|9aOz#?RV?P11ih>(uaPW67!h~MabH*=g00^5Aojl1-G zf@vlgdB7pnu+%B`-Ijhq|I|;{5gnAJay7CU6c_Eig^;MkS!NMgp7j5siEtNDxy5fR z``V6?-vm@77j$(^_P#!T9YN)bt#T}zYfGV^SPca`{~#i&F*f?Ct@Oryy!4ut5EbL8 za@+1|Ei#(a99^c6nwECX92QG(RR1Hn#(6hJv%#vCvJ4wA@`D-Cnk`sWbL0<4D&~s5 zfdexCP2?ZU^tIQ`8L=}S@%!SmnuNpTlT??3;h)K{9>6zev{Fm7b?tgqQBG-MmyBFc zHof3`C{L6`1TJ;PJ-NIkg*H@caAu;B-=4>~-twi17mqwc_FM2VG z!I7ZiuzK)U5%>d`7wYtLG(>HwkF91K%wBu6J=(?yl`;ACkhB65J$ftnd$JSA(_wr=*4eTqJJfrYuKAz5sPm_r!)j zZSc1U(4Ik{jqrGYR(paqMdVX#yPH@Q(obbZL*2cRldSz*q$KMZa`KeNGS8?i%XF4E zl%<^WGDF~C7h&4=@TzM9T+HgJg+Ed7Kt^~^;C0K$+r(YU*wt^_(kA$oiW6B*BXXZ2 zC+jC|&oUY_XNuTAN^uX%ZQ*<5WgFCj_g+~c?C85k(7EoSx9q-F1HnN^n+*;vl;ZKl zd%@b|_cgqP8w2=D%Ts?IHk^-t=w%T@*9}w-OC+Dmih?)A=h$SvmYPLbm6)sCvA&|< zVwy7X-vSu!S)m3$(||gI)YwEL!$IWS6Eaws*?Tk`Oc$OxBSRSm-e<3_@p?3t?-vnV z|IikwiTWUTDsSrm+KeXT<819Q!q_A@(GisOka>fUD-!vCCstqWsvvU)Q(RwxkK+Mi z^u#1=HOH>0; z!IZ{PF^?jD3`t%5-TlWnfaVRBJ>@gijXC#E$n_@ha~d;UPsxXT5R3CSaD@77Ml@AfiyQ65`aPzlj{f_Wq9#I>=m)`R(ogw} zZe68sjPrK`tzkO0R|XNiGy)hR1asV2cOAdwmU}O#Rg{H~0&+SR`1h*QkvTV7+YGJy zy6=e?H?sRY%H04L=LqW~Y_lLzTt<5O8RK8D{5b?-bkH!NkHAR34=*V2l7JR_TrlvN z?OXroJJG3n&84HKyyF8er@F=a8Os%pnfEWczR_<7Ci(-A{5xFf)gvDg(ptZ@}5 z49!QcPFgpE1bpnKM`~knO4kw`yEA1_a+<9Ui#<%_nhp%!cE|a1eL2y_G$q2Z3U$xh z1@o{=Vl_g@;A@+aoKP}>@Iy(u_cYk~vg@c_97xpZOsUW5->Lm_1$M2=2igO6F*k+KIYcHWbaHy?LD;d5}|6$DZ z!!25{hOV0*vl4NSZn`2k@}v2SK0}R{b!u`@Jz)@^vLn+t{#!1ND0KkL-;&PY2<3U$ zAuEt;+q?R6n&VoKHsZONF(`Fl>ppwa0ICd?WO{;5Bkj~OV}Us&CUa_-Uv*Q_n@H4^ z1^;cL!L0?7#0F|g={Q|QIW|;-3zsDNdnM1+!@z-wiarexmOi5ax?9>QCfbc*yYQ?XnpGJzW(u~-=RgjW;w)f+DmF-=A*aeMf zkrQy1cK}~6+@DuuHcC?#e#T&2fR0xka`zr-prlOYm_M)X>WoQa?TYRBi?Ipx? z+_D~%OE~i^|67GytNM_0JvrMqHUb1t9Y3BC8Pbj}Hmln23YrFnBJMCW`f3iNNS7YH zu=DLE3dy|@q${B{dK~Cfl$% zkQ-%LPRZ99oFUa@s#SHS(P%p%LThi%Fqk>`EgqbEXrhj#m^5W`+@l$(W< zfIJ3-2|}9i-w$no1d>7pDX&FeP+LS=FGOjUo+VP1aUWy|C{b|tgwv^%RFMTIwwH>O zg?mdgHXFIoRP$BKyV}gk#=~o5F?#x#1!Orb2r`L&N;DmB4BdN~cIJ!k%7E`;=`W7` z1Fr1z8{=KBgLlp@fQmf>)#ITk%7^`aw%2V6x^9o6aCSVDT<%*F0$Oh3;y`$9@=YF@ z_T(J$8hlk{gy8#tMhqZDcLj?oAF9+9fUXtX&G9ZDQE(!R=Ve0CKj$aeDb=rA zZB*VsEVt-RI6PvF92-;az5!$klJKm*HGS#vxcaoH3T{7S2z3XT*1+iV`dfbv;f{e7 zo+4PvY*!m{8iGY*PyEYjXv0`lDy?DbJyw_n+eOh35$52V*!PjK+hJGFqV&NaX{s(h zQszlkI4iQ4z@hdaTBPFD&>)gtkY$gM4=$UkoFrA&3VJuV)w*uJ-hN@@YB_-HOF;)J zJ~`?%Jt0tkf82y7UVR$a;TS&Cefutcj2rmq1o1EbYVePA z2W`b`V)UZtF))QNk6dLwe$5x-uBnRCJfrAuAFXN zFM2&B|A4UVb0Jq;SK2KeO_>+fy*!4p$-4BffLVw?xU8gXFku}cim}xY zcA&wFa;|p!#1q>f+t`{e26g_{RhM-Z1h{Y9!1c-#$tpf2JM%!01MLZ`5`rD6bCaSd zq}oNR`oETyh)4qq#b2|K5tqTcA7*aJDfFr?WD6?@SoA4>w$x{oy@gS zd#tC%8?Fkz_ka=J5yzB|G-Xo^L~|aAGLvwQNN(Sb|JL4v7Y~5lnO&M(V#aCNsf)4a z52xeQf0&AR9&-PTL&MYY_fS~5F7hx5BEf5DFqtb2* z)CNICJrdbpG%5L>v$>y{Q!~9Un&&)$F;j9Z69s&*VCdOwouNXTb|=LQc#VELUS+kRZxjVb%l4Z(`fG>&vUG)U);HE3y7dH3W3yBE*gH27XJ}NRH3wYI5_!I znm6Am_>Z9MfnYTXZrLK#n4BEL0@rSIrxY4~Hu9iH&*)hf_~6vcX6N$xd6oS;TfH;RffnKi z`B-UgRH5%vgzTi?6PyIfI)1V`JDY30tFj2Q^vMVKz%)+8xM|+~Y^`If|+TPZtw;l-cfb5&G$GQ5{{m^KBXpAWd(}=7_ z4*C0O1PYHt>?#;0T{%S}NJ%$%9>M^!iIlN9IF?m0Q!qwo%*&!QvVxPtqg_qax;OAd zT-EQ79h{*8gx%T4nb`%2UhJWzo9Yu9Mm0|uCd0BDI<;)MJCcb0x_xBfqd(^{38jJ{ zB9?hQT3=1Xd1Dm*ewm)X`S`!1)mt>C7wf*9E9AIDdx0;}&Cl~mni>Lk^OCA?m@^}^ zDYA8+jV1?h*i$L-GmIM-Ey`aBR8jI16Gef_znLS;8fErR{zzB6ORN>4!HTf(KBRiJ z7Kv4o&}VDxM^~cplE6^8wEvy5KY3LR)=XUtUk*9dvB`?utGUxQq{M?v{pht~!JTtA z>#x(l1vEV+d%Pb%2UywOs0glWOf@41b^>%>G`HMN>~Y#3c<7~qdaN0W7OYhCT+hd( zNZ}vNQ_BzLXj=@G6Cnk;W(|F7^LS($MevBTTMd49n`d}Uo+K40wYX303c{==y%f1a zzNR3O@_2)_5(LM0Fp}eOho}eO^eI^;_0_tK^qH=GDF=eX{d&hf_lkQ+#ty`;WCfnbigHoL#yS7 zI@XGFer%0lB750}g5|g@am-j0=${Imr3OKC&n)x3;C8JvB#XMoT1~*aLYO7RB25bB z(5POb#D&K4RTEHmR-t74liwvbe1eV72Vhc}k9foww;(c4=l-Q9CkB@U>X`=LwcnW3lG@QvJ3R*u-FsxhQPBl;u_wfXwDWWG3Y@6+y8|!zVtQEcL|B1_E8_<5bk83l@+~6a6!Y6XPNp@ z%!YKjE>D3g*fK&TL|+W~Uv$4RVk8rc>9`+eH`WiGBZ~Bt-Ann{VD9wA{L0vkaCk%@ zL|kLGV;}r}sJaK-UkJ_gS)ZvoOlf&w+Zfi#cV~={QZfk7MHWKUx%p;kx}dR$g9(!H z;4A3;1-gW$Ld__J0?E(|EvbiZ7oQYPeXk$1Qyoc}5Asq2KOBQz;}T`U#}>$7^=Ttr zrCW519cSVabvLbFP(K#hpO-EoJupGLRTlx&Zz9Z?T*`_rsvWQgJPa2%%%-Is!^7^A z_8!bVVL)t^2ea!xxTt_s$TT2hgU?mxV%Gk%wIM51X9ST*BZYOg^@mh+2OKMz)&xcw zw1p3=A@to~qPRMu{s76{P6|7p=J)R{7Z>ue*1D(~(-5}sV#mNol8G*ee!!ZZ8G=3~ z?VZBgid_w;!%>Wvo8y2qZP= zs*N9S>u?P+>-y*FD}PI!z$Z)eDZ@`0{06P4O0(Ol8c~Nj_ zs>4A7OhA;x)pMShS_$7)STi!zTz6MsnP9)TxE)q$0j3d>ivI6$wCgH%d_*qUP$U}u zqK*m^wCTU!O|G=Q`iLt(hambnENfaeuR8HZc^^bNJ8hte-T809m!^(Vled*aI?EwCaA%%8c}$ z%fcy{2i0VuBnLvcrQXvMAL6I0q&3D>ip^AM5RYa_BGD~-HA1j8aLrOSH;S0}jW|IN~U47aZy4;>l|*HBZi7Fc~V19rDUZx|w0EBC=~5ZDUI zFF4op3}mp_l>Hgs=_e%u*-sghk5QUfRr^Wmeyu=dk>r>Sa+1;dkr)3K&;PxP{Z|l= z5)ndi!M|e1iWGpOqoYY{Qe)5}6;wX_>iD6AyTedM5;t9=aPaU-XBMy95x4(XYc&f3 z{Zv}5lD6Ge{WRg3S|Y_XcF5ddY)VLqP%XYDjSV6iC{~s-Q$w@OK>6_!Sfn#W+2{$} z2Ka>IorQ|*Mi~wD^BcIuE7A@WeG2akJUX>1#)tIvHk*g2Ee0O0{Vg&TsU-Ep_zf_U z3Ia)yWz?PBE!$PU3*PrtCjW5`9mPLCW-Q_m8aU6{GzT6+eEdqccKpK&~|PMt$(a^sPxl+BGlNH55H{4JT~O!6AwQ- zM_AO-qn_g%gY6b|Sp47MA_m@G;Qo0u%N6-XbQU(;935r62B)^9CRGX<>1ZLGT6j#& z_wT~fFmjzV@rc^dk--qC8+%rj2rJhVna#t}2;Y*y8)`6hjD)7zTr>PL`$ zYF_2?_q#obfQ;0XxXCRsbz&1MI-Azvc2%~x@gxgob4Wf1WUl2xekJpUry5tz;f4xP zpcq@{0?7r06p*~|H+M+|yC@WCXN;^dmz8j3jkp+?@^dB$VceSed~8xImQfMUpxUDG@LX}xKgLWzONo!&6T2E>NO7p_x}#HCXA+& z(Fh$2k)1-ugr?5zQQ5*|9t$E4)+i#As)m^R`*i3D<=<%53hxQB&2l-nTQP7qkls zQWwmkA8&>dI&GwHq7hPbt=%KN(JW@+^swYa*-m5$9z0aK*<}?q$${1Ll zIpR6;$waVIjQ8tMW0k_Dm z0X)pu<(YKw2z*g$C--@26+oIt2)Fv(Kbo1L1d6S{fhK#XlcxH0I7Q8PRai=xm+ ztvdk+P0T~4h@=x?NP3Mn9otXYKD8C?Yq`m9eug)TNH;_QR*=n7EBS7%+5B&pVhLR# zZiG7=3)4%=!c=Oiye%9*OTx!Ua*d-fBd^E`QDMEz<|@!nlrVwdxoGf&TRHE_{TLr} zWJer8+^REDB|HuziY5GU+vumF3#ADrIPYQZ*%lQ;;t|>_wBX%YhYrkN2ej57LJvHO z$E%k5fTe?@qk=d|+7HlD^^=7|KUaK9jTiY?7^-wiF%5`!jB?Ew>0b~zooNlh(!g7K zgO>|Gh|3_U+n0|}gB4W`-Z{XdEyGJo%-H=f&i;iDSt|M45>hI)32uJ zcraSqy5e#8jsNPDAsFO9)9m`#fB*;#-5ye7pP1C*IZB;M6Ew23CC{^HMW;upS_LJQ zaZ#7T=2CG3FU!nUqsU_peRX()PkaOwi|#9yY_F5_5GzFKr~DdR3n+ z>4BIr7Wi`wcB&WP@~H8YlE2Ge8=PPF5Q`AlIcB?3(;^n}v`*iJtbm$ED@O=5HeQ@* zBJkTy6{}2*QNADjR<*yTk=KM2RNyKVlCpzVKJ>=#rrkUyW_*Jc)mt^7qaS^}e+~5-jDxU0wlp68-Ivz|UM0B92(0pof%!m03<*}HnHx`T zOfF9wDC$lVE*LwjedzFq3;>4lf%S3P=>Cm;7E~Dzx>?Gqs-%O>>PGr5At8JWAK|~n zbV~6U$leov%?|Sc3r1NAa%_J4eeHK9YIr=?AYj&EU z5_Bjo_(kSHa8_1Lk&2~Yw6_PGYoB%4+`=<$1vDpR{)}rJ7x+GyBxFtml)~cSpR@H4 zda1z&7jpT;lOn*p;xy-F+snc%h7io0u3*oYrBbHQ;1NilF1?tuXW$I7aB;J#y0207 zcqN-kKjn?#ELKQ^;1KpK3f|3&)&w`6ORG~k_UHrUx4Z`&M=mGM?!(;z#GPQiP7rrn zpT9n#FO>Zf1Of`?tzj9#E$hUP}lkoj{|8bvE&rMCM`<$Y4p=in@z(4ZQ~(jhIYt`JgDN%}$+HRUMV;bmhqDK8&~;>_nUOY?IM zBMKq*!Hp6d4I;^-g-WQ%SQk*(l?ggDzzogud{=6*mJsj{-sc+n-_ADtBFl@lo24sY z37Uq6rvHU!_^RVt1UnG!%-1?i<;}~Qod@{z_e%p@Zd#%_!y&Z`C6#R)Wq4LiO-!Gb zLji~x{d;Q-E0iKP^ht-9+?J*d<>9Dg*qo|i#w{aXR=4twj@;Ga#Ev2N=bP35)3R2=9A0RR0()5Qwkb>kpe!ujBWR|1z4BnAXi`ze~m zD&NH|wTJ2*(xK_02jM2z9)6tp#HGS=l#_63N0lK38;F?Jz(|92a4of;mK-BNdA$&i za*12s2LP|~$zC99D2MGe9o}?#Y_w~V&FPq#rn4J0$#?jzfH}G)QX>9ujbtN9kPS*hQ}+uYB4o4OCCBV(~z44Ds^pk>B(Jqimh{;Vf;}DT${Og>c+!S;dJVx%rsZ*jmS9pAHAV$0Y7FsT6$g<$QVF%GO7I9(mRkVNdccWm_M^K9jcJ>8C zWAo}%Xb;M2LmV$_ZAIy?OECM;su^_)g*&kr`V32%>4OQ;j)&;N$pG9o^I zQ&KH*{N?*;Z|MtIicpzb>-J*v?REFLThFbNNs@Rf55zt=C@ZB<4IW`BL2QVjZ6=Nt zn~r8CWaVIAdR>^Rl?(y)oE^ZUq-o~k5qQ_iQ%RZ~5RfU;y^O0H)RPhlL2~~(pmfek zJ91&mP)(heFL{`zSSU#5gT;$iw-5zR6UO59ToSe$p?;vDqi@NaLH41yl|4U$9K~o> zM@ooASU26whoZk03xn}zP;{Q(bFta7W>;%w=J-6?Dtk8$7SpfHm)v0|rKHnBhi-of z>$F5t8)%M@vb%@GUTX{REcV+AA$DcY$wc{R?k$%9&0-al&YMB_{R6j+C+d8Ub!`$zSJ=1nWM;2dCaRK9l78#wnUJS08a?XSahddu1{XO`nZj*GphMonIueeOcBH=SA$5bJ)7BX4EIOD>y7_pH$~=bJjqGoC z{nZa`G%5}BQ0!UBzwhYDzt!9e=ma0Fj`i_(iSRqm7mTl0=pvVe$5z**4nQ)G=rm$K zE3~i7|G91Cba4~7hG=UPi!J1K{$sl@ z!GB8mbDMqwfX+VeFOV-KHT}0F?O!dc7x(#}%>wsF?qYUZx0Y(`EMYLgN87H-wpCRT zMbf?`d>j%Wpw6kN+ zqbVBM2g!MEXyfy`-8$Bcy>lVjDSGq!p-}Q=445IM&R(Oo))r z85~2bCX7;{8@qwSr7&|g;TXpy#R%k%%b@cvConD}7F@n3&}RPE9;-j790{Jmy1%AQ zF`dT2abBKAiCW&TU*j&sMNSR;_STY&SH{zf)NSakSo2y;$i0Du5| zeDoxBc1*XxTmlr-y625kOLY*6KB+dC$5`q z<@NynUV69NBf96={Fe-kG%fCNqG@ROuWV!*0@x5WU+UWzj5*gJjyC)dtq5PAO{?b^ zmioGe-?HCO>L_Ci5Rh#!N*Sj-q-TblAWjlL|5%iBu^&w$HVw7o@~xoFG}w>YM#c1hP~VV zCj#!Dh+GBG8Fq2gC|t1K=XjIkZm|n(`BdQ}0VP<6p(s`s?K=!2isp@-qsiFmqOrlI z1c;a_<_~!AF)j@Fy~aX=)o}`!PYZI;_J(H7+b=2kpd)%pUB)|2O5iw?R@>|REeL-PfsTqB~$xm>~f6{l*D^i@&Y+?omsem^r?SgLp z&NHG(SE|%2M;ser9T6%sYv~!|0F9Q~)-R{p;TN<^s-u9q?gKcc`}ctK{`c%Q@I@O) z3k}day@LxSH+FCPcZc=9!qsacm7qZ)OShg+)4=Z;L?)2}ckcCDFqjZM!OYu*RNyas z7VQc!3Y=uN0rC}hCjK7geBm9LOIy~v^YiF9`*J~!@iut(R^s{JgQhWFT|bD8trKaq zX#lX2w)^rHu%YDGjY?yaI;i%qIe>0&0^qfy$#yOATL#Rr@x*bTD&;9=0sOWXUK zcWv&#ZLeT#LSjFfoZ+$jAjIPh`0j1$-GAeIToZ^5_<~+N?}Hjb5BLr5OI;WK>;Y1; z0ylU6KonpH*-i31syX)F+Xl|&0vUSkgX{>|jd`PKtXT(YY5B+eS+Kbcdd0)*nxi5?AHTh0(kp?1gN)5 zkjYLfhThj9KJvp>3H+vdc!olI=o&W`PgZB48RC$?>y6Wexj=lj0r{N8iV^Sl4wwV!^v zx~e`^)vLSqUTb$nD9V3FfW?Ic000P5l48mL0Eo~(4I1(&Ljxc{Pyt|nDUiP(00=JNA8UUpfHVl+|DlyZsQya_6aWab0)YRQj`rXFpG)#@ z`v>#y9xM;!KNj;q|63bGC=cvE^gpl!GwG+l9gKsdmNNhVhxShc0sPFu1^__EtW-5! zH05M@OziC#j7;r~%@{oF9R5K8_&j+2l6Gb;M#LU=wm@ed4}Q{rY4H4||6wzd68}rZ z#fG0$Q%;ds)ZWRAn1g|dfr(TAmYA5B&&kxBM_Ekbzu`wH;(zQK8QZ(M z@RO4Me}kEMSp9#%{*n9}_OE&U+a2FO z#(1=p&7AFRUH=J7fRlyqUmpHn%>TsqZ@_=_)qqwm0{;#9U+n*l()^G7KW_df^M3;r zoUF|L-pGH&#r$7k{ZHQi(En$4Jc?ExX11DQR(57U=YQtH&Cbfl_{9{QPT=t0q!9v0i3zKEfSh;2B%3{HQ9k-UUA%SKH@d62K6FW@+G^nqt(fEEMI(r^ z#P^25V|o7-sz{J`rM=vs=BVHUXjvjS{Fm50DAl z_*r|!n0erul5K|-DKh30+5nC5-BWZ(!dpcfyh{h14Q22OGhb)_Y(%xgB@sS5@|$ic z{4Qc9yLdSlrKZ@V*CtF_|88wMyneR)(dpy&$Z_B=<6?0yaYg?;*v?qBO!4oNmx^{z z65NIQRtTYmuMS*ZRcq&$(GsWS#0m1^vQV5&ncq6>GKd?)FatSVz#G6l&It88XDzdC zS_$C#g})LfjdLuNkND-Iwv0I-;UNh!Fha82&~VU6>y=2V=OrH8a^oik=87^gNWgCi zhd&@|eKkv5Tt~b*bMCze#NRpQaPE^Cn@f4Il^M43e17xFV&SgtSSHi6hfXixP2yer zF~sTqKDu{ve&E!)^c#=if^(kqdY3HkTMrgdy~a5&1;qv$1&0BCnM?DeX~+Cgo$N!{ zzF1>uQL*BTnRvlG8fvm;`XdZ~A|8SoL;f5dd?+byLju@znfkiv0!+CE-okl|2hFKS ziLB}ww7?W6gqOKI1^;Qt3p6uAx~pWow>YlL+wRR`0noEqtT|g=={I|xaidhDc}a?} zaix3cci8zm^C+YGKbF-^ZT;E$)y+|9OXY(a=an=1&neLXsQ`n~W51{6CLQ?D;(+N- zx43b)>gl}>vGUVdlJS8Lc~oOb^`r!CU-(;6d748S^c(E~#nJ|6-4fG28N~V=6GVcG zVp=vaaS!VuT6K;)pMw{Ch<9g(&Ff$sNuP*?qt}Sdn@(|)#y@ANBYRJ3<}A5H`c#1&&BMZK+L~^n2eKVLDKlvzt=5aZPX)6_A3qLp@8%tU1NUNH=Yljf&mUoLP>uAet z*S2Z;v~>IPmQkS*w_fe!(MVJxzAY{;Bon|$qs#a*dYQNN0<0c?0K*>NB)V@iMy!a) zQYU-aJe+l`s!E@f*by4y8B<>|VO6BD#Fz~adPK6JYD||qwn8um3-QG~byHg#Z=}Yo zOB%O2aPlVqD^bc{fl~`|lRJH)8;N1dEHh1?xYku5)Ha_EI zux9$?NH(PYR`slc|1llG#SyoTtx6d^)AE_43ow%6D6!~q?fh<4oa=mlF*`C{#uJ)5 z`7B1DadiwxMrPDxc~wet&%jyDc%@RkZW4uXB}&qC3<1%}aohSEAND1F!EV8_p#^NLr1*&$mD(MfS`<(dqrP@}G0Q!>UU!sX5v)IgwHvDU= zqmj~$gY)$yx0>Me)%f$bh%_~YCbigP92xZJab8l5&q`*n(xI}!_9YQTX;C3O5StVz zJQk#6)FZgkgWbo1{2u7Aj6!}xdSnnnDRlc`Lg zp~k;pnl~Tq!3c-1fAiBvN)19)^2-^64G|f zBe~PD4n@x$aLY8YY}H;(IUA%HB^G|Oi&t$N+E(@5)MVqu5H8+9{Mll3vdFt0F~#7h z2$7hCl@punK|Hm*%af>AVR-qlMGBR|aa*}?uK8iANvmGAczi4TOMF=!kqf0Earq4S zqDy-CfaEvuh&qiH1(nFe#F!n~9l?Y^MIujw@4E3zn)@Ua>hu#v}x`OVOuf zp1YEw?Rw8*-3{r>CAX8oa?g&%N|^SM4qVM9U_Xu=FzvMRI29sbK!y}{k(m1-)U~+m zb3P3k0mVxC{jAljE1}e9OxePf%f7z`%cR##NSf@_vYiw_Hnox&LLq$MW^4eJeecjY zO04Sn3Nqi+P%pBsB~G*CjvxRF4Rspd+7|~Xf`kD;$qV(c&;1%90VF@9fX26S;T1Ya z3FD&7bsQ(n-*MeA3UC+n-SVV)^uqePfsv4vM?dvGA>#TUKl7F^P1P6ThlddF%U_)I z&Mhi7MBy@^y{UFNl7@{$Tn`F-do;4t!Cvqste>Ea5$&j{y+oAJp;jT!W(CJ^HtKC~ zxQPbne#f|JG93oHd=W4wOg6_`-uDqW+&c11Q)KItiqcZ&L|rRyOEEA zX9lmGg*r99rQ|+sb{`mT?0qvbeQ1JjPHp{ z+Z}3urEex?pavVLHmFa+&I7NkvwFov8a~IJ!a$!s-WYK?JI^W7rZ1=r0&TMmg4g{J z3Bmr)Ea(++{EHn3A*rzkftL>;WR6yV-_S>|P*HCeSB8snSH3c#A_ilkG2La+hEc|Y zd4=Y{(;`o~E=#x&c~8g(|E)wv0pzxG)9(^&L*|S^RKaHTq;x(M=V%X7TM1s9-F#z| zMuvIE{9f~keMr$1fWx7D39SoFQ@>#_U%OhKjPswH-D1rv6j;t#p+Qf@B@vZS3C%P=;=EPb#uOE{!^XP z>Hcz^0)zrcOcFg{J0O1+rbY_EgxMVgx&k^KdcSaq815=j4r-1HuNl%04b2v6PFNDr zo{nGFTS8Prvz)GmFOXk>ZNu~Yp4shS45 zeVAX$+1B%jV0A`J20O?T$)dI}b4wZ)AyI;a%vN8yubS-StJMYKY{`BS+;1#q3Z9uz z3ldMrXkQLcAky&av5spU1cxLnBt>^K)y#nB{Qz0y^)RCI0hM3>Shc?Ez2fg!V{L@) zctx2p57^>A%<-6cLrEL|MA?V_lAVBCTLqAH629RhR2B*c|y7KTI6dE@T zL4{B#XY`GRN5v}E;sDT>@=t%luVeG+H%X{r zJL+4ejYJy{v9eXIXcP$R^Ha4~cPe^CXJk9hvnm;`Iyi9 zY+>pSpm}wnMx`P8YskBFvAy&Yy`0V4GHKGqtWXSD-LQEve4nOWkGa&HS0#K|RIyyR zzLLKIdgCX`3{8){zwU}v{rj^m&2xo$l4IBW#ke@Ao^E)Qe1-UEUQGQ#({U;6Fc>0A z6D_G;r3uB5i|e<3juI0-0-4a>tkOnFnp$vOibt?F&bbWByVBUyWm!$~yqS9FxM7KY zH#j^avSvjb`Ime240xrR77_DO0xnR$hSrskd#`=5kQFuwYMt{4!I%9l2j-;wkRs82 z8oeR-xJMR@gfh1kaB6I5GA2~AR5}T*R%TQT@%V-rAek_$kv#XOCs@NRw%{CK9Fxv) zKx{=mL^h)l49D;g7{k4ux|BO+trYGK z{k2gS@Bze@kjS#IR=lBrJ`=HZZ7U`}fyrH0jwjmSqwwZGu8GTY{KA`%IN1z5BOKH* zlV^von9CIyN>%R9EgQX;>7^UThEt`;m@MEhbQyqA<2e;5sYZV6TbONlrJa1e!CzVw ze71p^l9uLR3uy61W|3g3C@C94O;hKMnPM|0J6`n)7!#lg_oj*Hat&>6Oj_iJ(`L*+ z%ELH(nDnEG%L>#OIEU-S_a}@CIo~a-IiCsMy>2xY2(`h18PJyW*IR;re|I)t1to&2 z3>>uRWAoNwy)>{EGnGViJZ3K9SH-{y88I9Kk8FJDXh2;)ci|tr^nOOJS$b9ryOD<# zipUr`M_Et9xBblSAOuTCcfFL9bSh*#r07iF995r-{%seJ=sss#%S)}|B3u)299Z$D zX>)94$Z~5S(4dggG**I1ag5ge<=LqFJ7L^>n@`mM(TP3ulL%+F>+ww#vDFULj+!%1 zUoj-I4_VEiY!z#ph%Di(W;?#;EEN9%25e2s5@O5@a?KgyP2Z*EIc^_&Ty;eQgINeK zE}4pZ1X=9)=P_KQdgSwYdf*S0=-dh#3G#c~MEf)qqmr3?^=K&DU2F`N#C#&>nK+^} z?Yw!{&$w)x}ok>-$`AJqt^(^?!*aqWS%yhrYq?dHr0ol!Z{&&4a|iS zcAhDxrIBBW$TXj~P)6vOyM8+IF^!an?t))trDY7IF;G=TAZFlrtbh6WB!w@r zB;?P-6t9Q3T@VLHla=QlvP=vnDoQGXByi?4>EgGpL_ACs9M<#TN?|lDd&93i_g{J7 zc=_K`E?5&WWU>n0Bhht*xG5$if_D4P<@;}OEIAJQMLuuag~kg5fmeM*hE3ZyN*u3(~0 z_RqnFFG~~;640hRdh(zB2K@XOK36Uqyw7LD&0ot-zhB-Y<*T3HpXG7^`BSJatFlB9 zkoaG_3#L77eu_%7!n;4*Xt$eT#l_E{UbQgk4^1}^(j&5Kkx{%CQO;iNc6!C-E3XIP z>UYbI<`gY@%`sZ{r=rjnf>wlT=Y*ryz{DithYyM}l2|+1QZbPP8YD`N_|ap$247E^ z9lTKbWaZ1$d6Pw0UiPN9ka=h=9T~uU9|CDR(x~&D(E6gK<<))mpg3BPHIPX-wN)#9 z6TJ7AUP2B+Wt|e03c&8fkj8%6>Zw`6z!Dlp(RObkf&db%(rdoKP*OvW zBdQ7#jn0C7Ul3C~xm!eJ6^`8pW!<`H##q!aHR&-_lcL)g z6jS5Vg`Jnap?uT?EhXylR@|+g3 z^#S%hL?GUPQ_*nalHv#l?kT%&nfo9t$G&7iWWV{j5ydJtl;~R=CWsjEc_X8-#nXw$ zg6AzfPo>xISer?ar+kl=rqmAyl1=HMy)OTs@@cX9WdWU9tv!b|TOZY6e#>76O&LL0 z79Z9)&UHS;lj(&KXlQ;~2(yPqQet3kp+0=mauD%ZZ5=UU7)aU!8-3&#g_CM7UTbn` z!?qD288ztWA7>?TC{{3ieWrvsg7PFfsdXb@++GWd0{lX$NeI9gu>*$; zz(FCP!ni*R6+V7@+-I-@8`>=sILD0359p}K* z5bvbtTW(>Ag{5oiH|EBcFm827F`WLUK1|3csTE;z4~wuG_Fcy66^p3Sn*8t};?~AU zRKG}+5;N&X1t>3ykKUL&&Pd3t5Zu_T(H*KQ8n&n?T*Zy2oZi8kenX3yJ~bu|f)83O{3Z zD1>&tWR3fGldE*fig_4%uN3#kNJV*=*(LeFKVC2mG9^hu3>1PDcBs9*Wf7WJ4Uu8XntuaWQ2Iw_@WbQE^KMn z1+ZsEGQ8M9$+O>rMrX0)$(4j0hCs_DV-z+HXYzr@>+4)=(;NJZ@kF)f6)dZ|#C-08bDUz6xP1jXY!HH*_X9ah ze6IN3oct+CkD;BNOPiLN>(?s!^h-LxujF7jTYlKyGplLawvX}`86qOj!|rH`2}l>c zXC;r@>I0h~j$zXHH*M@&_rUgw!od{M!X2^bu@uN`L7c;PC7io@kPu=4!!R%%gX6&| z$F#!II&`q2bZ@x?f%3d!aB>JxD4vPks;VEaZZz@>C-J6Un%@RORsR@lVflYtv*>A! z#~?&0Is|G@m>9Aq!fKo1YUpyI)VMCfF>z4AnqfxYiow&cf-y1GoQS(hmSV?zm?fBkONz}gr!2kE5c2CBCTBkP2ZD>O}i=))m+JG zyKXj6AhtX_Noken!eF0=Fi~sC6Slut7L7?N6_MWdX^tl~O;`^>$L=u2zl3Txr#dz$ zs~^3pyP+i)$*bq@LS!gODUVPECX&2KFPrDZHo)>@aEoVrzx7mZ4U;a$5c(e4 z=Om>fe0G5~r(d($31^3i=G z>$m#(`?-P8k?YhETLdIkh9X;7j%n_Od58~+3nMW?1C811Hd1arW+j%jCe3A$v&(_k z&uS4XA5M)A4|}U?#*@=${I{Cn>71$RB0`mm*FMAUews0WB4f z8*YIlKiRNA;lVRiwNCOtt$UOvp=h)f7U8Pr`&LB|D1^=Xc7yX}7~%Q0_3Ig?vLa^= zZ-nmYNOB^%M}#q|l7IGr$MB0^%R_wt+r1c6-=n*7b}? z)zqn~zQp@L4wn ztJi}|@22)M}9 z28i2Uips%+Kp|rlK|QOpRDPZZy8fBg2%KIVXC#D>Q^VL4Qtu$YpMgeNc9fBiV86O$IUXsOfwFG8+P*Ou#!k95g`>OjBm`2~ zX`ep1aCl5?2CikGqcw4MK0*q>yaV0b6Z?3SJoW-ea~uyaT>~p-fRkqt;1@}01*4km zw=^T-z)f-OXlCzdqR{#J(34qn_s@j(==}5Svhl*cf17t72P7I`hUjp=2{(|ZA>eMii*FJ;gvrVvni zfdmarZqOqo6e7E;M~@^-xcm1;3R%$WLPX4>F?kG48mTAtgj>IC*1ri!5Q_RljtEv(TA$#-CSHvA}38y+eJ8+43`KMFs`+_wf~ z8e)0`pE%oIqAsOhBE|IvS1v@uSywQs?$dc~-iQ`ZOI%L)FQC7!y?OK+D6LRDW>F+1 zd}6y>-X4>VHW2r=*A~@$NzaE_s>1dyhHP~~f}q1;6j`sjO;E5G8GtKfZ67{Am|{q< z1DTVD29ceQF;)b!GT`8NBe5o$x0+MRjj<1td3XO2adYY0&7p4Kl+kOpoliJYFZ2#QIS7zBlY5) zYDc?*vo}2&$$;Y|

5y%D}hGHHxmn$I1kM@tpXY!G40$xOqj&2fpKV1aEXL9hj?o z|9w}&AVb#rDdT6(g30CXM)Iv~GgScdx@MgS-TVmpXBr1n*{@?rK08cvHRc%|=ptHR zBU80`Ph~XXjuS659=G3R9x3J#={XZ(ZGCd=g7($>!r`_!Jwx1SN z$2;N-{Z~0B_9xF&D3k-w`5w|`@jnMT6}W-ddTE#fa3BfeUk^~1YP>PHQNSc(a(J&& z-{=?C^k@fx^ZAqddb$KOPM|F)sksSJaFC;%BGysUC~P-gcM2&QwJxh+J*^k!HpTCY zWv&oX0JaFP`R3P%dHoCR!!N!q167&3P^o|(oZJXV-&{U^Ja4Q+V;WW28PW0$z-SW! zCOCZ0C}@)_a&k3Wd1J8^Wk?_4(t1Zh30LnFm+;~_2{>%{M=jMh8Gtx5<0x;%wqA!y zHZpz4p%wIe_*HR3_R#NNZO^5y#w-wBxvar^3tAQt&(mKk)cKrQSay z@)h_t6ZfaOjm{7PqZk9s5smXfV1J*^4G(=%ZlP3_xe8=KpP*)VW{=%tG(cEPh4L2_ z0L3t>f0TcbE*o(*+&7E!7z6LD##W^1Zo)rsplfynd2_oGoclLW8JtD#kb`cXeWR?<|Bl4+mS&r3v){i0$lLn+Kwy zfdo^wq**BhnminjC8Scu@F>B&L7 zv$TB+?*(?@&$GX7~#u-2?^GCrGqV?KS+hFG7z_z;`pKA*JgEb6NkE z#Ofcuiv*&b_}DUWdGVKzD+fOk<+HLNZU9AK6J<}2EUIA`=Lp1DnDX+d9Yc~QuEfib z(e#k}qr@|~qOKr)z4jsZ*BWj8B;+gIZ+ouUfUB-Ipl_l2Yw@V%wL4Y`X^wvTZ)3P| z8AS_$Zsb6XA?8hI?udgh=@fy5*b+3-Y`fZVyAn%Md7jK%zOFn=OlK3;+>b)oGg*1& zpuwTBQ^f2x2gnh9xYi(&$Jb3J9ObfP+M60RilE8K5bL;~zCns~`s%!{Gi$_Y5WTdS zyaA7^q|M5Jl~e3^J{(t(>*N!wnQ2+cUz+=4%4#zJ$|9rK?<_tM+%DFj{83KSMvXp?5KeY_ol~;m)`3k z&ZDF0=~&u82VvIvNNkX29mWjJC%naNHl9ip|I_L;64|+Z))EUT!Q_3)W>jQ*l^w2kspXjyotF1QVW3)5&f|@B$0Q zO?;YscE+w-?Z}b*FjWe40y?!}LjtWvE~>zIKwK1A`1^NNsGU}Mlz zC?icrgq7t%*?F*Q5gYy1fGi`6=a1%Itf3>TJdxsJHH)-q(M>NL)W<}X%zT=b?m>~1 zxcn+QM^8gd!+M)!rnK5*HKNEO&L?S9Os3GT0HlZ#;V65iV8$>IpiL=gAjAeAjqpk9 zCxqdbA7tmn@E1aIS=U~|Vpr+k^P}j1aoZeO3XxpFgvhK`mlN`P##%%8_Ta8eGLYXB zh1mo%5^6sK;dUWQ?e%nXEt`ih;cVV8N>Q|^*x08P_}!tkd!b?XdDd11f?u%a$L@6U z9JN60NhgMew>_bFzP0MPm1 z@*CK9dT@{OPB$=BcG~L7^O+5o^ei_x)T|&kPWKV4>uB!god_$c`sh+sszih~ehPSC zK4gfNShyDt)NlJsqui)TjHUasQF1I|h1}8Q`~}^TGAj?Oa|;qT#Skx8bMKj@`M^#R z_IG$5{;b8&p2K{-p=hPu_nJ|&8u}qtiBaVf>7y21f)w-i0L&+{_}bTlI8=h&xW82~ z4z%KiJMVJ~7DpDSGWOhp-&LINoVbNAoMPRV-5pu4W>=G7CME)1ET6EWq-K?cDP}Ty z22SIL?FJu$A{AY%92v&2lj3k>5p&aQ)oFFn-}rhSOk7tqMY`JnZ&>Ae5d#2SIg}EM z4j*XBJ}+-@x@x4~>?7#|sM0qA+t3$^5?;`~uc^r4%X*+}nUn6tB2&|wP07Jm&7#>6 zUZtDB8zfzMcfpGqC__HG-JvwlVRg~iDEHQiF~Z_^a;p>G>OpzAR<)vlGOAGgeal$3^m9E zZ`g0t;8Mxre-{)Q>nX31vmOXH3=#Fn?u~2jwx?8iUbuUbfF`BMPx~Fl-rdPO{qYs% z>M(-b30PX@3~Zx{2MhTjcapRwR6_nL34|1=6bjrhE=htz(bkkqEHfDPpb$S5C(x0g7rScH`{x=#G~frO)0A*b}4 zV+B+!WYFA+yuGViD9pX}ADi@FV5Y)Ty$64jm-p&W!`@G%^ zaRvi5SmH0~gaDA}Sot3nXSZR;WAo$h+d&G@-CKijNA_U!!=M*kq^amk^nLiX>66Uq zL;&gvm-H!Ws!JhJJSVJ0GgK%5N4OqM z+H|Tm2Bd;nRKnvR`cA{5QW7Z0ujMGK9;cTDKxc)vH!_MxyF2dQH$)aJFjSo9H&8Ik z^UK%Qxbwntbuj`;kc&P?Y+0olt+XDDreffXW`4mY4=t+4;uC{x(-fh)P#ST9{CN$a z^+jtdtV&PM*$(`1)`W=YO$VJk6i}x~))$xOFf#7NTm(2S^d6p>Bf1~0)JRDQ;<;{D z7eJYVyFRQHYRF+&`uSw?^B#HI++P9s<(dQ#7JO@M&ieTtM-tcX>Lx%Tor1}Bs2D?% z9uXgAM2z_7JpMZ*+NLk{=!H+JE%H?~Da?th#N%6b)}&f*^09KyRZ;UM$M~y0gs!Gb z)=H)P)UOdkM+Zc=L^x-oQ+7PthE{5H7|~Axhjf@3x4g+T{*AD6^kiE-0XqIX;yor9 z+E0cyH6C4^AQaE+(9TCqfzw)>c0QcD9rRqe1krL?qPV12eseeUZ7t*uF(P)*n0xoR9Nm1cN$%ZlRs_BXLS{LKhziQ>uN&qlP749y6>=Pzsoy$(o0 zXHMm2m_<25s1TLDxxO8?=7kxsX z3-|!83o3~D5Vf};tI78mv+U0$3GSP;icI61zf7$kaiP}CjNVb^O&X)O^WyM~ap340 zPC$Aq(n&;|v%u5{C6|>!LxIXC|3RP=eoe3M-QdGDi=w2e%wh7*GS86&4`z?=`Oy-^ zi^<2$6t0GGUhEKHhb+aeW$Pp=gQ--Ax10l!2~LRDQ)pT*{^!pq&2VcZSI;ILN}XXT zlsxdaFhB+F1e)^UVSAtA+g|SzdrXpkwcs`iM|aK7;DEB%x23MvdAV~5U@@Bhn_dI7 z4?k$V%1v&~I-URXql|NGJ5WctKLMaq$)Vp`%V2tY*0b(q$BJ}?57mmmFD)xJ`?;%4 zk3j4;)8}eqF=$#ii=4xA{3j2btQ?toG%a5_xHFvSu9LUC@m?ZHC);6;e*DXn<`OBhfznEgX>rAP{~asy@GhZE8^^R zV3kP&2GDP2g6)HEP$J=wbK*}eT`QoOw_#w#gk{)wUTV3m)oJ!4DX0Y4$LH*|6VW>Y zkt@8Q(%xp8_8B5h84iD#N7wmmuUz%oK-Rh5Ag6{YZfs81o`15qhU_LK6-EDa(X^aT z(r7-u6Y_1~fY}MJ zuY7^UV~>m?5!H+yNM7HF1d+w|yhL)xO>>Yh@T&!{Wjjn_$p`l{&SS&b1|`fo2Ft z##Tp1MkRD@+vpAkHK+UKHU3Q~ZvG{|tmW$IvX$$%VGyf!R={r1dhhz1+1e_10h}|4 zRdaZ((t*JK378Qh1_@Y*)=x!dnM~O>6j|2%;KPB1WFbeYS1xQaa@zJ~Bm_Y?wg*{s zN%?UEw5Q(IgrDBWlBSVd&BkJ)@wK#ofGY4zcl+uAvWC4lL*aiG%Wc^ZNFQ}t*98fo7Y!?G-;dU3!jD7u83efC=EoR6iy_a-61<;DE%D@rQ1 zJ_A-o!}|u!qKqf`q<~f}iW?0<`qHt|yQw7_>nC2t_nBavSuzcEOFQ4pdMK#<{&%MH zB^5+6W~y;&40Jp~TQZ%N!$(*<@{2B~_-f!!j1MTWNer+femH5nOE zj8zr5Q#0E{*{v z1CNK$M$jyaBoTj-i{iG|M zvt^(>^!GZx0kIDr+py(;X{av98jsOg7f_{*jL!xQ)EqT~Q@z}_5WzevJ8KtVUK@!t ziv-P5>P{^YRV_4v_a;e{9(M3g^719l1oZF~aF@38wJRlZ3IQ5+8X$*T*BRweWl@d; z!!$-Oo#EbbiGi7vxq?(#;O_^l9#KCN$G5Um*9`Rcplha&KAxU7y6(pjpNkoCw9&gH ze{e1@AHQdyrB|RB_#bMuv0X_JnVTBWi4Q|rbu}gC!>Z2GPLkBPPmEW5uO#|gqL4w1 zp;0{Uo>kyh7JgCPvjDp=U#fLxA z6?aI?6i>V%0WW!Q z+nzm=73`d($O81X-$ zk%Sg5aeW$ej#cg#uN)}?MEg;@hM48mMiZvYH*MsF6+&ZrE9PA}N0k(`U1 z2*%`(-}ilx3O!*9%B}Ja_*wDxZ8F7a{3j8Z5kyYkv^OuC(ZyT7q2?{yM8v(Ge!cu@ z$D}Luu;Muo-NlB*b|Skmf}67k8Mjchb|4d^YZ@-Et7!~&af_oMQ~x8h`ku;4!js}M zLx6>fo|SH7(k+b`m#JMIf=fousv#6eMzLnd-^j{~pP6GCNFkbUDCqwf>40wnP4{ z@PVU&pD@-rBNt%Xx8Q?)z$MvSu?pZO z{dN&n3cI-nfh|@;a~G8erAll#`Sj<|q^P0S8(XPq z3v6--!gAuORf%$wWJUbga0rY7{$T|k#19Afn$`*4mnFDI6oZOSb439Et&jXdCcmvW z-nSq^xH#=MUVVjTF;F%fSwZF`*bNAutd;TP9n{6$%|cYhE6n58NqnK9AxF3q$J3Bu zyPZr)!b(o5V)Qy*0_>?iB&@!0OSb%P9C?E(DjQ=&(hc0lwoLd>l;P{#0$daG`i|il zq@}hO9O3Kg6NKk|h}bgzMM9h3M7=CyUmS$;(g5pH&!9mFpc?G(*7bXHzV` zHQ(i{Gn<)`uy^G{lp-2x2Oh_x^#A@}p}2nEGZBFMozlEN8DQDI2r7lYW;Q6Dtvv~% zwjG)hB%kNv%gc_ogzy^lrjxDzYZFi3cW}z(BBT-8M+JP>(SQ!++VAzznex(kbz-;A zIM~7hb*!vjhK)G|wBiR0JS(c4v5C-T}^h{o@iNoRsoixVI2l(jrZNKB5$|}_&#I7)?PDDrGFjm*$59Fdk#1OrhQ;O}1${(6 zM0bO>?$y6~1r6@N!xMk{-*H9@T5#ZD4gV4{)RYw82k8+sl!^|^P@pdX(drFY*b#YI z|3%{iv9HdB?NUE}(&M9&k9JpETMlko3nH>qOCMj5V2nvd8LSABOlKaqy{yyEVg@cn z*Gb9`10T=|y`9L>JO^5eF5j~#m+0#2M`V9tzAYVx;IJ?(T}`3xUUA-lFnahf|3^3$ zJt^cZhK5|X>yXQ$w>OxRJCix7$1cvA7-#}12{VClglRDtF$jb2z{=6DUTze60=Dbx zEeM65k7PZKY!eWX_}v)KyWhJ9vvmzGy6LNTc23}LInP0y&=`=#7ZXy#J=o#@0Zu@% zzq3!kC&%FnacXuRUCV&{;V@Nq04@f|HkJ&tSwGxT^r#v)YfDiAEO3Zo4P5?fxq-7mIsk^?^y2S|3q zZkx-1xw$YxdTz5oPd~p8I2zOTaSb?K9Z9D|RDU!9a)TYxOX#xY#+SbE)V{Qs;7p;O z2OI0hDvi!}O@1u2D1dQ!qao@RMtG&a`Y|a%zTK13kN5Z7 zzn#XbD?n$UV*=X>s0+UL?$f-rwhIn17#r9QXHY1Z0C58`fh1F_jZLsL3X`f#l8;{j z^ULG1(lhHhP#l<#3bksl-}^wlJr+Ryo`Uw8CqVirL0ixGvq*sPQ`2E77$6$p(Aoii zi3S|x#1aH^G6Dw?tz#3oy25zHJ`ZCfRO6ILhOiMeG{QL3BmaD{V`u3JX)JY<_Uh@mVLXVD(xh9node0Z4Q z+3BP(^)k>EL%6Uk+0SBFTWg4!l#fvI^TB=rP!|RAR0!8#r)w@v(Xqfv@++Q z1P1aOGC3`&=7K--DF`1TZg77ax57u57d(7zml9}!SAqL;g@cmwOD6DcRd56Mwzh#! z0tU~fa^Yx~f&g&VS|bc+fgys&t{c7`#pA5h9OExPbOV6r23l5l7YqlA7v2QzP6u;G zpvbXZiTG{2zrI5DP!h*Nik1>ZlW`tkjsMJQMk>09aLCO=vkfN@W*6`-2|z z{B4h%_602hFEIixm+StCMEK7q6R`=8%ckCT7Blk(RL^ZrF+V>|2F$@(3YF^HaC9@1 zBQ{S%J&F-bcYMJBl?!Fi%9s}xQ@neK>)`qpotY1jV_4>XZvdWti}H1t8dVw`3_4+k zlp`IaslYR!YQ8`b5Dg@(W_RxhSHGT?SN0jOGVZn-r0=&m;x*H1x-MBAxW0h@1I#5J zaDq;1#heh^VkAQ(KOxng!0+X1qtj{i20zswjwGJ;MLlkb1PH~}5Bofh7*^z1Z-XrX zu#XeN*r3zIVW_

OPMfLF{?HkYS&-!VycET|uRY6Dtj&auw{4xl3En-auX@AT{N z2l;&*O^E~79$Eyx{|KPEF#S8zyB4Qne;V++{ShnxfAq(G2%S+6Gi@uMyRg7sFTnj! zRljlNt6!_o&=RMynB3dl$G`P_yuS;-n-&&`I;v+51Wbqc;+Z5+6guACKBhvc4Rv&g zD@k&b5loOy6Zkxge6nTX@jAH$3NBQSKu6V42Fnf?ikdDgE#i32#>N56kqne6w&*c{ zs-=lKx*|Nsj8@ksg`CE8QtTH|_2C-9as?FAK9-d~3nIdwzLWsn5(yA}{Lh$8#+Wn! zha|R$2c4++z_>t}+klvWG!(J}rt-FP2~cmGbX*-N_neDDVIsg^dUS>5Vq;<=PS1l+ zXg3Y;?>;+BJ=(7sX{QVvlVXJoPK#q+Y-xcVQn9(nSNj~ccFCb@K{~%KvQ-ra3Y0}8 zrC`VpG`fldU!x+B<_@QgoH{5qgFbzIy$pcnHisNKVFytg5`u~kCk=qwu<08r^Cuwp z)2pNEOi-s*l7g$S1bj{yA=t3Co#k|<2oQ9W*7maW7Z1Xy3GG1<`rerDN*WK^2{_MT zvjuhQJY=%81o%?UYVCWzWd!WMVR`>tGWaLwPfod@kPJ1Y+hGM!0er>Gsfh%nsQl8B zfk#*j*7jGV!y)(JGEP7>M1jYv6-x}=OOZa5Do4QG#lyg(w8zz3ZGS`b^fpv=dC3hH zCbyp(7-Tz*fT&@MA=RAR5K8Ev$dC)nR5FN>kqryTW3qw*x3+O0A`U%#aKQEe1g;6e zVVwU^PuBrcYXaN_fNAUnfbSC>&|iU;0AZok9gW1#Kuhx_cS^TH0+bqGKQ)yIB`a_y zmm2C=XgH9QwY5AEaZh%FC0|{>PSD)F2f58A%u92p}7j|Gfyo2@`vRu+UiDlW3 znQtB&;orlVtW|*D@^aQb`C=k~g~x}_ot*rG``>!jT89ccDk^&V+8Tr6Mz>!+$&E^Z zvgx#dHWGWC3hl3L3#ojxgB!I}bjf2$(X3SkL`qp0Ogu9;LECVAlOrJmTpJ)kC3(4$ zJ3#JG0I#AWuL(sAe6_==ghWONDJl$M5^#ErbmsIVJ^A!HS>XgXgmgV1G}wkJ^z>$# zvmg`Txg=q3nE7GT3wRr-aq?pem0t%U!nJ0*F&d5jDXy27RQY{9cDF-0x+ zvc!#Jp5_Jrc6~o59I%T7(~ZuZjFZ=fofM)XRbVZ+8vrlFO|**@C_XPN&eHPQAwBcl zCf|G4Svqxgfu4d9Is85NWYQu0<{nMWM5Gu2RAnKP1K*D9?k-oeS(=&*(8@-hCPGGt zpgP%u*_{rv!~*6IHnSddJU~_~0xkCdykaM{idk|60?<6OPzwq#QWVwc^$3ds6nw70 zxnh$OsR+jaBY^)Y{m1|3zd+7ACpu7)xq;tt6VR9Y&PK;~IH^?O0pHOH>{Ar8M)@A4 zwpkxY9-Mr+Mth(oz^|XF+Q*k10iVbD^U09=HA|*k%iMsWa;j6=HLi7Wg}N`eWX%BY$?M+YGpb=2i1cjin@SGl?KLAke_#ZuqZr6 zA`Q{%MpjzT!k@!}Q4yYm3)Tbyncw*dy zao06K&GvFFx#flQ)d&Ov^VlrRfi@YX1takZ2pfP3i?V1m_({}%fBuq#^1?UX5(&_3 zG=$)k_D=ZYV#sL_``H>Bp}K4`7`crKP>;+%fzoU7^b|I%0+k!w;L)l^5>fVm0^EXkMfuUY6=-2PLF5D*LBFK0fK{af>40pz=@(AT);H_O>wlvr`8+xSly-SVllK#wlD_R!GcJ7ec0w zU*8t1tK0PRA3P-q*O1yKu@1^xxzrIeaXXF^k{6Qd!BK-woJz^VFS;ty)JP|$rbFmf zT?A27E`f_!gSolMgsKeO8)>MnpcR&?&Tfw#2S(2hx1DY-A4}~6Y?+557zy%TwvOtt zNi#_=J$z*qLa9yq)EBSHbw4&)4%*7p#|h2_A~^uiG#zsZ=?>TKt0E8VV8mkuntlik zvu5Tk1N9So-^S4STOCa8b6NzxAp#uqx_;yQshMAiM*Lzj6~&UyNSBr(ker7ql0PCE zSTN)<_5mXUFqA4tz8SU{^`e-|vrVo!ELLn_L1$|g2qJPoO&FzO3;RF~m3n-zzvSag z2~{74<5*Fg#`RPIo%JY<=L4i1z_sGV*ay}hs5((O-~q-5fXphi{SI&)2#5-g0D6Y) z#E2kDhv^n(epvpY0tDX;*IO9ZnhR(NBr5*@*n1Q3IIr`*_na9FX5TO~n8CgS1i_t@ zC{l~HSW;{`j$=ECZ=J+zA38_8l{r0cORl=l-%l{%bkhn?;P7d^e+Wu z-@(S))9P0nm0 z8F7?gP;}8gjLF6LD2v%wq%~v^2a&!v(6Oo^VbG;~o@ue7%GyTGZwDw@Mf5iX=c)_# z0BDGL{WxVWe%zReDKiJDfm=sV9nvQR9wWh&(?;-<=`Z&OYD+f#{P<#VJ74+;b~>+F z@RN(7Y6CwJ6s=~?0f=k*v(hf9_;1`Lip`I`reobly1LUUK zw(cC_k&D(}2Z!p59#snVe_NujnI(NAMQKE3~l#>!X0}9+}_Fs&SH0 zX6fIR80+E~uU&B1?9E;%Jt0dBfoP|L%{AQ>LFpSqB!4u)Zihi{6^lMVoDxL6Nc>bv zB~i>!GbmDU=~z5uzKK;I*uUOZ^>6?9DGj(=-G58JCGrpj27oB5YDceZFyR9$RaWAj z6{DAe4x;fBwAUHzun}m$@`3G2IY&?M>9_4&eDj09k`>Um9UlAmzTHFrYNRht9I>0u z9HsW71ZtnunC7*E}U{n*OFYex>wn$UWD=y#&v48^oqwZSN9LOI%_v+>1%;Jox%`hQ9b?qq;5ka93Ny4yeXQ zK!ayy!kma4Ak2Bpn0ve77O!TEgI%yK=G6_}k*Ul;=`RV=_9VG`ES zimuselKuyL-tD4VT1+X6N(XM)tKa=EFQ^d=oemDhRU34^`|cdY zwcf6dAL>QxkUk`VH55X+pOY>+W?2LgD1{NC#v+gli7|)))qOYnViKO@ca`&bxs!YI zXK%k1*f-q!%bo3sf792Ku=FPP?H?h}!_TI=ZR_mrFlU~5ih%>P0#N-T+ZMrJJG+i5 zGvWr6tEK{L>Ewyax@X@$6{1I|OKE~t%`{2{T0TM5l$LDRBa9d zURy-iOmKA>TuR@3J(;KYt!cr-p17Pe8_3<$N7BOcJ;~rPfFBN~B`Nv5iSkma!sN%GGy%wty=;7H1tk_dfOmK6ER>5%KBLb*f11gyPdVzK$Qzg$ z5j4ekoT^e3Wz0DJzrDLURa<9VXVy1t1zxXIsHpCQTXpnz>(2ge@#`?`aQe#3rk+~O zsovg%9vYRbjgdVq5EN%nagJ)Cw0R+Sk6&5AObOsKG`wt;X&EZGcHTP-Rqo+uX4My; zo>lw$Th*@4fVDdw*pr~&N5MgPjsERu8%TQ( z0{O}BRBY>+(tR*su2K#CM+@{Ut^QlC8oSwO+R2JPm{B~#$Ic@aP$r@LZd6KvW350{dxUH6PV_U=~qJV;d z3^Am?e+rQU+~2gv*<`la_;UiV<$=^7eL))<0L`$#DpYKM&~l}pAOU#z^*SqWXaMwh zJni5t|4(lC|J;)Z-#OA9HSIJ?J5Yc=G89rrc1Klrn^)a38r265jjDm&yUZrAk}%jg zQBLtQ?H%ZbqljZ0F_IW(GbOw~W1(g>IlZLwDO6x7D;>>tO3xON)6vgeTr#pl4dNpr z{fXz!uBri`FHw{)itJL&;cg?~=rsC*Hn&$}%%TqL?Nc>pRQeifi%hRlV@JZvgY3+f zL949H1bBGQAU!*qZtZGQndCORWZ67rARVEh zD@gbpldmXP!y~)ns;9+{krI4KZ?lQ;T-@(;ciaoz2Xkk)9T0fUQ7Tt|kk2$xuGBBT zUKhg6*S)b^`RGV*+!kX<5d+lR(H_Q6+Y7fGsWvL7h3Q%C0hru7x|?g5 zp<8l`UOD||PhG8}^M_xK*+Q%XO$5X~X!U@h6}~Rlp;}6@!E?{7tY#QA3M)j5O|5eK z8K`ib*WtSRJWkypBea8TxHz4(`h*T9gzHRqCF_$u;fX8j3Z6d|pM!}oE{Y5o$yI1y z8Pt}XVuG##IpYPAPnRl{pCqAq-GZN-@z*Q>GKs~^&HrGmCGg;2XY}B%uGT}8Z+Ci} z&3DCu{`+DP|37Am`mxrC|A&XW<37xk1@uh+ZU@~^!CT8c7B8!Q~EW-$rDvMfkb$Z=$gOd?GQuBIzHU*`u3W(BX zretX0G`c751CLZzLwiS&uCke>`86E^5Lzy=+c5Qog$G`++v;Tq3q&No@jo?`#yC|? zAw*VEFP>XPHi7ck@5OLsM_swPVKVd+AAWkuJb89e-?6t#9UP6Tgak7h(7hc&mB6Fe zi#}SsEvlVp8wz%iwuo|(&H7+{Br{B4>J@q`IrIUGkiID=T0EFJxoogP%yy~PU)tQ?sxvpqy*iF6ToG4Bj);9s2=HiSK6}?g{wZdl3u`f=L4RV>f+Z@S0$Az;IJT-9dBk zV7KlY-KB#4w}`&K78+S96fnDw0O)XHLzi z(f*iPKp|GRv`$?WT|MA5iV{?-6ygfwvw)&I0XaJmSM>!{TOyzX=#(i>RL_I&>+Wy2 zq<#F@@OfI=I4*>w*^$JF>f+@^EsbFD_o$KX6U5bgs>)QCoid-*-*|pPUs*`0-Q5wS z?y>P?YPD0Yia;uG0j)*`qdHrx<2l@=pM3m0&0#mkB`_Fv-W28TK!@4fNWz13X+L1- z920FyUNu86@f`;!-RZ0FdOifBx6=8_ldt4N-~64o<_a9Y^}uh&!hv6X=dqDSC>#dG z5LZJZ{d(WN-AsE!LSuv9>1Q?vey_<`whjVk5t({j&ZwRAI`@&$L6nTdZT6WHWQo{U z_HdtOk&|nUI8BUlx?qr-_s1|NZ#CkcShqn>25{%*AC=>VtQq58Ef}3<(IY{Z0aUH$mr`oqXs1Cs-&Wo! znr_%7VuW<+r8!Doz|RCO@gXUqNj5&Ys$Rs?!vR4&K_4+p$sXl|&yr!LQ)&JDxfwOZ zFr=FS4ir9p>+S?3pt?yg=`~FbIF_x}G%OGaspa_<`X?=_2TQ>+Fqv3uNS#Ab=D?kI z=q7*IEUu;?k(aIEPm)sp0@LLu&aJDpOi^!eTa6xZJ5vF)JMSKKKXKLfvpBczMl>k4;8y|Xrfl%HE&IB^<5kQEj4 zfl997u@l8>o61T&uo+31cIiLc!`cDlOjE6RkcoqUU>O)S9DdMWK?R3cb;SL;nOWWH z;;O;HQC~&%C7H(^5it@sHnC4%P71CM6&EDtDDPU?_(kt?4SQJ5oyl{Yk_HOyui0Uf zV<;27vbNA{ zFlu4CB*^kyZbt`O6MAtuY0h1kLmLoT{fbSmpjk$AAst7Eu_4oFH^H*qbS8p&j#4@d z4Q_%UeD&%QQr%1Hh0DvT6N#;{uAsnK^dvGqWhT6$6)1+$qWNc|x3ZJXvTM4a5S8!? z$)UL6Rlz7_{=Q){){0ubI-!$mDGlJ-JbrdXfBn=lc7O$yVoIFhbI62@o=KLS@{i== zd800no9}adu0X)m{A&;0bKuwajdV7InG|d978N1g+Z9zoscf7+)iW?;PQP>>`e=d@ zHK>ZYw9cTfI_eFb++v`^y!~1ox z?{?B+q~SN`&CbTSUc7u7F~)wR=KX4O<{ZFij}}2vT;bG}xpj?+ki4hvMPMteAYG5cxSTlrxh}}>b0Hw*zwE7n2O)x^#;rLh-o zO&%XqHjK{US__GK5w|!IPE2Ed)Tr0!mzBwnjE1R3M#_l>WF%$pQVI$Vi!OZ!N=c?% z4UR7a5*|JdXGo5-hRwN$sn#lzRb7~#ODX?nUwBG{9SO>I6%RR~3_*2UT~nWbQ2)^8P z{Z{>%4u`}4?$)ucU%B(}U_FNuo0xV93@Z|FsNR8IHF;@V4?qxTX=ydV%F!Mm_QLOHk#?P&w;rem!frUYRI2i=e@^feL|l zd_JW+VgZvT17w&<|FK%yDpr0ReZQ~187J-g4S*wUt)Z_R81C-H^Al4Okwe;QNLeW&*`HCs%K7IrhILs zjOHXxFQ}f*h?KE#BMWdP*YzxBNl%kezqqvzwaLs~w^!IfTT!W?vaw3n5BN-l5 zS+nK1xPubR%<7ilS%uYw!ja!fdLrolm8inC{o)GmK7OH=?R5n5ytI#7+yG z*t*qh6{QIPnUbwClCP>o{0?pfx+3-kGgwwq-itmVFd2pX4*{J)1=DKFH0jLQ8GU+U zO`o~C#xXloSJY!cR&a9@z9QhxCcO!Vt%uULx7DQsV63tnD_mmS?2z?NcvN@PrT*j# zr`4zb>LlfOR?s{^UVN%Q5isvL+NnDtF1^4pfsZuwO9<>Vj)|+2OKB4aJS`2%waiYR zv&nuxG5pbTy`KJFPxj5{-MA~TZ*=4n_uRJo7k=Ww5wp5gGnw2Dq5|ksa!vJ7X$g$# znJ1sa-XW`d`#MzsmPV5&qRX3$IyZNMAz?)s`rORXxMta)?OwIGP6d@w4CVSPi%Q+( zV6c}JxS4UD4U&!935)Pa8Y&memGr*Gz2`CbE{FCGqj_VBK* zcXYLS8XM(%%8AZjkwHN3P^W(M@e52%!vqi))1)TSbxy3KtY;QQ zpa94SDL8`=8mnc31t*x&7hOT9H%*dS8%krz`o!Ice?`-;CRru3XsT*5dkmQLd@^fh z7B*?bHj7KWconD(-0ewZ!5$XryVZ^K6_=LQ)iW1Y^)zZO^ej}2Ad7jC6-@CTmLP0V zcf6u#!y4FNkua&2K8JC#QQUv$h!uc8_J@y}Cn$X-sQKMab9`Tyc2Hge)K<{wY~p1s zT7$(EE zt2O$&cicAiFWz~4xPAq^D;kxSnb*_Lyrc%>E^u^SbNZPVkZ?;VUrU?K8w~2zsq@@W z4(>yCOjDy>gwAB$u0!zz$QVRNQYo`ma;fgIgLIhk7N4GMj?7FO5BSu*{Y-f41sEfK zNC;_Vrxz%J%mI{R6U7(GKQ+Iwq|Tk60_TT}JsO+%cpEl>2M;?JQ3^Xb=h<-Ug(|KA3{!-v~FjUD}&{llH_--kFs#80GWkRV`P z_M7sa&s`C0X;&pn6k*|zMEx|8=gG}fb?WR35=dsK;3*Haf#lrnME zDJBXWSDxwl;nBFpKTdg|&&{l4)m5;8!|i^8r==knp&WiMu6ma!p{Kux^L_IvH`ods z+%@>grgRw4Rfw#E#kq>W|wMZkSLSgFs+$l za#|JANrZvH)Yd{)OUXeGJ)475Pta;|w^)G<0aQ&HY^fUAsf=zr(1~e8T}@q?(Q7&K z!6q*WhlhUujuw|g89>g>E+Ck?qy$|tyRwB~?T9*})A*gR~-;uR4g2>q9 z9(D2BD7ut4wu(PftH1fdPcDU*4S>;T)W5ix{`8#(2Yz&*yS32@&P~)zguWuyl|yX- zL)52ytYJ>u~LE*6uM^B9(J3IRU&gz$gaQ}v5VQD_}GxPvVhGN85_Q#+C9*! zZ`;$ULQsD&s1pSjvk4XMk!L6LNxVk^bV~AQ$ypKKt)WW5)nr6D8sMf7!Yy8;4L=jn z`vU>P?(8xb;TS)1Y7V0K4#ygyWe|e~*QI{wo?U8n6Dl8y8zbFF3pH$dW^D%`Ydby* z$bXYTwk43~0xv4GI@-y=4)}aTTKcX!Mjrq{$pF31_cu4&SHW7HmE*hlcx$XcZ)f7; zw;$;F#XApn)WejmBL@$vT44+IsV%jq%VqM#2JkJC4Qij(M9|hTv{#i3)e^3VxiMX1}6nE)u@rw9ve=|?;XIP z75#^Q{-WYI^o8jSoywKCfQ-t)0JpL}N^dEl`8}(b2(g78%Qf(|pAXX4Dkq+DfzLF# zQqbQ#vx1UMRqyVOs{Wo1oe*R_!F_|_MsLE4o*Lk1M!owP!ffRmx}t#|ETniIJ1W!e z*`+OdfZM9KJwz{4D?r`4nf8JUZ$i;fL^a1j=KD}M;3{VeH@$>k?t;Hje%2E7e`0TU z@LzuLo&n>b%E*B&ef*hm?cv%c;-J-60-TJ(In+-$w7T2`RV~`5nyXn|DHT*BInD%@ zYa|d>mnRmqGZ3Y(%;=B%$%bYn`!jQqX%HJtz_P+xkQMFp>f3x6l0$DhePvS@wNHEC z-wV|fUv1@aG<0z*sZXAr0^XI?sY0E&5YQ;Pb58?orqtz@u&8gM2qSo4pc&%xZ6onR zRB7iUR79^@#{RWDhuRBe9WtfWbGiy)x;fIPs++)B=q<2&VFtPZ z|In^yt}v#@sSR6RHNTNlYm~TRk}e9fLg86QxmNmY;szBaI~r;yaF7vAEM%nLr^b3i z8s!v9UCLns9yWOvBxZ$)Fj7;Jp&|d%uuhQB*`Tcupm3Q2JS`WP2rmjIDMgT9T-ijF z0G={|UKDGo7PQcL#ER3)?}@w*Pv?Ct1iJ+QN5%(iV$(IQBN!YPy^jfk-#5;#S;4mx zX2=7#^r~GLGa-1&G4gu!O5Cu@5ATXFxeJ*f8C^?jTqhUd8}AfUe|NylEPx(1^MDxDi(%5D0wiw%r}S zeD~fs!E0G}^>wR!Q>*s*LS}S0K@-WC64gW^j?BC#M30Y6JkpEx9rNzL9)+O8}uL7Vm-V*%meMkC!^sYS#B0N@iIF{|--9cc;(A=-61k8)&^)21s z>IMS`gk^WT8t9M66j((uHEsn2nFj+D?j!^3={1$@q^$}09lIMTdjxrEb!g z&M#h4*;HCU*fc_jkHDaK!LabC>{+2!<@;rt{Ek8G=JL1$%fYU4V zIQbO|IL*^kf7HqMs+7>ih8Yt9ut~+4HglfE&w%95Q2Nqprb2Ea0`Yog1svcr6KQ&+ z4b{(7cmSAC$ZIzVlyE!ZLAAJ9HiAnO?L*=I-hQ}C<$x)mS*UB?pgQ}~l&R5AoljRV zU5e`4ZW+=~KR*G;yNT(uTeX4<ca+_=F&O63b7R;a29(`jYrb?xdSzjef z3(}vf*zCC_6~wqGpteL_1>ujHskMygcBq@KKuk4C8F=bd;4c|Gvp0dORmkM%vu7Ks zx387SECaZb)0~Xt~rw4jz=;eM+WvTA#k7EUUuo;tTk#mit0Wh3eT12=RG!v6um@6WHt6Y(7Z(BpCa>O1ze|J;2CyM(~3FVCjc z=s-lWAXNnu#Pm_7s7S^e74|rE7Y+Hiw=RqdwfFd~rn|3Gi}{Z~8a4A*SH$FqQjHXj zdek0hQ~CK909ay$N0ye~T2oIyF^)8qmytfbK=kt@GfD&;Op>aoLKJ|q|8)K@S2S zJx!#yIlgqhYKswEDhwV3B0~_7*FlQ^S}$*D%hEm5Q6|4+fs_##w@^2WxT-uydUoIt zbw5ha$l9xCCo>k(eHahT2x6)t^OGd=yS$KLS1#)J+_xJ&K%|Gr0O{oDbSAI&4oCFN zx#dkt6qhv>xAXhuN=@i|0xZ?zGbyY-0}3#|EssQ2=6l}yuR+L`87p_I-|&#SqK3AKsn@U!C^ zLe11spkmVE5m!UO$Xe=5+*oofwAOKRLwXC0Zz@wj>aX&T$Ox~?;3wC_-wA;2@!&fT z4R`$UPdv2Wjv=@-wk(sMJk9t-&_M=eSGxvU(Fr5504;uSIIOx*W$x^ZsSNH>HjTAp zdxzS*dJ*p67TH=ADeRDG9Xi18|Q5McFl2P>D#2HG;VMglo88ktK;K28wwCOC_4}r}JXq2c7DIj$aEJ^ev3#30t zI%n;Lz@*5QC`mP zn;*Pq)SSMwV(Z2)WQh#6%DV2}*C|d~2qi=;6#e{K0RVMKKmXD!$8Sd~F#*qV&4MHB z?(xGK@v5G#m~tW2FK~mA4j-FXvwOd&IfLs% za>^f}q}4i-h^pp5R9kH(r(4a_L*FDQhT6MyDYdHd8>`BOOkA$&RC6m?RGGz&wV`Ix zT&oN`q{_jILeE}ZP>a)7VUOh1OLO>RQ(+)}A}Y>Y{|@dMhLI!+;@2ycj#OG=YPKZo zEVSLYPk#~=+;!FS`=3w$w*%n5!H&*d-JvgjvJD>;FnFwLzRhMzb|rO|$Ova9d~dt4e}Mxn|`FF{@hMHGWunoTl2$$~afpWvC4 zof6ot^&Jp7kUoD?p!`hUOeDEwYD-)ttFv1s>);8k z;0n@*p+gNS5CahEW?Fk(WD*oD*o!qM)?>ZC4trqbonjv`owZp-NMG?0JpVWti|Tma}UBunFO146DIRN zU;Wp?*OK{t%Io>rV|&|v|0C}|s;B0X>h55R7@4~1wAl2Vkwl%W}C~TCCd{zH6t5xVLO9~SJE0%>g{FKWJ)>!zrAD+s|7Y! zVUEbX31|hJZG;N1W|By=EzHbeJ+Y{4SiU}edCj_p<^Bl)ZWaWFxVFg*K#Yt0{NY|= zvRvf8%_qzz0BBZ`q1!Z|VQPO=j~R_q;;Hp{c9G=? zZLj6YqAHRdsvxQ`8}M`Cw(8ZDjG0=i1N4Pw*y>XYD;dDSbzmXL(Ob??bs!{V8(?S2 z;S4xGTZA!<1RqLQD^QB+il|Kp4M!%nH0A`7LU}2GXDi@I?BgL*U-`?*0;L4DvP{VY z!?0VrBB)z{O2aDBCIg=)k z@}|(9T$Dsd_qXc}GHB>^swELpGf-qiEMb~juRR9M1Z$*1eed>AE}uU|Kd{4xjf?EnZc@#xjLG!=Rb{oNrX>jP?TaSPm5K}H-WM`iEAY+DuHlKXB{Ei;m9!LO)0?6*7S@}T zklXp9y1J6nJ(P&V7cg(=oY8xb?28Ii0~1iI$nYe{n4sVhu#89oN#G)ry2kdf1a}C8 zJlz_GNJ&j5ha!w%{fI)u7nxe&BVtH6EgYX@e-Z$tET*gmB7zxeLV*fNf0IC@<}r(Da2(q-knu%9$e{rE*>5z8 z(_OGXL`Xs0wnAtO2IYg>nsjnw8=!Wx>hFuu8!ejorGoAu2!8dsY3n@~*Rp2!SeH67 z96@9?ryf1AhysUzx+#s=9mtCB98ae2pj3I14{)ra7&t$fZi*(DG&!#gC5r0uTpF&cTOYlp+XUPCbhr=q zsGfb)qC48y0a#u|5n}zK9zC=l6cG(u)MrmVep;QGFRB)N3X-X;Nx;+PCIxFw<2M-6 z(`#jg+LS_R78RBjlv@g@%I5VTs?U;VNyBwlSBPm<(F$;q`3b`VD4a}op}si~B=wjp z%Q-7OXSymEgKSJP>1}{WM}L($44+HNUCG`g^P_aVCJjVw%8zscwHGP}09vdIWS2wg2My;;EXfD224pOo+7 zgV0PPP|{nG#*ff=4gegLOi!fDWk-hErcYCLP&4a}L`WmAWkL~v&#fUnJGo$NhW=&~ z)}feGudt4L`lISuCeBM-w1d52{6(5H-pcCIQWknAnJH2pFiw=)<>=+3zmszOW?%5t z-Z`(q@3ZOtV+@RbL@~sjAf4Cf9nMp0H1~4ee4{ zM;if5&FtFo+e&1eLp%)stLDXTjF`ylzYEz|Rm*RGjVX=;Dr;hv^|3J%hhS zmi}WA|6ruAmP?iL2WdS$`x-3G8}<_44uFAB1XY~?UqGP0+=obOBN4d1lG0L5(d%Ov z!H3@f)*>v5buyFFQz-&Sv+n30#8^BG-DXi2S1-{AaNr`fs+($A<&F2Msf!ajvqBch zf^GrYq!$LAY)iF9z3SBEHSK16e{p(KH+y|%3Yi=+B;u@F8DW_WiT6j4p8jBJTjX{4 zJ~CDWGEtY!6Qt?{yiQ&HpGtS8A85~Uj51M z_>|ZAH@+PJ3k7r{9QHv1$Wf7ZZz0kj8;etZ$<1tsR^~!gg)uj2ur&22=C;&}FPvq9 z0KIE)K-FyEg82UmnGlgsZ7eM6x$!IX6jI#C1PvspJn#_r?CnwK$CoV;(|J@+kkeOd z$t>8ofNEla7go|1uvxM$4|bUJ6;vxKD9Np&WP$`KA`Cg4a1A93LjqsQQ34{V7pG}6 zSGMu>GFkqw@x&Yf$-{3vO5PVBC<--P;@6kK4>!mjTg8A`pfE!5lOX2h#7gfD)`tXQ z$=Iv`hLi^^1E(MM2cJ(g1ucjS1acBZE(!^g*rXCfTPS(uXo%GkG_8RMf~tb(2b|4h zoN^dBAT>p>0!d^fc%#c|0B_sEWP}MAHLHNSh3@vkIaH@l&1)C<%Nn%|WIia>KA1CA zK(A@t*4tsCtx;j;7?C>?EF#-BW;Ozg;l^nJGYB@*@4Qxyw+7q|SSiuX?9RJ6{xfVO zbJzJb`j$MrW~D|Ck9GZeSIleQv$xl5%+2XgYmDoP_DX6AltfUSpT?+bBdNv??ltEx zFY9n~MZ^16>5U9t9bQvF&B$*rXotT|4UCNeI0hq&a7JHuD>GtEj~!)~Aj6W46*fi6 zr9h{0xP1@`c8plEuB~VE+?C6!Eoc)Kl)5mJH0Oa=G1L$Qf?+xgtHT9<7v*Q(SKJx7 z1uPNbJgL(e~5bQLho|m;^hSa>BV4 z&LRKUsQ>E%@YR`8LqGD|xEg=ytiEk;LQR}Iqm~wy)a3aI{oK>%V2tD#gkWC8)aVqu z*U>}Wvcut#;e(RX&>wuy0hObyP4YdmM9HLR=7$hDC4=<1jYw^cBZMFTYvL2700Rf? zBSrp7b|IhJVex&XBo(Y6pl9*^*lM;p-opv`OP>A(h$(;0x%!NwX8w$J#y3g6{*XlM zQ)K*)5i1_#IZ-mfhD@9#qmzChJYmT&t*NnmEdXk%dnLk2n3FIlfl&Hl^e4hP*%SayTD)$B_a2 zZW@BS15M%PdMc~ijtpr>qevv&RwKIx6!LLcQpU{C-aR_IwxFuMZsiU~z~c$@C9Zai z+o~c)hdWi&TV)E%GvqD3h@b#Xn!$&l{>UIt9lP3+*_9|bf=!>ZPsY#G59M7w`@u#96`Wo@rESKZXSH)El&Huld{j zodAd|eUcjSN73+V5wNXLwPaigm(S{Zi2{j~cVccV0B15L42j(8+)P@ZI5(?)@B{ZU zCikjK7qHy}$SiVLXabtKBnD@pu3QmVhJl@nDJZvcxY zD8aIMw>~~@Z2ue*UuMT$rUG5}l&Cg}lCeWl^=k!*AFz^6DP6#7kUWFT)O? zSplEkDHiqlQ)f*%$NP}oU0uyteRpw8l%bs9{KU6cCcI>Rv}&-2g{FCNE`@x3nIRpO zM!o)3o5S^^rD}EI^Pckm6$Rh>_P?Bk} zRmMt8=zx}Wbs??3^35~qT@M@(=m!l|h8V+L7N}BsTVe4gz#q4x)gzD&VKuhD(%>c$1Q{dC1D7_a`MU0tpryLi%Acy_F1emD+&-^$Grb z@_St{0$GZ7+G7N;+sJY|1OrGiX9dVB-|k+3^*6i8j{AN8P+*n9-{)obAS!E9(40QQdpAPp#ma zGET2{2lftFF>~y27rqhD5CN{*IdUH&3eXExcfxDPI)}EmixD>ahkxTYti!(fpVx5( zD#h%*PFM3UbR}9u&P`N^Oud^E`TnJKOkCvqAd(mIW;+dwt@>1#q`3 zu!l0SF1NX zn&9sQKw0%Bd(#pF)Q=EooXA(prXl4fH@akmHERk>$tTT!!I;quZIWg^mUkW`@pshc zzIeh64z}q#ZXdHCF12z+i>JnDU(lM^&Ma=L|NiK>+J!abWf&7@@xcfot6v6YL-lCU z-6X(?nF=o~JwfrukW3KFQpMu#B1LZ#5OWSg(*l1GI^`&BA20XRKdRE-p9la!Z3EBp z|FM;=BKlJI(mxEN*C_J~abXh;g(?1giz@swvFQ@Y;1%NAc|Jd}N+%>El;|iAsf2;( zSa2H=S&+ad7gd4CCmL!6BE3-OBoh?lrr~xkw78PQTrxy~-WYw$0293Js+wFT@DXHh zfgVRA8f9vL&O0UD-6fD9qnDRb_(NcxPF8nyc0)gQE=lHW)LlnM)cEBEH3!Y^@K_r| z7|s!lcgdvZ=F_&7B;pWQ7m8z@16ah&YF@2w#fB}wVq6I_0Ya?AtVgwS+13wEu@!LvpU$;ZR{{REPc*8Lyq}HJuyD5CZ^`$@~72B zhGt?JL6U)ZM>7;Q7h59`F)~1rf#W*2*hKbL{pT*s0XcjNJia3U z!naKWB95Z+dR(#*{8w-LXU(r7Yzw zM4JzCHOjO^9+^N%)DXO%uqwdw{Wb%VU-^E6-`}1;DnbZt>N!g4KP5E$B?;b>#Qulj z7Jo8SQ@;r*@Xz@97py-@{dfNUi%>&-uGlbX_J%RO4$wp7(~CHQO_(7TL?&OMG$!!D zV^#u(ks!`-3s;!wwFTtn`7%(c$%LI?F4Urix^)EdI%8zY4Te#aAY{KSK0CNAQbB1% zvKE5nnFk1M0}ga)K7*1BmWBkxD_7Uq!BLZLiz1n{V}WJayHNAVGuaa$ghLJE%e;or z#OEKKR4-lKqz4P(9Z4hqu>ZuSHJ_TqSr|L*AUN6I_q_X1u|FdXz*AY&(V zH7K4Mt5)lLcD3RHXfHw*h^VGY5;TMz2$Pd*LQteAiATTzR*Pne-us#Hv>Mz!V%#Q8 zt%3nr&1*YxVMT=_VN-ANBTMhp4e){`L=B#J>LqpV;*3t=kT{3O2SpBjJ{N5zua0BO zAXX59^pMT63#7(5t|f$zxyQCS7Jia3U%IB)qo#P0=mtSCCb!zg570hv#78LMJhW@!@gO~ zqHZBOR0z1p^T@*Ux9VWng7<(mg-xNtA{Lf1(2%rSxXAeej?8>5*`K(-1z?6q@D&pw zDSfL9WC)6127LLP9Q&($7-CC`Z1*z3@I0}IB1GDU-}!(IEja1t5aIm-eZ+3AzA>;_ zR`4N!;#p$xBRNKfDg;FNL%&bt`empDKCmjAfJ~OH3y!{@fUsrIWWenqHtQD0J-O(u z0EpC=UQI!JLQd-xebe6F5O2nDnX*n{_(bw8Q#u{wVn!_o&xV|sN1^ahg1hN7Kk@$k ztR(^y{P};sdPe`(zrIKtV@KVFJ*kVnXQIVN@U8cx()DKA9^blI3V-K0e=WWPuJ$M5 zEujy0CZfjg_aHWk{u{X#+Kws&0zd%#U9Dan3*xX-6itvbdNR*E3}jkbg=g^dDY}HMpK$<9`_d>2hUh z|AGIpKPZ_WG&CbFPBPh?r6)z1r!(A03=arc;Bn5~H*$5!z z-gcU|k&s$JK06JjPy(U!_@pr}VBHDy$cDP3TBvuO{8=S4lpdrE7sLQ&1dB*1!3mBQ z9EL0h{GJ>>Ani&uNC5l{Usze#8@a%=X0~@V)L*T`K__tD>Ido)V4Y+D0^G&IF;dAg z(WOsFDkiN%iJZbMCV;c9sQr<#$>do~qs2}%4Mk3xk4%b1Dhm}EEC}2*w1KoZh`9MW z98Z{aEw-}4^GTnc%&8xG@PMqRZi)KL7M{g-9Pgo*lrz0?kG|uUPW9B;dG$M=d0Kz& z%Rse&)7*PxP!FQMb7T2ZG3j_BMpv9Fpy2=@-wfcX!jJ-Eap}<^Nv|+4CiFu$(9{PpL|(h^)^Xs3e7mHXBw z0oU)r8{1)(wp3ck=z9P;?^xm^bi)NrCBbilZTTm;WUd<(=hyOD`7QxaDy+sC`t$c< zxY8w=31uF5IST`4jmJfTN}>&SFS1kMhy=JtAsa%SR?0kf4^rm}-_qRV&9prl>e%jx zx=Igl3U3QhHI@uW_`RapGQGMj0gxNMq%KU*hT>JwIJ0_JfnKtS@w{$Xf1a7n=!NB!*@qI$3xLTHOf^4p zZy)CY8&2?&nXQ}(qxlA(7~(t0GvEI=zyA&OCtrLC9=1{+xO`@N?4c}CF&$r~`)G6Ox(U9kteSVKUh-IaR$^Ilj z2TY?msC~#?&0;fA%$KwbnL&eg?3+cC}Jz{2wfu=m1#Muqb6N z4rSc&P`6mAqV)e@O@Jk1)zS0hrjzVUASwczNutoqy)PhNy@PR5U&3QfOl}E`h5%Q! z3{wJN`vmK^wUx82t#3)TBz*@%<5hRIxJB*QO4DVRlFU(hfbbbD76#*Z34VU=({J(f znK$u7i7Nirh+hJ@J|=xQY1c{QlS-9&gB-7wJqsjA0;IqT_=~lxqqyso(w_^}`kL&T z?U&!(F4a-+_2U&B)@SE(Y6Xnsu{~}2o6k?{a5Rieevm$6lN#s=Aws)_7=j2YB(<-p zrp{bhRiFC&Dg2Yu>V0<%s~UMMK~a!d=QE37s-QHs5gER4?y`E} zxl`)I3#W~SqDmqIq|B%Y$}b|qOA%u5!)E&hqQ&p{7_11?jp3*C;(?vzH22;_-BjKO z;o!`x{Mgs^ufJRXl-Aok3lv~cZkL@yN>(syVo^!YNoZ;4EE1IDPDLoy;@b+NLV}b` zh)JWR6gC)>%gMpRgOV3TWWl~k-=R0=#>B|U34n6Lu49nii6U%(nzCefV)|pmf>V-^ z72MeFKG1FT2St(!44Xt0q4ya0voF*+66sSt<#1@>y6jQJ*pJHEXJsGDwtW?gtadHLU!sRX0j7^DEox*xrQx z*&o}le4N1m%n=W;p_6A8$rQPu=_UQeV^{RU&(5;8&HA5w@D}sHz5D3_t>E5OQXTCf zz_$H*GnF&*YdN)d-;kc2-7rHvEgI!-y|S5S>Mt3pz70vE!J)R!$JVx>0o{DO;uY`& zJx5}};J=H81CAETU!j*JQyFWMOw!ytdT19gB{H(i7WS6yx)VLX&cR)r*MM?kbD5$S z-^g#Mfx&j2v&B>vjm~tTVPeq$#Dbs+1^s%b4qRm|r8idB)bczE&Op(`tqp*I#pxAL zGgTEp_<)WP4A;|n!Khk;ML}T*=Xfc(rJ43V=V@|$kb89U6@TCxPh{L`o}hWfQmx&$A{^){es+-wZ24 z0wDApyhDXUW^A{PVgE>)KEHt`9zj%gBoDnb5knQ9Q2@}GE&J3sxTTJW8yRk=PSV5| zoj~a~mP)lHT;INUJ*+z$$2V9L=^)=JKxpuV3~TrX!y{=YNPvq{_7e0=1odkVSVxu+ zVb!|jYUgFMk(8^A065y|7ks1IH`XrXUiH|EbBLnG%n!VyUo9_e>h8WaGc?kvUc##- zwOz4PXMX(;AJtP!S+i##p$6~|Id-s*KCGMGB9iAQ#WYcv_oEz*(g<03kpUAxZM;?< ze(EY^5ipC0hY50-Npes$8}^OQuiUbGpykoE%_431n~ztp0lZ7}bq6+kQ=?e;T1OMq3G&Vg_ z`WhKriiThj7=);er!YGw_R0}^0g$l*sUm%4pdSds9Ep*pE~ z9SmH}gfJR9H;0@zuokh8BxqP>=L8H(1zCDl;!ken~%Y94_|NlzQUSvUa(g=I~fd=eBcN%CfGWC>g}Ix_o6- z-#VJmF@`R2K<=Ob`p=GUJizqv6s((-H-B|+tRrx_nm;_y5&fkYY>xfA`_xP4r`3h= zS$ti>>Y@8?5d|bRd8OLh@XaCb9vbL09neu_UvvhLKU-PTm(E`j3sn8JZ=O_3YZ>*` zzkUIixO4i&7tWcB=db9Q=>=^=?{9K?89E(m(cB~PXbA4E&Fb?J0D+rDEf+zdnhH`w zAZ%iGo~PeOW+;;OpW|l#`^3ZSjkUtB{Aj<20BF5g&Z0BX)Q?}>G*wazVSA9CtNB#P z!WA-5v7k008X#q&h@Vbl%p{qRWRv3HCUV^JGcrjd4PMIO>WNd8KbAwWj`%wG4;aBG z84Ur)SNHMx8FovTf!xItKf&ms_-u`ZuJGKjPlW$z+xc~|ig znL`oh{g`1VkVX>8E*H;RTTbga@R64nw$$(Zw-*Vfc6F3877CncXRvYa(O!&i3TkmZ zYumO3Zj&Jl>>dJPQQv#KU&S!C|N3+DY5+eIx(rHy8f+S(HNT+ruf*|U4VoF!AsY!~!_eS_+K?>cT#7x7ZF zC~jSt7llwjmpjQ;-!Y2_gG^nxpdS6!3+jopi|Wc;isYc7C#O&lXLAzTqsY`nF$n{n zW=rNOqM0dBDl*A{#pM^KiQHR5T`>d>9BLL3ghY#nDYM(iaB&T{MN9dqT>brWrTTL$ z>f5T-@x2B>>ET@|?bMgZWZq3yhNGoJ0+1*w3x!R@7NwU*U!f@U8H?Qiw=fwATw*;b znVPU0=GO7NfY>?-X|XB zr$dN<=Mr}rl$2L!@ct1e5R~xs^I$rBf&_4mzu(2WI3x&4Ni0%Zen8T~7$Lj1{6$2^ zP+1dIAE^^hfdgR3g_5XbJvKqvS z^p>Hx7*fNo@zO^Wzu~Gr2v^wY^5`?Di;&^za&gD@;RpNR@hN?AZrkd;#(^HqEoM+1 zS(R*^bD-5Fg5)}Iiw=zGJ6nU=?{#|DQrY)1y`SWqUb%i&`MNz4TfiImba#97j-K{N zgVqD-0Ju)>lOMY8kZQsau(d6&HgXNq)e#l_J${VOOb(k$aJ1ie>?L&(Pv4D9!MM== z3ws4kQRiZ*GR&yK_7R6TCHPn^J>wPHMRH#XosHX!>Z)c$yi^*Ef)QPZ&L#*2>HCQU z+a^82)K*?vFCXRF`(<8AIxlal$M+lnWmPCM&yrqM>Gi!+oS(o5TGL%grKP9W%8n%E zs{|x6HMy}ZK(oMMB-0^STB4*%z zK*1WitN;p@a#Fz3ych78sxFu7|B(~hkjpd`*r zeuX?(`(V@h(GGdD>%!sjwE5aE9z36l*6JTlLrd4jOkR03;wA~8|p8fTTsJY z5w*WBipv~$O)y^kmLBK`=*M1Mqyyfh?!TSPk%sw`rgwIb^NHUyzha74Evne#QG2lsp+wyZhC#7?FdvnET)q{DVDL8K;=N? zv)h!tX-ZY-Y}nC?43==|_x43BgDo~#s}l0S77^1TX$=*3Dyp|FWKq~ckO#;Dt?P~J zgLmMb0d~XirAj6B&w+dVU*F{;yyd_BeFZ>S4W@!G0L<(}9pa#ruH3^kSZQF8cM}9b zmb-)%V)Yj!1C(B&@P!Eif-kZ_(sBaQ^fSCJyNCcVIkj4c*7Q?)7xFvAP62>gCrf@` zdVwMfCEpM+RAHtFtVGIRfYOuA?&?p&{Ng~a&j*3-BG}S7L~PzocGbi#EI@Oa{95=% z$$(MV)zS-flNnCKnmI9@6O|aULTNf6_XTlNaD=FK!2x#ZRd)S9dwSmN>uu3-ko?kH zl=WxBVFMBdiZ*v2?o=l)t}!`IoBe%$+WM#i zJ)#yUxdA%~h#ebKYdVdRw@dXvY1QB&}WoF!lH+YWrUQtP~Ny<0NoQfxgJEI z7p4PxG$ARtfRIESR01AlG1EqImA3)?a8U+VQJiU_-1X|d zL;!uj0z`0>D!8LXr%$-W@>~ev*kcyzD6{;NLcv`3cqPan=S$VbC1TkRG9YRe-Y>XQ zOWIyOzxa*F2AxJHa5`BwqlhJl@PSN5q(3NuU)VSz#`^VhDgBjG%cdvl(eFLd4*Z2? zHnLpAQ6wdrEWgduD+XYiu%&PY@UZ-0Ee1FgB2oEW^gRyN}c8Zz+_YLKft@>Ak+Wx0Ka0 z?F5?g<$a^Qt&0c(-+8cK9qjR_L$@AOtz82o1T2t3DP%Ggey zJgZ-P?j=)!#wflq(&3S0pa>OX9Y)Y9QdAg!vHoO+Bpj0UIq3;W^2q%qmJ0L)-F(l6 zu4Gj(vR2!v1;G*V)m}>hbIF%-*vS1PCGuas*=zM?zyB)j8a&vE+v2=9$>K%MCOXu11Co*4W>;5 zL8T!fm_a7LlxCvN0-&1zVXSYxPzsPWP2S(87x)MA+BeAK3ZH$PWcIxP$icf2FzOoX~kB_%ZgE3=x?pOuzY?Dq1p0 zYe$10E1H6zA^`rs+B;yh&g)*IsGb`Ur(!DU8O9GVdNA-3v!mhlYlxyUYiyC+M;g7yvCfL zOd?O_R(3WbkujS<2NJ!;VI&^zs+1~^aGcgF{p>*{lTPw>Y<8UGEqZnT_QIefi>~V#`y{o}JU@5D<|zy2!#w5JSI_ z-xZFVwf762)Ji(!p5k8xWz`}HBs2Jg&~JplZ_(a_zyP+-;MZ#_6iX?}gP)>O`^u~M zjBlx5eKi447NW#N@+-Kf4dV$sCZ~n~MG#~nc6HE!JSWw!aY30!dtp48sihrKqC$aK| zd$2y2mM{g0%CpV5!%WGMDBmh-AoNBbgts`XEe!mx9${0plFW%mhdTA*c?G~hee;D$J&VPusKAIP4mOk)HdO8>DRG5TN*Vzn!9{SC zez>T6E2wKlYw!^TMkA`t^6-&V2ObmR=M`RU0F;%evAAEO z#NFlhICqnNYhV~wvsKC{&I5oJiD2+y>E`3(A&N29kR(RGkJO%%*^Ji!{U>%i(tHtC zpiH(j|M;3dBCjJV(E|44WXiPk69mV9!Fyjhdm86!J9 z4^v=}BzQnsL4nGMM3LxCEMc%74>-vR>tYHr2t@E z>EQ zS-GCOI@?qyD$E1jah*burFW>$9KK~AD$F<`x?OO%W7cr+f@eozd)q{t3i{C# zS8+hhV(HSvG{4z`jd8703E-mxoJA4}32xGAk|}SF^A?8H!Tlu<9pyQzSWbw@YIi&c z%gC+f*V5wJc9pKt5AWk9a`0BCksw;u}Jymm5JYU_@e z(-)H#XW7$KH{W=AMm&Y-8zM)B`iPUV+~;dD=Ps_o&EC;%SRV)uK;ExC;ze(;06f4G zO@w{)HyvjY!kE4B&(O<%f{%*C)$@)|-ZIwr3yD}z`ZRhFSGF!7D5W?Fw|qB z?VXrj)$n#{hVog_FP@o1p?e)YOgvm>rq$P;oq*j`62W1%FPsF)+!=6iJ8ASC!M=hR zSON$y*NEt43K<|wfRp562hmAEN}!G|!BTgF>Ev?JR~C~}Eq#+IU4Hpb^8c~- z9zc@c*L~;v^-SlSCiLXAJF__8cc1r%s8W@vKx!A*U33?hyO_}Z z@BhF1^?UD|-kaaOQ3X)XUX?=kMf?nQw(`YCpun{9DP|Qx)Ug-(j028n3$F+-bPlJr zS6E)>TO6I8y`V02`80DzEB*9icZl9tjZw>+>=S>S-~X*U@KT*;ocS0hq*JZ3wBv&( z2tk&@&wvm4+@ZB>>{Ttu_LHAS7&pwxw$I`@CSVB+J?+Sm_9U=VsTzH@YA^- zt~*3Gh6${{HoYuwo}a`R zvVcSLxIFs`=B9uf5s1)^2`&wQ7B+U=x>F}C3=KC_8r~}wwFc{@25SyJdid`GkrTxd zh&+W)|0q3-bI%9i?qa@J{>7t5?azX*tlxp}nH%zyZe#&;4yatKou=@;&INmbaTwQc zpMDxC570qk>3l?uQ)@5`BSA@ z?b#dhtZ%(LY495F0XKS>&d%XC#-P1iSuSdxh|zyvhZoo$acdxx^MRr!w9sBCe(e6z zez`=32%d}Z2tnYnPNxI2moSbP>5hqJE}x&?w2*O)&wylbA30Pe4b0w(kjR(WL+Qwb zExU$WX{ZxjMMIuGGb{HT?jnpQZNB>YB4k=?gS_$f4W@7-;rgH?nVQepO*F`bLcvZf zY?J+x)NYkP_llnp;N!GJD}1=!afInd0m}dBFSq`?{X<><;>1{QW0en&2;=C(CkI?} z3+ouzddb%fOHXfy8k;NnXBb}Xi?3XP1Gr?$>;-42Oye`F`g=4AC^aCqGX-hWM<-Xa zZ)$iN=67D+DU?26ELMMm#^D#3F21^q zu;>9}C%ju1qQ^fh)> zbcp(C$3>=#XppexwE9YNex2(0RH@Opc2Q?I4w0U9aKT&8|pyMsXk1BTg=~ zA*6IJxPc`zx3c3nfXqRANv4v=rsH`|l3fBfVlN9DrR1xH$@oq8Q&#l8chGB(mJV`Dugx4MSb(+3h;kgvaT z*`(Xs+2J8Y%VshDof9ZAy!Mr>Tn4m&^v)=&G#0%ES~QWovCAzJK!&SQx$5HIyqBuv zH&$PuKlrz<7WdB$k5vEt%BuYh8p1!MKEB9%NZ%_>=bpdEi{tH;``_aSzoQ@dUTKMa z#sA;45dRd2R3-uH$oQmF7~MxhxzKvyDSEoY)xueEx<5K0EuuOWe-3ZGX8$QG*1v$XCQyJHX+z7vl%VdW_U6kJB};6O_~eftqfN(X%G+ z8;#1;dRfv@FN_BzmitWzot2tlLV@8Erl!I2C`qE^2eWhXd!Wc99E3eb@)z%6v%e=y z7DbEg?@wVz5|BUq!fOB{o#G*kXLBuUhuXcezLGUv!@ai71GBG}x$Uz3lRtXJ^?~~a z;VT;UQ$cY@6OBK3=Dh36FJ5gxVf>|^+Npf{7jIM4{;#~0(o8tNesEv!NA?jvIXSar zA3Ql?-N7gq#$!$$hFghc)y3D&8do7_6K!#fD7FEp@^*fC6So)0>JTQcO|3A<>MNi6 zs5@^~4A|70?cF`hX~RxIT_;UZ$6!{1bbIK!YxO1GGJi%_{eP2p{rX=yrshAt!!vMG zI>5g?7ObgC<=UCHi2HZh3stz7V|;pHe}|k0JMJ_~TtE#cAY73xUF7R%w_Paa7=wR~ zw*Kca)hB=GJ#n+G(zrnP_Jaew#kA8y-4hOsP%*q+mIFJ|&eq z-{JqXfO3kR&QO=;>K39OC}C(b9he$SERiR{*Kv4pvVjvJB1h>oG?#?Ek-*g zNt&YOlu*oQG^N36qKI4Z0W_$IwzQnPvSyZ+G4*U|HJK>Mb!d&}R(EB3dY#9Kkp=BC zM~C8&d|F-ET64q>^g9DDV=8X_X)B;zQC_LYmXcBTyOciVKcA_cr<})%*Xt+ z1W;it=&%@e2;4G@zFXA)@~XAZt3A0FMLVZi9$$$Zh@9@?@_|qS-#g$0AVYFp@JW|R)IW{IxgV=CSEHegK5FO->j`} zoxn@-FkS99I5w*6ZFBRalc9g=^?H8ep_BXiQbZ9D4Pg%M&q&qfH*t5_YTRg=Jda(J z9c!zb019||(J46(dg z#k{nls;C*jkY5S}LO)$BmcCZ4-d5-Awz#L{?vU(M|>m}+O}9w`uaLR&GVSeuGmK(z1yKG4fSUTZWF7Y$(c~L!dpL5l6;~D0} z0^#aDKtOHPLM%!hfQYblwAEyc5VS;x=oClwTU4}N`ITy)MKcCkNDIx^+Svyx-;w|+ zvSKaWAv$b!PAaWZ`Jpag)lZIV!pQ9=VsRJulBhcNsA>Uw{2C!nHF@RIvaIA#>O|la zG6F~KE@QRVRCl&O^a;~}R&#ND)4A_0ECKiiV!s7oYy!?!0Bq~ zvKfE>{LG}>1;M$!qs_z;DVdsCmdR@qW^QU4>XLynlt=RpPZL@SsfX&P$k@wyXV0o& z2Z3VFDuPkBfle1H3;fJ_(xNI zP%odvMB$$R=6<9r=G8o5xPMjCk4K==hA@w{gic{X(JDQ3V2Z3$&#Zd+kvK=(<=cvy zCI0*5P~5h7d`Jvi?3+_t$m#q>^GV2jCi2uLoynj*F~S=idPW@Urb$HkKA&HX9O;t( z^lQ)CAAjn&LHa0jb8B{KuVFrVh?F@@vz9lj=K4axe&MAV^P?X}@0k0K_t*ke`RPBugriA$0b_+9#pUK}@76VF3kKUkem;oJ)DN*n9fO6sLw?5_ z#cJ(7&fmz%1N}14m9SY{obNu|i-ub4H{M(}54`Vj8|&=DaMW#fx3co}ue}0rQ8c~X zF}uCKDFt?=bBk+4T;}ln+e7W-r2YXIxhaZuK9DqP8^aK!Y*^7NhwlozjDJg&5zn}jf()lh9%V5k5f%i_yOSEAfYws+!2ZxBb>p`LNiU*3@8`_hiX$Q+?m zIZ)!Zh{rUb((D_IThJ?bevH>RnshMQ&dhGuelnax*yz9V>a-mjPMRpF^y&^Ynu3w@ z<7+b58F4%djXy|B@5XAYD2L$>V#UvKkP-0EpxxOjN7+R_HMdrnr&oPPX-@t9{k}%E z{L|Pp|2n(4AM<$K!tUY^ihS-4u(x{T0xx8RjwoB|=eM)W7C|ETFp^H~(BRcOw z*|mzj6}sX7*B1zWKYPAAjxTwuzxSF2@K(P5sh zF-d?AFZ;iidI1EK;>crk^|fRBG;aplql5xQ4XCHDR4IRq(C%5D>iNI&bZ_QAdsYBe%q()UH=7L*+C_-KFHz^0u&Q~G1N5kWAo962 zgsOD41%&K&X~E3FZClLp8XXfkk4nrAg~Jje?lcqgx-i1{$l`jwND+VKW}aajc8;bK zu`hL{qxTXNWaB86Xu;&6lYQpMaJ%g9OPaRs4zs~=LRB><4iDiS3h{iXOWt??0s3Z- z433U6{?B7X;4$4q6})wASsjqfet45_o*!4YWoLh?iG^w@rVCuaDyzj0l-$7Y!wIRd zfT#Hn{)M#jJ;iponb-H*z3*lL{C3aSE%NgKh|lt_3G?2(U-^^ih)2;z$5fV%!}{p> z=n@%`D-KVP$(K$ayUq_>zfr^2rqoW>4;S{5d3q)-GxN#b3 zMJBglFHUZnk z-#EeOoS;KWMUbXYeXmWfB8zOXIGP~lZ)wF}@Pi9$JDuP2-`J;bM!acXCU)HC3;f>b zK>J9{?`_}^k8wl`32Om4#xXzf$fz7WHYO+bb(lkkGIEJgUio`O71$HU$6S#}Ncu+h z$=c$)Okba5u&rd)?ZsNEm$ zMatRR^b0kj2vgWFl;bNcW`@WCwV(h)!A8sL#15cDF{;y^L-C>R+?yNQ^7JdSrmHQA z%S=eNKzw!N%?$Sus$+a32bbQSRM zi7s-M(>VL?s_&j8`}WJ=U5}Wh$!Xi$88FB1IxMfeHX-Yqc=pk~@eyKn7Ot66ZVltw zvYD7%w##c->1&Uepd0s`*QVb#o1g${9**M)ta3JxU|m6=X?GDC_W4lVejgn7_bk=x zR(i`d3*fDEHNO30(Q)`Il(sm<@SX%tF!=qM9BgNmv?)4_zL=dpv>1w@8citqd1a?c zSEEs+Z^xC=Ey7C^ncAojrx}pGv=0*syaq9&ec*7r>=2@(_mxgW95aTMja}W}$tup( z&pdxc?j1{+>+^e{oh5Vn%DRm9#B3Z_hPI9*97OcY>?E-?10Yx?tQ&gG4wnB^2Wm1# z4Po4AlrmB>`}-1h3*t6kKDkwCd6Z`IS@d5J7XM+lfXDL`ySd*c^)7v4IA!l0O`86M zOHLl`lSD^{)V5Y-|G|BR91r8o+#?;G5wkLR*{)x?EJukHjWl*i)0+bD$k|46lg2b?3S_dP4kKo9qkzw!8y#z&^68>bMd1k&zsMeOVrd7k#4Rsfn0J=@jIO3s zZ&jL$^pR1b#OZQIVowr!p4cb)Ga^y<~srMs@Ow0YM@a0&j}FQ9l6rg$#GLBN;eksJT?wv+@k zOBz~BLjFFO7J@x27Z%`8`q0b=&&wdU5`N0#+ew#X?~dE|5yOyJz}@-lG)&A(?^$T zT%bR8TA>s6cg4XX4-AEm?^#e`x$Tmn7*8KDtTu;5tp~G}KY>6= zF-kD;u0;S<%-85Yfhu`0n4fLwG)D+6qcY@VbxJU%NAu***d-jfqc3O3W8MjY+zK zfGn4935+c-z#m|m&-XVIM|SgMb=Rpm&*&A9q4A6p#>1rvC5A&%WQ2^Yi{`HL#oGOY z%yoL8cz}_NB8z-ZHlY8MCxOWQsELYirnL#vB*YYI8Z74Cu%Hg%?93lM<6&SP=1}Gn zQbM}TkmPgQfj)iv%QM^W$3u8#m#*)QoCs$FW8T*N^-y35S+e=b{Z&tD(^pC0O#Y(9Qk zNEO7;NZlJlo%KTL^ww1%>@8<1FPNo)K=_t>z(-ookuP%CZ!sY3ua3E^m+z#o_rGs- z1!<51Zk?ZtYFqrVk)mtwEKE8G+Lwz`eL6Xt{i{n1pIxOIAY#oN&5VpE$B{*awGD^q zZ4b$b&HpI%RQ}qH4$n(cVYn1Ew$N0ZgTHvH64SHP_v`$9n~#@aFm-Mj*YX=xqWqmo zV!?5xln!$aIN=NqXn0q-S6SgK#q+nP%zE%9 z!sSnl?ce9*ud=TGZ{lr!irmcD7*lc1Ut%H`fG7a##Xjv(2B*^k9QC-D19e4Qad8ES z6@0V{kHnoI#tD;XbIi@P^nQfnc3fm{iP}#rXA*%J>Ux+>(v{lr&ooKUq?Q8 zveB0!9jTzqcN}|e60;Yx!IzpWOVXg%fXE?58wij+$V3L=e3jPIYCY|CpZbCD zd@cXiq>>^#(Ar|8z5im{e$Wu$eR75LI%^C$xms~fKBD2h)9YgZn^_rPm=V?{Q{1BL z6<7G0P?Mc7R>n4RAv6$rCK`@c&-&LINo%4~SC=NeLit=HqHSv8_~`KB1s%Lw_}B^; zhiSS+uD5-GRqg>4Qlmh5vH_%bW8d7~{%bGmk-5VuTlVMnxR&o2a(+4QXmax>Il>uf zUtfUn6yRR~V+mN^sy(cNeiu5U|nz40RJ{TV|xiUI%i_+(tmb0vo+fcki#&6dk*F{ zu$W{fy0QLYCxW+5*9ATV`-7XN&%jBv9eI>HLgZvb;%rHC&|52!e>`&uKIsKnr`j3=6nCa2zbEqNBS0dr%7}j&d}g z1b#ZOZ13eL%AAHOR>!89n#ly377C_onnuP9uwthXh5pzPl*&I13R$DN1t zV{lx#+&+*s?R<0s62LGP;)D3Hro1Ffq6$1UpIN|Lh1k{iE`Kcbxec5jbk?e|{&@|? z>``FeV-x(lIuE3OES4mLLCgX(G=q3uuGjMmOvg|iz`7AJT6vJ*9RHo^P) z%+tu$H$ysbVw(uu4(!iFI`ibpy^w0q#cb_?Kvn~ja>CxT5`!4UKq2GCK z#^i|-6J^TpqzQmA>5}cifO#8w`D(42vNZZh16@kh=1?uXoXx&h>O1G$Z91@TVZE@I9wilh3bz+})>;{c912d$;I zhY?`A)ycNv_fLq8jorz;Vi14U;(&Z8&bEfc0NO$~01#)Zoe(Dz3j$N>2k`&QAs&q% zWMKGpS8-~=R!VHCLaeSBX3^V40LBI^3o)K2(zqoyQ@Ita#hHl<8|{!=62-gJ9`%m7 z{>xE+Pre^!_g19_?<9Qm-x0h1#9Ugl(qR1EC?r8`M~0?4=Mouah*am_NMJNXiWIgl zlN59h;cd|B4g~Upq2ujXuQabyog_y+$o$o$iT(5Opo8PHBkC-QEb_&ycDkLx4e29j zE}Xj(^CQX^AiN8V55TApT;qta%uLR3vy}=}yOA`ZVw!V*%nfntU3Cv)I^<2u|6GT* z{LJO2?ACpXr6fD1w?Jhj&*(h-cMMfjzdDOHYL-ZB2^&pndO(rSfW>IMBUr)39)Pr) z5%L&e!dpfL>n|{ay;fI0c8A|>ju zicWD<|Gr=D;={K@fm)zYyDt5!U9g)@CXtk}xe(PRqnyFzoJ_u8`{7ZVg3(A^XzWLC zlgT#5)asX2`OA9Yc3@7yeJIbrJdB}rhfBM>B4To%9{BeSdmtqYrFn4nYPL`2(}lFV$`EzlZ7Q~@C+ z5{dRy-F>QIg&4=>=HKup!?1+cSLjrK;?ji9Y;M(LKM&7w!0h1pWKp@)EJ!rn1ba2+ zDi)Uds-jemBZ~TS+^OCrR}c%c<<|{ ziC;rsf{s`o*sYv2U@X!@jaW%cfjb7k2=h730{;xsAJ<2g`K-!IWkGnQ*vkz2qalJh z0r{pr0a9CKW5`&Bcgd;qz3|SaaJfdT2%K$vW>j9`WVH*rL5@63Eo!qA=-S6rWE^=s z_5$ym+j=&zj=Nf>2kJS1JGBKvg5J0-_@1-*7`52Ek0Iy=s%QpQG10_h|C%*zpm=O zm66$WEG}v_jv7`&n2lR%o5UGt9WI)N)qk0pUo35e&s{MBut;qQ$B%DOd7b#qQ@yDD zE1A+jTZ#7hWsi@TBtu>Kdr_b(NMKl;Sv_D^&+fw~Q*S%yGgdow6-P{>iTqS-;U3&P z+$T2JaLrvYl4TIW6SMNuz$w$VN|D$T*%d9Q*;1APB8~WbJ_`Mp-ouBtlH%$avQJkj zN8utMkFyNFh3$HqZ8G-R8fU z{T0^eoL1H*%k|gvmv(u4ChsEMST%532SAa`CDGM4vwr4}4v$T^yTT?dR~IQVya9=Kduh12>GHlCg@NLeT=mR!DAapl87Qy3hJJV%6knFy`=4pdS| zHANF5x1)$owhcEr;P5W#p3;(kYRr6WwsRBSJXaIQE~Elr936*?^p0ycg+{)FQ0o+6 zBc7E9vzRX~zF&$CK;8IVww?2YwLE|9OgPOgFtEBj+MVYrYmE>byFiwutKaFFj=!c|=5NiF4||B;Q$>8YJ|W_qm7 zGg`vMefo-aDs6lm++nG{zAFt)prEcoG1gvN>WEzJ!q#OSOxUNPR*|&FctEjgvif=j z4|76S@&w0uHQHgollf;}(ahU~1!xE>w>( zDI#@wXu2y*j$A71&{xpTV#MpZlnTIfEWlz_#jPrSpJH0YM7NF@&DpU#idJ|Eh)RB* z#4th6RvMYhzkCEauTEN*XVul{YO_FgeB8y2h|3aUCuCXCdsOf6E@MZGiOr`SQSI_Js7kWVDpSpqC(JJs%5G_yo z5`p!jHn8B%RtNwI<3~vSf^JrxjYT$E(|+)|DiDF{;LOl)p{I0X7r$QfIZW09lwgBU#1lLxcf*xm4p<^ zf37sErhLt~S|+x0x&<~|Vo;1bBc8_;cn9Xz5c8=ycpCSWOeza2v%Cb#Xjwmi{ka?) z$2{8yLnW}9+RctZ{Bz0hqs@76>`1Z(iW`l6fRSYLz^fl^f=I8UHKdFf1{Z z{1|N>zdKC33{*lZNxmRscsC9&r@QaUnS4*ic(M1S&p8){UKS;NMR%{feW{ z%}$nCT3?Rqjjg-q$+heDnWZxq_y(%nR4QNgP?C56+rR#|3uMw0;0amW=AdKzih&$+af+*%jc)y@ z6e)b~7dhz^-PcrD(j33t!)LOx0y zGd1suK-kxXi3b#ykQjA89f2gKKyXoOXw@Ans~?kkN`pr=s;hO?NB3*ITR%ZdVRe2< zEp6{x=gcn@9GALx@cr*A!CyWt#xpINOC7>Gw_a>RP}oQ2hU3X1)XgXtsSPmyK1zj| z2X4L_9(nwI%G?-&D%&9HH+v;1Zs44#=rM(g?@#mcacZ#=0P=f87aUftMH+|*ZV|tf zV~vElO)+Jep=Lo*?W9?2N>F}-oW}@+7*ql{jSF>AZt?1<+0@#5fpoFK&Q84417O-% zLO2A1bD zeI2^MhI#FB38Pg$?&IJ?QKv7tf@rEAFdt^bx!DitC$G)IsdbL9wm}`ed6HwAVd7j} z-GoZpmpCDT?_q>yR-YXGzW%#6H?J(w+DY34CZV*kfiM74jFkT^km?w{uo-fqH50>* zlCr7~)v<+5d1Y#iV=I$cJ+uf`i4rG>ui~xbEenou z|JenbD`9{#WfXFs?gecfYC$Vn=d9pl{@7yHn*O-WK7KA0kmtS9hm5 zl*~N*n7_A018Iad8lq86LAk{&{+O!T8%AGd;W*#4|z~K)!~F-^2!=Kw6BZ&qRq`z4;H}<~g;^bU}K> z=)qv7CTVFG%V}LymrX-k9f{2tW@55#TG76;jU$vD9oB4oO>6HjzX4!7#S zV*{!O$i8I8o#*A`Sv2=EPp3Gz(8=9}TwbAW?JAAB`U=7qO7>BW!bf_``osTzMUP$E zVMArG7A@>MR?zE$z!!iLES^C?QctgsgO~)ka4IPluyrbhK`!*4N={%_Y&>vaZY}4) z=5u>6S82{pAY6d*!JcAH*SW!u%70Qw%yx9eq6QatSf`0Ev`K9emuJ>SOy#n#)XUII zl>Era%*arA{IwEGGAxQbzPP@_Osm}bZX>6M`qTTD^0roD(kt0W|AcYw8Y5N>(%!?{ zkUJMY>h20y0VIrO2jSV?-n~a`jjDsEVc1wg>Lz8Lf0^(=88-m4TPn2E`N_K-LG;Kh zsYD_R@RTFkixi2lNOOz!A=i_V^Y_Y3C*;Vx+v&gfYP-$JTbmboh~e0Fg~3?$5i>jl z{t)8qDh8YIY{fr%Wp1+EN6imTh2)GmK=^k=kZI4cqnOmE9^@V4r*6_5pH!=o_J$pP zt7~twk@R1yzmpd@QX-P83A=IpJna9h*Y=69NOB5VSg7h4U)uow44GMMH9cI$kSLyD zRpN{Wlv1WnX~ID>J3EL9mFnT{@rSTf&M*_?A$}sZB|uH47zOGnOMA^>S<4U)7)7Xq}JqU4{I|HSwIt|(KntsO4}N0h2u;V*Yn#fMH;&J zBp4Ly-saO~4>}b0ajvJqa`On*VwrER2*pjsCshu~kIfFG=Cv!PS{Qh7z;l$!%5XzF zcFKzXdh3t?u2iX0Z;UrZyOEskGv}N%tBuq9+9Mp+KD9(+$=S%Yv+~s=duo6 zePV|rBFDwwUTDQ{i3#1=7x}9Lm~9XyBvunsSJxlJU$GPzxY;$PC~o+*D#BJuWF;xF z6T#+MZ#Bvm)96v}m*XA%YCC^sy84pVgxZ9uI?0BmS+WbQZYI`+&48hYB>#8N0y*9w zO>K`Xz`XTGnE+FQkQ^JbnvJyFXGG6kHRxNbl30oK|l}H@4`B6ch9-r!m>c` zb-mu$zKek!BaI=xgcWji7n zOI*$tNMJnaxUpyutZi2`aB+r{jR4Rf|BP%1lG$7J1&O2rm$`5XS{>zjPMEfjo3{F| z=zq!w<$tcx{q0MKe6h*-ubSU1zv_985$@*Oto^FAW%|Sffgh+ek~%>oTvaFoyB&{$zmPz_-P6+*@8SuM8(VS@zEd z1B1XbgX4gUQ~EGV4Vnl5lG#;IO|;VUB6IMvq%nz#RxJ9!mI{bOQDss94Fm6IEOrvK zNEogKCO6N<&%v6&XEB>y?VHQ8a=-2xF&HXGUp9&*cU7wVq+h)x-?$6nMVp&*va}0AlWKHZ8?twJ5Zt+a?LqZ5YeplGCSvtn*|1drI6YuzYtnyhP_8qjgYqE4xTk zHJ>dhUC(D$oyjL}!KB3OGjmLxCguRyVo6C!Wc)LHf;VfR)r+C90u8}DpB*XKZySe5 zQvTJ)q~+z4L&~JRvPP%j7GSk+8rvzep>o~P=^-|I>gS3S{L0i*V zY+9jxq3++=xg_Qtn_$Q-JZXW*lAUV*`Y(N@o50S!@=1frTEC_)D@Vu${@T(BlGmp7>;FQ6OV_EbCK9tVHiGxKjTm9b*WVx*v+oGc zPz%M8PbDU`EJkuy=YkzDI{7fd(Mn_v4H4RASkjeJT|eqIUwGb_-?Z+?|-;dLSsR}Naa%K?Yl+X{0Jr+u85xf3r}Uh z7MXn!J6yUPg??vF5exflP$EgVtve!$H%}VJoo5z@;}Xv@v*1;e_S&3YHzlh|)#+lT z);R0&=oPE4Y@A60@8V99`ihMUYKBv`0Kc!-poW%b(8gtOF-miv(!tWt5tNdch?~pB zhjjFk>Q!su#H5ukw@*ys^^8(Rfuy13kKd6a_!N_e=_fXL!=JSRFiho(#aq)L;R=3@ z^rXsSsK_o%zQw6HKhzO5b-0I+MO?I^NtjM@T+2z%!zPCk-kG27nR4|3oDcq`;`aW{ zF~7h@mRl@ZW&}si5^2loDqGulYg>IYiRj}iO}eGH$&=>tpc&RT;{hBGrFlP3At)By+b4v(`*>&_b^lFvGz$N%7Nj;L+y68wC9;Y*9S$Rm8BKy(afba ztvJiTeH|QfVO>U-Iu6j%+c6J&9N=K6=>lAu>CO_E;py1K?47H({QL7&jr=0G`D{WD4_>&K53hH$G?r{1lbpB}l9My`8Jt@sDwxc?KC1jrp+`fvmU+?Fm* znRn7veV-26uh)qLWYO_$T4tDRySn`ywm@oqc8ftyvSCF}&XW<0-gKTMeR*E##$TV? ziY_k@oz_oHE-Q1t-G!LSnjNyqUC?zE8>SvDcTo_3_byFUn;oVEc9T(q%t4cWO5 zFI?}h?_Eac7$5#;4gdCb<|a2S%UmAJhc2L&Ho}P(qXbSqigs)DlRdooC>wN|{w_LU z%lu>HC?qej|3PiS+mo>Dh+ zno+P%$Fx&#Tv+Zv%urRD_Qg+Cbzmj7r3ncJvOV=EXVCPGwbWsO>WJN4%RTrHd$c!MyPM4_P+T3i7^gP#FIU3 zXEY<*@xqNJK-VqhI}=Xr*gwl>|0ELF#K$9X(TT3&qv8M44kUf@rj*)DU6u}BphE^O z_w2Q@)!<2cIx79IZbZxt{sM|^MlyrHf;ClUBZNgJ;JV|`UVr2CiyC#6?V_33C)}ww z!9@9HbG*e&d?j&ua}whcuKh<`*(pSE$KHj55gsic1sL(6L%N0EV*6()kAD(tm3OeV zvca^q$@b(72JUz9bcvIw!A%yMBnvB!u5K-{;lup15Y5!iT}z6bP()lVy-o!35VL)~ z1Hp`Rg#J?C?^6XJM(ioZM8ZfqE$x*yji8k}g}Ru-@ZjPgxsyrq2vQ}P=!CAUne*RU z6>6bc1N;gwJ(1^$hv$M%n&u`8l$eAuN=5*L2r^D7-GN2Ba@3xRufHJq$bldaYtk98 zUM}1dC%Q)ZBbe_*&(dKKPd?Ce-GmC`Z&zRY4_p-Rd4?p^s?8E5gG^rIB|c?AV8K1P zyrCy)U~K;conSP-D*{;otwR-7#NXV{sHOD;u9?FJ{dTeeI8<;*Q-No}tqY>X>d z?@4jhD*`0<{hnT=t8OQP^B1-@8E%u-1l@KT!I1H0_J3<${s<8p3W!#ajsZYeFz)^YDNMghrYS4!{rEb<9z%!JsY4F;dmu=^zt3tR` z#qL_Nx}?#O(p6x$YuWiP%GbWh-#8GFN|?DF3xRk2Izt&@aeZzMxkx~G9?O$!X=Co9 zvXix=(FFqnfwOb{fkcAZw~54`;KWVTWSbH`u9>LgK$YiCI8W%=4uO#{EeLRvCtaY{ zL&wnSdJo9xwi&6F)TF7j4#C0~S3)A)*nRI}HAv3h80agJSI^J{QlrJLBi}W10Xj$C zh?V+%iU}mw*`kAV308kVV!(CYmuuW~Dba;$-4594~by~bIH_T_$Q$n z8>J5RKci;br5*-sfTcPOp*Y^*zhSjAciy1Bd7})|3m1sS{1pb_QhNYih~I__AvyqdxLO4bAirR2yb_2k}ppF$|u5a%N6I>RI^q|Wz{^B>Dyn-vrG{vPxVfUKt*4Bq(n6|*AvxL4t#i(!e3P7O`Ubqmt&PDg9S zt1C9CX{L^Lpgs#y)#8jhC|)ION%s(i}Su~IY^M;F#5cJZM=w{UfbutT+ zs^{%XsV5>6uqDUp*2K_^@W5A@vBd{~l?P&|{lDZoI5)B2U58|o;29-v`%4}#!8kk? z6AJKB3;&ro5=qZ`!jM`<2gqiox4P6VEHbsyq5GaGA-nlvlD3}g;2J}+_K6-dIZqZ( zu>Q`W)XbREg2f|rM6A#y)DeD|;%!E*p-B&i1mSOrFS${he80kq)hfHVfK;x8w;z?O zwn5{Mp1h*2$vAJBus-gx&-{rA%=HiB>MOoHKk@4RMsgD?l*|L6u1syVxAAuQap(<2 z8XF(N{3g}5V5~M`3qq};B`oYq4I}M;c?F+DEC$CaG!9Qrc403}YGiYjR!wH;Ht=q$ zMEz%ug(mq+YlAVVtFz7i%~?T_31db#MV69UdP2K6dhuefNmaQJXA$W+YOIorB`u)- zB0rb0y#B^m9ddLxK9AlvxKO_yGc*5-@xh&0@L6MnCN=IWP$zBE(Zw?X;AiQdY;WJp z4e`8pR~g?T!AdF#y)l7-e$vlcq7ksZ#9RUHAq`XCNhs+cdVlo5ZOrndGpn2G!i63+ z@W3frPjTwh8srsm^)I~-zOpCYr=T$-v?r*?n;T+*u00a^Y?XIy1ippGbCRt!q9cfU zrX*OaNFv1QVgw%_hRIQ&slKg4=-KUQ+Gll>f2R7UC!b^eXoFWK&I@_X!+v8Pla0&O zE)73S?~ZkA5sH>IQYpx;wpUwDW-r_amlhX$d$VznPNp(y5x-VchI^a1?ua(q2YOi8 z@wfNNwh~L!UizG{$9iFpI1X-f;1`w2>ToD+(OlMFg6E%~OHiRqjCju0G)^Ggc ztorWag4<65^wd|T>S&6$pd}bSQ&^-&q$v0*N%h5 zER{dN&on$1QbeUE;L5iCd7{uCkl}-zX?6GZ!o5v#o^mF$3rZE7Iptjz`#d$~OfiXf z?sbU^%&-G8U)96{;!0ejOix5Y1698cAoJ3~%u~lXH!nm0sjdZS#ED$Zs)d?Wt0KjQZaFi8^kN zA9Q<03I@Ug=5n9!L9^yQr7JSnu`TjI?>Onlu)?MPa*xLIk7umWT=OhhU-502?o;9c z5K<#wC$&+me@mjIUUY#$heaxz@?P0No?RJXv6TyB4VxWL+COaOc&F<(m?J(fpdb5&)cwen7+FzR?+IRg|NGDc{=WBx?g=2ZfsbpF zHf*R%PN%_h5n8wz|MDy3bt_-T$DbHgaQzK|yU0vjkVW(wM&PCc5Br$PTWGlH06uqL z9(d7~!eA{|a}hVDy|Tr`laWzh*KR6lEP^Bi6*oiChX`uzgVBh-;Qy!3ik=a{{ARx( z)h#rQcCAKVE1~)KN+XI$T2bg>s=o6EU$|O}1VwS4SZ*`9n0ZUYt{=V#yh#ga(@4Ko zHeaszz;fF+v9T8x&R3|a$_w`Uv30d7{TL<+WWdo-?`iRKs3 z`SoqTI-1?9qIw;38A!{z_xJg`+iPx1F$kVrA+g#M$BB1=H?R{|cYuq_X~6|0g@$zG zwh+U>`dQh3X@qmz*HM7%u)o05PFVL$koOkFY3PS|$ofSb=%&QPUZ+DGcH~tP?>GUDjnpXlPfEy zdVX> zKj^nFPF&+rL$a~ETb$E006!q)$e{9hg|BRx0#4v%g<}g;^g#^}*4|LV%%zLyU=i)K z<7cYu>67w;ntIj6AC9e>*6B$y`yF|Qa^Xso!sxkKf}GM{JtFkWkH+%|kOw!a_;g@l z??jS;tjIw+tH|(OKlNb`D2sl*h-swb)AZ7x_05$ndE}?RyOc^D1kwy?E#}R<3@7xu z8Aa5?Q`6*OopZXmy9}%KvRW!CGVDo-jP6%bw%X~pAN@$+T&Z$&HrL*_OjuD(C|;ej zf%qKmpXB~?_Ffb-PgCxdUjd(|GcIJpIpN@)ikKu%FB0RxWbMXu_1*QBtWXw7Moi4H z{)~S{cY#zeuj1_62QCaG1Ovb%fCcLtOxKl(9E9vNw^x|Xak$99P~;AhDTjeX{e>tS zxuvugu^er^(B3Pkd3btm>jut;(L8>lA8U5#95ezOb=wvwLcwXVp|uJpZSsVhbrL?J zg{;A0DbOXVXjJ4cUEqh(apMr46HM*QL3U9BOp&^wnmy7gLuuPi^h%a`VsEG69XM!; zMGSD#8>|PT1pPi5MnU>9=Aridp`?at-o9`V-{QK06L(zhNa9As%%p2^^^1@_WI#Cj zf$8f_*XGfe2$gDHu7_e$XkNn~rgx7$Omfy6XxdTK0xuo{Xsk@TP%fs;A8@()(u=m$ z5q6Ng&3jATB{kZFnz-x1gc*w6z#m)jqmm<8gkyjBy2ts8e*rg0p8R@Me@`77ptdqy z$F}Wjra)deGE>)gC*~|3NUv!BcJ@#>3nZX{_fo)B=BvEk{A?0lK(~t9%`TORbq9w2 zBYjb|Q__20D$R)9a_T)|ViTkYC(whgzZi@nIV{AnnBwva%&S;uR2mxD(K8koo{|a_ zkLk|cxV793Zvog@Bu#6JeZg8ZO>dPgA)=$+x6f}|H3TChfn_tDyILXjfNYY0W0tIEHic#qc9?Hbb5d+&ClBH`%d%@vl zPw01#O(~qaPIq+h#adviS&Z&-@K~lh_rMwG9C?n$#ho7i_-9Nd5A;!R5MRbU_SF5t zqC{Tn41B?A!pu+5RkX;Pj&Omh=8CBef#A3@_-~##r~Jyxd8+&#UMU+AC?4Ap)|YEf%bF`J*Y7XmITZfEQD z8Jmg9BkhDN%ar+O+(ZPOydQ5X3uU@qZCY&SaumNbfDGwH71wE!og=}g9u4q{H1W%S zAS~(r!q@=jpwmJ@(^ZQ4kp+6Th{%fE{=%-g zta3=glZw6~Y{J6nR_Sg?@}PyteTnnd?fCe3IOP`Q4<#TRZX>&S2XbQI=mF~vtehBx zA@CynQm2SE#x`&eorc`qXf7*(vv?Qg;A{RZs$($IAT1{&F=&pYm3!Xn3r&pkfJ zp{FO{W@orJHODsi{t=ak?V0(Qt_SWL?$K`EjZZ8K1)Za|DXtE=HR@5N$(YA3Hu1tg zs)9^{V)Bh#jyE-!-P{mS9W@tKwXCn$cci6fa>T}sEj-rg{}%O+2R%)lm&F})P9P2s zh&T;x&L0~}16LhG)g(-?7G?3xCyoh@tR5+4>Vs(Om2#akax|nq$Heiypx&OI1WmP& zvx{*=JpEeS3V+NQeBC;5+;S@owvucb!{nXCYJpa;NGv+@Yq}p;(k%KTb#2@T9%&d@ z+&T|)_7wvq!%u2-0md!Ztr{>{Q@`(o)Qis?COBX*uH(-eXN<{Hd3llcgG38IhC`nE zOj(hHhjW=YQRuI%Jh%wDh0~h!#k!Ck?d?MvL&Q*66_P|T;mb9#yZSgY((Cqek<=Sm zH48=vp>fg%RG}r^bKwWBjY4Q?=@T}|{aTn>=du5Swz(-1i#v=o_+W%%9Q{!{?%+Witd2@zc~>$*rgu-*egQ z1XwP8t)7y#NeFMUNgPKmpl0#5w`xet~U>fw9dQ~&0FwxYFAzWJ!HN= zM0xG9UZ4M8&+_o!NB;Kp^`$k;=h2wAjSsUQ;x&(?89;)+^YI_!&ki?kAr%;+5)!Wi zHw+-qf-|`-djHU?e-3~?J3AvE_4c=pKV57_+3pTXKl6i@Z|w=p0v)>wTt;1xGB$Qu z`FUbB05d$`M)%HlK&DdtF6ThEq&hl`UfCqwB3y6sg{L#jysl7qPMEzThBZXR2Q~Ys5 zP6+;ayE&GNs#P^T`zXhgw?_m$VM&`I|zUm&*1Y~ z>3%mb!W0G$3~b8)1nqsgI&pV5Wa@sp;cq-2jy<%+lFmeNqr%IN$N?HTY$r5$v@^L+ z(GmD-4c^%ip2_&Jbpy#Ij^KUlF^k=$RNm>sE(eZ}WNIBw;J*U>fc)uHhY0j|PYDqJ zESyK`a9W^|IT1!eigtWAyMV6D0AP?$qTg#C%Rw@5js;nGUNp}A!Cp!(vYDr&c592> z$=OQRD=hKSK&9(gp#)cepB7_enQV=VaBD4H1==(Ete~~y7p4*_18+;vD%w58-#6@T zZ^eX=lw$-i(?W*6(iS2Y26N`t!r!M)E1Oh5h4=hF@093HD>LV|a(wrI5R9M^S9c#s zqlWH}6D=%+&DdB~n>=#Rgo*V*>tZU`Mkmx?;VrmBA$E{Xy?#%GqDaw1RxoCo;99Jc zCzn~IR!D~*Kg;0yjOO;t*$`T3t{SNC7gSo1?^q9yZ|M+k(JP;f5gS_CD6%58g7a;KWEcHPHlluc5ipC|fx=|7mXma*DybiDOFOS-km}xcL8FNq|GH`THtt zbsVYPUsj@ZkKg;|E_7~2{#WPa*st7rd4G8O+|q{#`aI~~Dm0A=CWT1~?67@XXS^&_ zI^madu26lkm5xjuzp@{A2&%Lm$n9IX9z%2albWbh`T>$P5DiZ*Q^C;7c@8|NfC3a( zAj0{wq%({sv%1T{Hb!^0X|t4sqPuuC<<0FPZcr1a{x zHfEVw6y3ntgcz>k^pwH%;`70c?^R_LhJcI%+(yTwcbwPk3vgOUMoPgDj?2AKpnTPY zCKxmi2Ve_|K6a+lhh%l*krdfNj%kTQJmci(>v8rNA;30G@T0O|(rC2XEsIdJ6WK%l zEXVjH#rEtVTZa6*6Ji5d@OjUmAFKa;nG(2QG;H7Xl}8b?Q=rigOaVQ{YfEKO4CGv_ zMYV5s>C&#h$nFmnSEnAZ3u*?gCEH0C9Y73?HqOR7(UCg`yW4)R)236*%wfmz_FwZh zioby4>+6b(ZSAWKVSC{lbo<#4^lsKK6&FV~HLwO^ZNlw1&-28fM0|^W5t_CT#D5M( zqH0?&bB5Z(suhTnH#x7K$H zId5q98xhO0#Mpf@O+kG68hYN=eh%w1(MpO|v6$@>SQquEsaimsW$$C^`nVi5-A#cT)q*bx^22Xy(X^Fzo1ntp>4>FfJM(Zpf6L-T?_I33fiXG? za$dt?@V_56Nlxwgd(1~}#->C#q_nW9R(5yU(O=P= z0QwoQ0aE-k^k=*KHtgyvi87GhWBB_vlqPp*EjGGYj3goae{krCutX1?51Wn)8`})5 zUn@~tHXV02*g9(EfZXRuesP2^djR9>R@hu$7w)h5WmYl}Z4ClAsv`a5O5L`0ugkD} zbu>4c8UUu$n#%lH(N*DB|H^GgG>Z@#(;nD(WCf8bgxHYSlq0m=u9ncaoF7^PvJ9PU z{FsvbkBcMLi!%yY0(s0A(YLXj%_WN$giC{z>}J7Ef2-oqMhCF;j4x9LgFzt-0RK$q zpL}XQg___hla#mYwyj^lq} zS8(Pqjt>Q02n5Uj1Aah(zyJHkA07Fr`wn-@@BZ0a_WJy~{r%@Bq%V_})oh-kNAeD6 zr@qnIAtBzcJMPF9pSmx+Flj*&9kMVT5TbvV&9}HsH(a*-N7_jkOI2MFR7V!MaCO=CGrm51c~73WzelcI zo8arnzI>3GaeGPOq(aCzO<>)qtgVBwvIs{`N7;TPBE6{~)SNxLf%eyh%x;Jxsf1-L z>+Rx}a`Uv{1kIVNOBgE@90IX|F%(_=0=KP)v3QSUIz@+-XbtTJrxkdNIKe=ZaV`Xn z9z_H^nCcfNkVz{f}wI&F~7*@2!kW)0i&*j@c{{{zQmYiR=2 z%#IB0?-JkSJe_IHVz_1cIuj7Rt(-nLuko8QHrVdCZ?g++9KhylVr~)E{huV1>L`c*?$z&?%)TI`=3BThIZ_a5r92LT~Qd(+OM9Zs=1G|(>LP|&m`Bj(6Z zCtZ!-AV-teF0IQKUzwMev6EkzUN-usPk8eV1*E(=j=90MNe>>CU_2$O6IY~nxKGm1 zTP|K%Hk#U2Yp`?6Z_C(lC*yX7=*S`-fqZ{*%|UM`7INCHXtbnbZH4o_2~v19=cDA9 z5Y%+!HNB}2HVMWoVq~yg01#l22g2(Rr1etxcz#ERl5*zqGWTU|2ec82JzXEp(HJM( zwZGk}iK+H^yA))!zU2B=9iAOu(psk<3J{VF3KL8Td8{pp-ypz(>VMS+4DO=tD^(lp zL|@W;Sqv5UaM4{Q3Jr=>D3*d#=;CW3Yl>TdBfF4XsD2cspa0*!EE^j|*ZWQl!VfG- zM>+|`=>KQ$y@MoA&-=dj?U|mQ&T+b@C+E$(cY9$ExC4koCWydTHf55ws#N7aNSdNu zrIJddEzp!{Nw#HCvShy zbZe7jo!zsm#oQhz&vIzP~#j{OFw_R`gL zbNbX60BMXBJwcezN9gzr<}ZF6B}!7BqN21=^c=<7(3Q0e+FuG!kPeJug|cD@WRwPu zf*m(2+J&)`BYoYImt)XgCIDxsqZG(}6detMm2zGRC&L6G$$IlH%v8sz&(*yz-1xz2Epr|j>DeE=y2YOVvFCCLx13AZWYMX;T(}p$^w5`UTy96a? zC>N$s9-3$8YJr=akKg*pHwaOWq3-BIEplj15%xZikGf-c6b=@X_U6qMli?igVI`I? z_L&|m$NuH-eZzg|<1d(8CTV~8(UWEfIh-VXdhL-gWNSud9HZ^!LZO~qFrugAj$DH_d;}^uJwdJM^N)!aO7Jrfba1Ay9Mw&JYX;9m#0#} zbDd7t|6Q)_=STZfE|ZM1!~*O9b-d@^<3`%qTz?KTgChe1DeysSEQ&|KoIX7YbtFS! zI-&sf+JU}=o0;ra(QuI%7E!c3jHY%6N>7KYeo9JoeegZD03}ojP^K zL-Ocj}3{nbRluzP6bp*eDvy>r zo*8D8=C3||*w}k3hxG>3o`sk5x_mwV?7Ic_0H2yih=p{ zssIi1uD6}1Yp}MmzGg~r);0k`P9N*Hu|P|%Eb#SOV3_O#*Q&NR7WbJMZHm%Ct3!wo zq2V-1yNrqxbW_9q8E7KLM9jW#JJ6pb36E~G z<=i%Z%hYfNBtL2@yH$Q>+#d3P0jak8@D>};eWV!KY6SpN4mk%|yM=E0>~xZ{mIU(8&T+lMMD* z+X@A-S&kFjK>N9i8=a+%vi-=XF4+`xnv-K$FSQfSa=`yxI8`vu-Kx{2$rrH4EU)dD z6j>ygt=f-#{w5TnP4iPfc#b^fsO<#0TrWRk6f|4mOLMn3pbPPu2zoj-k>Y0rUA?{^ z;Iz>{(xvk-Y1t5+Hqt+TPP^Srti;d`yW4A3vkutw^tBE5jhjNtJKbcDyyKtz(_gdi zeD{05eB}zkRCnKA^bxpQ0AKW>dg;FqN?tk@PyKeRH}vxe{sVmwebKW**zK~zL_1a` z_oAENrZB5zloj=WnLaUP+c*yQ#uOQuv~!#LrT}@DDr7=fLUI4w1J`3J)jb=-;t78& zv$wNtigZ%Y&2V}_ugiFMcnC3hXHP|d8#6&61GQyT+`zj1<6oP%M|4=Va@oRtlYGoJ zu!nm3$~wh%+RkE#S}oz~oKM=tMVwLingiN5($CBnS@I`Qp8x~HfrGP$=|B@dKe{!D z(EC{3_sS`nT5PRcSU@+C&r$Sf&xSE;rS2(6$gGy?9yz8X=Ic?GNOw$ygH9uP+<#pB z?lgY>^P;}c`1!6gO>nu!d|wgxKkZEcU2tAuF-XTOGy6x*0Lm=+_yb#tp8uI}!)=`Rl^6@$Q8y~x92C=^S z+aI`3qrJ@(ax5*6Fjws+FfTzUeA(_b_9&Kd^U!@GB;)Pb?af_|1E#Wcx;K{h>?UaV z=GLy+*{Ra02JPuHqZIQl_qMZXimwfzGwYc->rAseN=Zmq= z?(TAd+SHLW7G;BN(b?g#tCe$$`v_Y|;;56IBKPnG6ZZS-NxZ5(%FH}GxdofuUq@qMj zG?6sD@r>yk95Q2=sXp3qcd9tYtdzZZ0nRbDOhMGXPn|x<2gf#9ruqaUX;J_fCm8F#!OHa`4GF~cJ|hW9XUB^CZ|Wu+-!-zwT1qxn6%CQ>X3*=F z%{Zjt6DLM3hSFx{p3{!yJYCEd1+ByTCCpKR<`OrxIxo;>3Ru)!#~xor4j@N6JGX-x z1X>3-XqR97Q>-9mA3$$mH|X0l3tAjgU^zvj;qB>ft?oh`4zLF;^ZomGIDg+HIe+t% z$UNqoxUipoeesJ^K?r{0k2h9ktBkR3b&c)r6wc3l9G&r@UD~L2K&o-@bMD6Sow~A3hc=N5*oB2n_eYQ2@(FkEf76tE z+x=sb@bMDqWeJN99W`;o#%llUn0@M*IoIewZ=u)|hU8T_*RS6;Bbi?J$Z5#eU1+x7 za@LFj&R|FDu=K}O8@>*zp8g_!g&uxSq4Wo6noDJ}RBD8@?>Aw=(}f?o&ctA9 zG!hEFXMUsl94}(wWw?A_S3mM@0sP*c&Oot{{?*=K@P9+rl`Q5G9adC+#GZqu9?jN1tIPuFiO*(r#>D{E!Dvc6@_V%wbmG!3mANlCbq>V@{S@6eae3Wx7zR2H!*R{MK>z&Z3V# zO#c(;L379d-+2?=1)DI_kKZAyI!_xBP(oeBaYdbBkI(ubAe{#UM~*K-S%Qu@V!!ml zI>m28k(u^K9va5AT5*{H_LxC;?@V6%M?;*b9nQtgeaCVrL5!tXyFY$n$rre{-#g4M zFoyZ70E`ZK$|2Buhh-Luo4pQTP{*vUmhHK-!zf|)&CR)WM(8egtiRVDACB1LCyMMu zd+ZLX8zDqTHtc1F$4-t={{To>Gmuq)@x+$F3)al>aKg}n*Wv6lv%3brSOcbF=azQu zcz+@TVE*IP{lm|3JXikw_}u;Xw;zGK1@POC$;dea`JyQExysA-Eg`n%&(hp-;s( z54V=#;UaFx+;DVKAy8CeAW{B85N?5zyY>qEbjP%7~|b@*H_R8H(ZUK;si#jRSLDZetNwH zLdlo5s@~-#hVY-Yn+H1h(V(|JzS1cbPE-=9H=xN-HZ_X33>n58i@M(-N6rMOthT^C{0>kEbQaC8j2V`84(xs7as5uZwkApp zI$%)a&pf~3vrT^Z!Z53WBYPWThA^Em&e{R2su*R7xTI@Su|7h9l7zGtbA3ch#!NPw01d9uNkjw^p!#o^PP1(Cxg;nm z8d2o?H)mIEt+wY*P4{E)*@M$dU0{YsdiThr=qCK=_N`TJ+CvNuB34_nE%Ibv6Mkl_&H^MvE5KV;OZC;*YGi^%_`r~A!gPcOP_AnX6- zyQge|?k_cR!lv_i^Tkhp-DMJV;H)re9D~wM)so4=-WD*2^SOi!avi$P6W6xQXCA+W zQAE`O5;>*ZX@`1;RE?bje`6gDIND*asHrs%^8bVF{ut_)yV=8cE}3up|GNe7+YZS~ z`vHKBcja^0k7g3l)3RWCGp-^xp+U%jSW%AqvV9_kGKCtz27+dKW(;Xn!j`x2=1V2m z(E%p38m1eLSuh;4D+_B5P=Px``LrF^1rn*f;Wiw+L?(+`W0a0>&+KkPeF3DX)fj1m zJw)|@Co?V;NMobn@m&nuWpiw@NDU_X67*M&C6bF#41KgDBda-v_un--mi1lI-sm4& zps{<6CN}U;W8iIEnqBvx%@Oi4h5q0K4dtUpVd(G!tKJez*7ei@)kcB8h#EQ~F{3X= zF~KfN2r?$BN{N zEtw}St++2fyJ0Ucmff>A*LbMZLDdgH%^`WqRR_6T9y3|J-V{a(M*vRjOy!FxAS%&3 zIQSxy#r2o=s($_aw7UiHWjvRuMC|8JPLKa~UnbdiY`oY$Gd*Az>6oNbogs|8!~#16 zon>-jl%3FuJ9Bz6fPVn3(5efA^vQNf!Ucut$R(uH6?W4jNxb+P4tFW*dPT4k)SP?8+b z0V$lkiKSJK?65F4rHh5>hO)&8(Uk;FyFQEo{Lez^@k;;@zw*Uzj=I#f$0>NHD4r)d zOFz-hS_0)ScAm#LjJ|SyP|c1MWW|W-q=$ZwXU#$ z3lsc-tF<5FGnu$~`Wol+c;0>kH2iBQcL+j#~|T z%BjI*Ku+468~RcB*@s6lj_F7FQ&;bZ6^L=6*D#H;s6Z<`HXnIJTCGnIdOqy66 z(jczupt){rtk2xII&V&&8B&;$=>p{~4CJ`c8QV5XOk#&tMx%wkr2XtyZ<#y}GPpuR zCuw2n6!jMI0#aydaFuCeXOHz1Ts0LIVl(+MfGMaD_<>f61Qi0A5v(IeJC16h$?PmX z9S$w*2g{J3o9al^DVZGB@cgVh{TgLv7Q(frQLWWpPdh$!f*PNN_TnrpGX@8p#qaj?ph-ao z+U7x{@2LPpC{8m+XSh{so9CBm_B6_ix1Y(H7jEx5g{BJy=kM2O4W`u5K9b`OIt?K7 zdtaF4-%`0m&HYmuJA({P*7r%s(9~PM51bkACp4W@J03ufJ+;Wsa^}J5ggG{vu=A_P z?^nOcCApK0$mOjjgf2hxmVB#o^uZ4x>jGlxwGyX`KsMCi`S`nGB8mh532 zH3H-F9I`FOY4ob=+-w;kuaKdUd|^HyO2*Q56KCfng{R}1@cj1R#L1yK1n7c3n9(f? zP%JJX1ZEb9+(dRwscgG@zN3=yXo&(gGk^+&Vz;tYrz2|9?M1N6+L2zFU0j>BS^y^& z9Hl8VN{YL=T9$ha$!OiEH;(unps}%Dr@bFeE;2fJo@V90f_3xBzwG!^N~^GE7HOV- zoZ_c%*X!Y3Z1gEoDm>!a>E}@%J*O{4!BSDwZcv?}3ZeSOCVM#*Csl?oUfVUt)#SyDefg9d7|P3c5Go5g>`Ix!?cCDU>*k=k%SvI_eChJ0 z`6vI^6Bel->x-b#{`5_!ixjk8I@1FraRf23lMJx3SSEHrZZV<=u{LnZH7*0tj6-Ti zUMEG4E2NR{fd2E(8|!=fBZ*Y|b9mbVuit(9^CNJ#0RH)rcnSW^w*H+H;{$*6;62CB z?CexJyF0tMBYCQdA)(Ejni_zsmqI~-wv!?_IX=YR>`nj=-5T5eBMM`e4P$53m+W%& z-5pcx&)AWHwE4;xFFGZeDGVHHo(s13k#2Qc)>JN*$(W;H65IBm&C>D6v3(uOC^2I| zBi#|8#-hCR`1xxayWAZ7&K43i3KF|An})x~M2IviPA+bcUep0bR2eC|kZ#dx(QY9n zLpR$Hg~gqo9xw^`i3hC%+t2H!keggO=EKmrK?xc{`H(;jLaC97fp%_$7KwJ^wDVMv zm7xKui!XJxI`8Fh|(&gbDMik1qKzLJvE#_cE=ik0{WF} zC3kUdHy{_8ciuB#rUnwIM9S18K69y zFsW_XgGSZdxIAkDAVXI!UL$F)+kEH~m))=b(ew7x-&i!GObU~n@AH5rWBq9;J-ti; zA%K=%9B7iZ232Suw4AOJ4xnt(0vJae_;>(-DEsv*8ysH(pSVsK=aE2mqqkNujGf=# zvkzjw{{=vist(58H_qDKgYeQE0SL|iZLz=axBk|Tye+r5u7R@S+@k6u@CIC$k-T*iHl@Ec!-tejawULp8@sw z$q5^E6=uNhKsQVOUZigu>t$OUA2VIys0E;~0&mc*U}ms|?i3`%bM}y@ zi3%gDEN((P{*I31I|q=`v|WsTEg~VKNIcd^mxD0v$n-^>z+Mog2pPN-2}VK%lv5nj zi3kOw$6ma?;*C5t*0Xr0+v0bF{?LVKXGllWU=J8(#|T2rKNMAs;|r$M{z0(Q%@=V7 z9ChR}b@OLv=6WcEj;>I^iWSv_dI%AljuIM=iX$Vd>X~goQQ^b<8&nwON0Pr~lVdSo zYPpGAj~#+{r{afSpaYqP9NlX!lbDB_4LeIXcYNP+sbwC&Ub0y_`THlcsA>R_SSyNV zL{1BZ7d(otQM9H*3jOul+dK>sNY$b?3CQx6hlc6kx8WW3m{A2VE$+~QVfV%H{W|0G zhdzDX{JY0*vlk7Shffci<6{LJX42}~YzDu;lcxr4unp+JFRie<+=eJV+8<*N*})kG zaEz8%>k1k`0au&~EjPEe z2OT9#;cA(G-~=|(fIG4J2C#Z&m9|K_kU;(nKNV# zFj&aYDTTQyNFamL7Xfu;>d{_`G(^I97=?TJn1iy?hjO8(RWhq{x6Il~i6?&q#OGcn;vG=Y}*uK`V_!LBAc}hq-cj7Zg`5-PO0M1hJqZ{*(+fi)w<*OGC5XYKXhn< zVNl>ex90&a^0Ng(Xmk~6I-QJ>8h41#UK_K7IGw@)6q_~zu5O<%!XE^8kWP^T$)3(E zZ`U18F1!x9*O7iYIg6n}U3UEZT!MxOHy6D{NQKNxAv&W|ZCGV^u&txs;ttK<0$t|A za3KE?ho1ZN#lKOmpP<>&_dH4+m0|_rHSh37!z&64^c3SFpuH&dC|C|;lo!Kyuv+lM zWQwAhDTICD6y=4T9tH5we?CI7#A!w(Y-D)d#CtX|C_uBE4h!%Jh;S^2Y918uGs6&` z59oqrM!PT0A#|V~mjQU9OEYbd+%I2Tr|ZiQaoTw1%_liW{ zfKr5&7%K>^971M-ahy~+4~}W?xdwaSeGgB8{s;Rwmmi>5z5rT0`{K{Q>+dV?7QnyZ zIb|pI=4d4GhxeSEeDhn*Pr6H2X3^Ky?J_$yMpMFdPy%qnYay@2Irk+`@(Pia6?Y2u zKX)o+h0b7qPZSbkkD77`v0b5vNkWK~Zno1EaV{7Q$fmxzF^iTrY?qfyu770EZYj$) z6y`=`(G3#3iSRy%X@W9>#*a;+D1pP4E{IErT_F25j8GY=EM0@ERO&3t(`F8UA_Ci1 zfCXr&c58NhXW!vd=eZ{A0^M2(9wFm+i_UIyw{CUQsOl8nrj=oHh{Yefx+OF^XjUxCZ8k`D0?gBM0%+I8*LMBJ) z0T|)AfhvYIGPppu=+* zLf~Bp{U>CR#zCdQD-d5lIc)29Qp;UePY+3PQHl+rN_1go?in`~TwKbPhER1396IVM zoj8<2#a(HfCQS#4X|iy-~t@Fdm>&S=-)YmlX6;IBfFJ`Jz3*~ zI35&H$PA1Qm?tmJxni=*+<;~y^gA5xb@MAb$N&$`zyJDe9Y>Pn;>3KW?Xi(QG}H&4 z9Bu+3)+GrjL%sHm_aVuvW0p#0vJ3Rk0n*j-pizsvf|fWz=c$oD677{9Sfj9VjX53G zb1vBdx)Na55*)>Nl<&t!i^;=vv9uH*^j1jR)%rmQ4(EGmrEjvjxbhbV`1|Ocy9Mw& zIN4KZ>N|zY!Zb> z8Mol17w8|kg~e^al_RsTylsb6XdybQ-RShDEyf8Z1lJE!Rk|T#zj1LEK48k5r?Q)~ zqr(Mm$Pi>`6djP5hx!vZ5K}Olbf2)x&^&nBiU_&eWqkN3YzhjJ&KE;db$mYL=#H&D zH@|NdDYhV{0u87w6wk1BRd$yyXdS(?2*j$CN1gUBFtL8}J2>Qj)t7Elhd)B2yhFWx zw?=Hei2&{Y$FJ0`P@^ILvshtq9w2b5ePZ+$ZHB@oQwH?HifN)Fq&8CL%q*#JlFbSA zr63aa-uCnp?PMi8V?AhDYuNjb=iLJ6{&Tm>X0ShHhVwD^9AQqGSill;XK$eamX=p2 z?lq9|j}t7PBq06VWC5cHA@04VOwsD%+Ud_FY=Hs_dD5m`5eLkXz@_u z9P&^a?2Ec+BoxB<^*w#5@NMLNFZ@*p{Pp;`y9Mw?o>c(X(OOsV!xJNeKRGhc=Y|Vu z$i)}{5PqX&`a`D%L=I-r4W}#$#R6ZvD*H05tQ36hLOj)TeR8wm~JkiKQeVjpaF8Hb3@izb8;fk~caIs4ijp4Z}E9W}NI+p$P&;E7C z{Ppzty9Mw?oJ}UeAIN6=K749s?17V0L%JwC5RC2ZrA^wdBX`eC5ixv?wGzv7AcT>0 z(0uLUZFBM3x*5ViV0a9xq+)@45{?=p6lkgIwQW$`v?-2^fDj(=KKr^f#j!(ILdQXk zv6nAi#QcDf8Q>!lPr2HG3e}#vDcHa$L?AKCk%d5VXfVq~_CtZLpnK-py7DW$MwyNPGt)M&Ic(-p zbl}g&Zt2j|VRS>~sn{Y&3-#rGqto-wF1`S)F*56G8B4|c`vN!L&+uzVBYDZ+!~xu8 zB>gcuYG^Hxx#{Xy>rv3)2vXcv*yl+boMRWOR!=)Yxz@-4K_&+pp;h>@?D-aVS|sar zY(H8<;VY-uvtjX1wy_J;;7y9!bOOqYqCWSr_aggPDh^+C4*)(uYRO`eD^A<7W3)>E zDW7_3-f4uM8qDy$SwF6dJ|mYLjoGTm98Y_OI0UXW`TXI?&9ST2?Un1Ba6#$d*=cgI zu}%@=Fu=!RpwHbt9KM(fgBWLRimtLohZkhu$U};20pkZak=kD(*TVo~pQ0buYZjN$ zn_oO}WfiqZ)Sa6y2Jv2eA8E}Q_Qap#EK%}b|8I8-;9q@?;g^|TF68@ul67#ql#1;*ky;W>{!G>xN6!}Jdfm|fHs?58Zq zD|L{R2;^_ER2DMZfyw2w1WnYT$ZXL9#!PW^+{|Bl&Xra-O)Q;(Jc~wkV-s`ymfc?4 z1mVPJfP%EKfdYg4*ef?yoCvR=z;>CD?7&I-Am6bW$n3O-9cRsh zAFYD}is~Iym>Z7$p_%8<{2+%J+nATjTg$r6MXm?3bgZD=rDJD1hw!uoI*Bl|ZN|+#uQLaQWh!8;p`1OPR3(WI0iRM3qtJ47e%-2&;92I`C^4 z6kNtO|M3^rFeo$f1>|#lFI}IGOP!zpiKiCX54Ol1$q{%?%qY7%*;RRfhU3;x(w|0H z;sq#F-R9!OMSK2%DV|+N*mDKp|8?+iY?=G->Bro5j}i!S;|slk9E}W)2M9Ukdx95yl(HzyKEmqa!w3$UBzWjA0!!JvC^?r^b07dIsat z(zUA?mxAsN4{&F7v-i915O|pW$r;zI)!6F<$c-rR5vUwd_KF1N%*`$+INY>xgYMw` zymy2Z6iSY+9;i3$_@adIY;2U+-x1W!YXp6yQzRRgJ6|lZ2ppa^|GqK121ojiOpVdl z4i+=)wQzmLu^CqvsYUE2wp=nyRR;^QXyK1JNVVL|5pK?=M^mY|bRBRGRYkZ#ak$q0 z4gQP!DLy23;+OR;@?Hl-xPy@@iFy>oQ&?Y7AFOo19a8oNQSeM*V$nyUT&Y3?Cf#k5e^76@SrYkf zEbq7+->V0R0;xXt{JNdR6=s?)oc*CdAT!>dhI(`WkQH>vZ?Xe)&a-e6Wo4$q`@)S) z5x1EpAa7HQ&z|TrUwd|)Zch#{(2^)t+dQoFePzGq2;ioJw)wiKl9mTDyFYdgoCb3tMn$>s|Al&r?o`9n==K_(}YnNj-8bO8Y6$jmP9;%k@`SaWWrT0awsB|fpI0O;58 z+uZ{Amz-R1iTiSy)Q8`2->G*#a{uwpL9^)|fBGiG)&%@HLbpK{8!V|cJ{AZv8Y!l1 zj_&aG{Fv?uipa`omSnf&)v4wN)_M0FuxHk@Xa}FlgfSsJH$9scW*kI@ncxte)}&u#4q`W z0I%z~tl)m^ew+@^qwlQmyuH45dED#T!9f(3S!}o2fWs#=l;S7Rg&%|0Du*r$ZAo+> z#RBm9HA<^C0x(291qKg9I^8y-HtsWU3}ul&QHZM8at!_Pkvx&z;(>Ym>K1B@w0YoI zfomt}$GO^gyDQ zkqUG+*2O})-Ew1jiOI->MfSF{JZBEOq9#2sW|rocY%JH$C@jMOBxE3i8gt;>QDE`u z!-m}+HeENJfkvY3)phnpZ3-DYF}QdEs!yrzId6uJ!bW3JlE&&9gIyQMO})NnWmd|} zhHQ+l-Km%eDvn*qy7C*8Dgz{t&aNr72P5v0V?PK00E&y*DrmE$bt3Qb54QGDF(i2d zO?4?MxOQ0B#)}6?$06Ded`Ct#6d8}xDE=J|FR;z`=8bxPg)T2k;krQo<40PJz$*5l z!^kcQ2WLl4Fk0jTPl0JkIYJvqbf)SZW(gDriamwg7E>X2iw;hEL{W!mx1rkvIo^lz zOSI42+;lYlzNm^A2tA1IPd%71Ph(RrrspH42el960LUr3*C*dyn%jX#$@|!xNtbv%>~UWW~a$y)a`H%6(FUR$l?A$ps$d2>~}H4wHUHO(ZLUL1aO8$pbK^*=>y1_ z7|AHnPP8MJ=V9HZAl5O(C<8``y40ky-dL=XHv>123OkFrLlieGG(&XXuRpLbMP5>?r1Pye{XjS;D0!$ArQY`K67mBpP!l@jUlRHF&!~0D_ed! zE-v3KA|QFl;}D#cQ!--^*5f>>%cQDRYE3iE1xUa)wnsZA8%8}*t6Im1I>y3!bF=0K z#|BNjEfog~`u^iMiQrB$FwDTWi;smQ6N?7L?a@`O1PB872e=z=R#T+gc}? ztS5rY3%?h#H5~+na^G=NlMV)7jh&Cw4SJoBqD4mL;;*6bK!ndtDB6p9WC6=d**BSfu1S zpJ)HiK-KvvKD_WB4)6EoJzmc5cpJefCzSZtL{YhzITi$PmUe?Q#98El7a_S zSQcJcuA&cys7&!x@RAG*^swykFu}G8gnS>w*%^wM9zw22C=(_UJYexg6A@5vvyM32_}ar2grFX6F}obS_v0laVr1@SR?R zNdPvWt-^~8L!l8@b%_A0G2FgWcnIo27>z`DXf^@2I+(o%;U~J`=DvF?0P<(CPTTq3 zoW<|mZ@F6l?;Pu)Xm9WDGw%MK(=#JlvEZ=>Eh31#s8RPSgPt~Oiuw^iN5sxh=)}~Q zc|eas*!N}h%_ z)jp#XRz_RvwpLn}t&}ZoY?!Y;al@QEHh{dh#%}0h82pJ+G zNwcE@?IT!xht7^5B|FEy5@#3FXf#bkqYi~i`>i^ev|5o-to*1d8+wHpap$uu;&a@2 zDr`-PrEVa$LG=UeWXPRaC8&t=5kN1V-g7m6W^JWOBNZcIYhg@Wa7)km2oRlRSpvUTP zab!UnXUxUfUBCVq&&P0R-qo&9=(o#*CND!c77d8CthvfLn!vC?YlkowlT0L(g`$T| zgn}rkTv6Z$Bx!LF-wYLn>j)dGI`k^q zPBw})hn!z>5L=_cw#{QhKZc>DA6rMJoyD9Wn&Q9t9qGcLUAl42BzoGQ%XNF}(lrph zIx@HrBz2Lalv*0v`81)K#I4uU6t&OJrX1fI?cgi zj?vrtq6MK=QaFDD@igb=MbHn@%WIc}&25${GD!8Kuv80D73)SsBkVg8jg^PAcM{u! zUW!~RCt?&2+bWRUH&($xp9dKDIc`O){9c(i+7#97FEbVXDK-Cysh6#q?c+6uBzu$Q zRs`e%MXreJ%eA#S2rfx(L}%{Sptj(gEtQ9K!>W>l*)Xom-U>|nLXFSt0gs3h70>|3 zktdT^n(#BOQT&n=#`~BOfkaBLe*Rj(QpKuf{@W21cEaYcGm3Uw& z_OzIb@bg<%j^Q$@&|XkgS)E_K6ZXXI>$97le#CWX0&oYD$mZHyPX`!rh%Temh|&V} zf|2W~_%g3{3@a{&xM4*q+`#s_}$zS9$fAcIFU69s$y#xhn>LChzU z=IjvT3HQL{NRGI|U6$j5eS1N0NA29r+c<>8(QWRzNFrrtriWY`A}r!iy!=A$#PM;L z%claNNP-Ws*E_^!ilW!3RcyR4M!5)>{NNB>-@3{3)Nj{H=BTn6xO{URHGj^e@_AX- z+gs#Gkc@6i+q=r-^%=l45p<(aQ7Bs56uc8>j@dObdh5F-F9##$(vTv@a(j}6yZHBWnZ7FaMLZKW=-~d0Chm8mLH9M9*Y^;F7e(FS ziO)&E5I*O*f4a2>KzCKJMv3`sutz&$4=VIq1(Uj95dXZkAh)hc^#5)+2Qc9oU`Td#V6f zM||gKj)>5nENYq`W;eXZ;d)-j?6v;({aOGA$m#g)bH_%1;r#KT5cHNd8r{GReEPEN z(FhYac`jmHb^TK^5sgAKK+DRto96Qi%UdRnrWA0&grf;lC>C6ciw@$c-P|Y{vM+3s zF(p^b(Ln^<;>|^QvAHtiM|T(+3qysnH9$8oI3Rj)YZblkRdzsao6HwXCZF>jg1hV( zlRTYwOFM377mgn)06eX1iZQ%YZ@WEE;h%lc002M$Nklzqp;aG zJ;Z2Iul(LlV1`g$brjun1uaHfg=(ufGC=e5+VmJ&(L1Mo6NzNV-73E0V+qfPx#{S7 zKz}{uLklz-2aQ(jyx)`B$`euRFX3Q(l|qB=f3P8vKwj!hI@5jFt;LqNR01{GEv zo8eQ9;0Vg`o`09_>Z0rq#H zFxi41NPP>`Fu-6Mh6&JP$EODk9trkne@~-5$$mJL!|9R`Qr#fRB2V|azEHsxys7EI zdIkh*ld;jfz~cAwnhya>zUnU>u8CLUIe#&)>HDz&J~TN}{C_XpbL=g!?cJ^0D{iDe zA@o)Y=ztu7B`WX6X$7&kIX98=g`{8nE#zIzT{14%x9sex;Dy}S6lSE+xZPqz#r`TF z4X2bDj1XRG7UgD%9Bq4IYdW7cSLbo+sFcmbu?e%ezUfxj)m5u>Q50Q`nn`T3F!S)C zL!$96Qfxt&=|s?!!g?*>Jc8?q5KDD{d_2THu&mH?Kkf+O6@Ve*0)0#cP&+$`<8>3F z+z|1Xa_4to>JC}?L5pED7Fe=@G)|5uFynk-gbZzy+^Czl$*8ERV+{mke4c; z2Qv#u-RuONg4|Ey%*;c>5%O+>RFHZ0f6@*MVV=NKP{&B&^L;2TM79>Tq8%LD1GL4M zBk=U7l24-CVSSEnQ^l6uq%6E?lliE4x{uSTRQ4=1yb{I{|=qd-thTwjML3^Exb5TY? zfn$(TsbqF?dKhDXh)MPLiznySaaobXKGRdz2O>=pv9@X4q16@FsTj=g8}w} zJigV{VKfSJzhTz4>n6ac;{ZbXvgCen<6?%uO@$tmpT*wKKcQKI%Si@);lBTz!px!9 z&=DRm0@YCpFajxH14VJWeWZfvRSo#qL6ilE;cAn@LVuHrp+-dA)cmP`NJsNa-|fZt za(_R?uf<^!6!L>H3J2H^of!UzAQv)f&0(slogXa@{6O}j>;Y+-Wv>r)2H6ayKxPU9 z8SIximv|cjAhdX(7&pHShmVKCSUwsM=pijGh&v#H;zE%VLN0CckP*qcfXM+wXeH+P z@e?~N3M(XSetkD?JtHKhUtb{6uP=j>ONgjXejSmiI{pfH561}D+T=k>HnUeCLC~I? zC*G55Vt6ngAj{pZpcE14fZl}ih4l@4 z&=`7R_+RFMdnN$xpw<8m?Q{NRuO&h?CcNr^(Zk2@?@Ps}zQ(veu#z|lo}x=nV0jDj*?phIM7 z&KO@~-y;r=@_SYEd{@HAEV8c*1yCC>{&pUtDf|kDYJks2DI%hlJ;7S=*XX2vm23Ys zaRcz0pQq`16Qk~!+?M$}kU^AxR%C<+{C~F zba>BFC`oeyd_%!8b$rA;MaIWI*CdyfnSfNiU#;17B#{&m&W0swM(5i{!><@l1+ZEFdY7TsW8#aqqy& zDuc4(`b`QC`$4)UPlA=$oC3xL)Ujjd&aq@B_$bLZlgZdj0TYD7hE0b%&|=aoq_Jh6 z+n`$`fE<#v+=cf`8`}ACRR#j%<9%j!Vaq~YFg+Bt{Tfa$>~7T2AxrX|T@ucgMpu3> zw40{MXGv-U=^G?O2Ma1XGx%>vt-Owv<+j|KX;@V7xCc<0FspiNdTtQWWEhDRu8AF+Sqe4 zR-kiq><{G%m?W<=0g zbKMQ*BCf?ai;&S=o-3KrLd2duI}A6Jy2hg$r3MDL$C(x?cAjX31_dpgOxYUzP5DID zQ1!6OHVn-uj8K*=ZhHgUSqkAeg?xNGD`el3u+xA*n-yiYfaRe;(nK_jQ}r5T@FOmu z$zoit-xR=Zdu%dC(In=82_+tn4bvgNi?QHI)IuwF4)bf{|6h*<(2si@{;s}s{2xF| z`PcWK7%lGa?RS>eN^%H>lhrnZj3Hv$Pfr(I77;qFlNl%H6^y{F;I14_B(W)v+cmna z2;U?7qLUAtGmk&LX3Okg;?Z75HUmrJE)YI6zwA=NP*$c-oiOSA0Nq`gZn0?QR(42G zOCm1{@FYL7QSv~Joj+kVRtP#GE;E6LTNT<%Hy(ab+?%i6+O*9h0?XO8M1tH5VS`?G z!zTGU++G;ajm=^`3>n(ZZ`3hYh|-i%Iw`DjfYHv6#lAfM#Ot#OfDJ607~LHl!77ab zN$ZU391ltDD5NB(L8cYmB?iqxG#Tl8!^}9?u-mnrz3UXz_i;RZcoDUdmJD>CP^KnXaF!NZFGE;l4o3;9bSbF zJp-9|c4Loj5gRbZUql!JQWzz}03vjB$nErV_&@JTt)Hh(8;+owaDWod1KTs9#(BC? zO&0nZJ)mhG3Ds=7p)480sV;MTI*;{L7kb}yJGVe0T`FeBpwR5#`^XA|haIaKaZ%~- zyW9JiFC@hg#D*>8rvY(Aw>M!jP`oy`*g+Gs8i(}!!u4&Oiqf_h%1e{I_2R^v_NW=7MmSI-SFo&B5v8{QIZwD9hjgDA_VaME-mt^cL8k z-5|VLiEVOVDWj0+;(d`=PuzRjlo>B_&(Kp=|GrG z+Et#^^(vzWKbtlspN-)?*uyTA z()E+wOuNSp+6_UjKQ`4b#}>E9bt^hkcc|AM@GzSk$s(gGo27*n_^Jv*VqAw!&?NB= zB0M_beeV*qNtA+ifu~QC4Z>)VEaWXVZ1&v!r#T3awUPV{V(M#J4clNfa=^8+4`|Y2 z9kT`46NCO#ZME#;iY60zFCI}OjaNZ2iA0#H-;e%#wbtB!P527F?g}8hx3_c$f`2$O zT72_hU&;~v=p;`Q`+p=KHRn$aTNzpI!+#pZr2}+~VaJ#Z3uky)1PBp41PwvxSCMh| zfFN=O&!h=9Dxtnm_;g<`OE7OQ_T^c#yI8BqHggZd1=(9cQDHxFhBg90tTJyDvQQ$BxEfN> z>UjL$CG=Ri5h{K>)xjw{Kh3fE!iy~`ueGn^q47dbp!+Knxcj*T`UOM?ID+`BDFGXL zx53PmM7Xq{CfGN!euKh=+_R87vITkFFwQ~3jRJa7jKlz#Y?xXvVNu0?X(YFIM84`fsMPq z;o96eN3|_BHZ3!c!-s(nxV*hjeR1sj92XiITz)omL*B2ORghgqZFX}Z7VjY_4i$92 zdnU>r68|?JbI3dcPmF@g&PlE<+=CtvVTY$ZjVhZ@ORF{aoUE>-Ic9grz6qmLR4Q8J zdCNFcz2i>mQ;9g`{;B8Cu1V zbpe6za*OscoN|e_7$|!%WRZN~9Z{yZi{vzBw0D$Zv+sH22g4j*`DaS~&15o3-l_PZv4@E;a4F8a1I_(amB9c!e zn-?Q9-Bc{-PEQXi_xN21B%L-Yt@^9ZqysaLeljW zGAxIZri@IC5!mN%sHIZID_EZiXL2lwYi5Th>HFSwLE{8ZbS{_xMF?*`UCcwnc}S;C zbI3?rEmz#wF;HTQc|l*F!EnS@;1+fhm5G@j1?j+S!MO|I^|p%20)qts7Z_|FBF<;_ z%TRph{BcwIZS zCe5H<`0MNa#aynnI)~2v0@RTI^X0toUS&U9xailwxjuvX=24!N*%B%VKp+$&huZsj zIt;*xH&hjX)D4O{PhKxiFy#>8A+2K$1Y9WOLdF-m>-WR8q`8NjOl?a>$PN* ziLG)}6lkRGTkj_*pfX71+Z{|CQ;0&y2H@&&eM}>rQ^8{A{?S+3Onp}#I^5*us0idG*p@C&y&@#I_oUmz@(@PswbNbv_bF^2%?PUn#Ok@bN zx4lbCPgl^cR}ZnHYMS}^6#}}A!09 zO3Q62Mf9`P(TSpif1B?*^nDaM#ax=D^^zytit-`{mnNJ(1sw^$;N)vRk17R|OaKo? zy$%cg{|?^UzkHPysPDMt$}Xm{ z?B^tv6Y|SM!RN_?fhQ>X45!BL0a<4dXJCW^yJ5P=Z3>_=jZ?T6-~t1H5XW{5M45-5 z(Bw2^{KZYOWdwXt6e*4lq?i-m0O)`U14y>hbS{$0*&!~%M;@9&8G;2_w~srp;}ErT z>9{Lz?b`YIb-0=UH`#(W%{_N^+_D@|CbjDdVVzbg;XafhI+X`HposW?Wct~}z$kLe z0sdA1KDLK(9B`_QHkmg7bLlu5W+s@$9l*K@Q&P}Bd_}z(=Wn5!qgtxD$-zuF1@OJ< zYI)#3#$nT;dsV%?mJ6UIN$bBSS?gb&J2`$|RlrXWvH)}g z81@x2GoCj`wOw3(Ti!Zoi(*8F#lB-dkY_ARLn=+@(l(qRGXa-gM6tFVBmsQ{w`Ru5 zg|(VUtvv0V85kddrc#Box98F*IHqUDF|O)ybb*2L+AX@Wu$jGeTZk>e5(EBXS2LVamJGk%8QnhO7UW(R#*?FqfNVt^}H7C8eRFc zA7h2wO35J(pSlNrxH=Z!A%=l9c}n|>4kv4w6?ivi$@r*|03*teU}tpLIb!+#i`33v ze^p(f-{TiUL%mK{ed*}P{bxK&yE()T@Jt1d^Ai-`&m+d7xNP!p)EKJ=mI%@mOCD(a zoAV>Rwj5_F`woIMN(`Wa5-SaTttM+ejs@4DBhLewWXQa*P-fZ&ZRT_AVcia6G}i*1 z9i|4#G@<>(4$F6MpZfZuId^I}@WyikE)Ij?>dg%}q)8X!p`PtUzZ`74S#0hLXv|k2 zJwJ18(~b=F$tlRr-MDSKP&o|`_j_=R2v2kMg_{Ji!vn0Fc+w=thd-FcY2X! zuB@n)b;XfUc!L#o)Gc>=rN%uHrE4rR%|I6lhvZjiZZ7V~0msP`QmceQMp@MDw{!)& zvZM=2HNtb|)${h6EPyaWhKl{U-(l?g*@>~CuG2H47!Sz`JZvtU$n(@?qdkP-f*MrLPm=T3X-!|<|_mh4T=Lu(W(<47PD7o z*C9z0ZqA4r>BjZ9*6^0(;V7C+kNPR#C?~@MtnZ8Vxr@)4LV+%kA{D`bSb4EHJ`uN~1=%X4!WIO!S+3K)Jxk>=cC8Du%M$Xv-ML%jYmmu((ar zF+yS|?o?UB8fSIFR3zo-T%f~iAS>HMQ6b%&5M|DFpaHR&WI9$C7)$U4_(*ivlAoWFOHE-qpUL^|LFXg1e2WwCFQeFJ9h)@`%P$U8VT zZol~Sy19B~&SWw1Amz|sM-zo^A>wWer`GlL3ZNeM0w7L?4p{usEXE*Q&#oubbAi>& z0EF*Pax7joMesFO0JF*H+i}PJL+qU=|T{?9BaG3B@418neE(ZbwH7W@Tx`9FPgjlg~A9iP_y+ zgJu))6dP!<$maGTkYYZG1aFp+U3U85CEREZGIQPZ7cx#%mP8_U#}|(7LQcQBa=2&w z-6lDlYM~0C^u4-KWpOUOuh2i)E~(O~*r`JlO-6u+KFkHqZRrB+xY%E{f01whz8rfw zbbsj^MerLOpbM0)Nj+y|fWQ&+oJ(jPzC6+mVv-8mn3BRhF0HoPW*01j1$FEKl}Lie z+<-<>&nvn~PqU#~%AS9m2e3G%-sqHbDUK@BG`u?8s+j^Pg-~h$yu>_!tNGXq8$8el zFjUPj;WW&~b_42?wdcj4{ z9wWa$8Z{#>p&r&~n*Fu2G{2xdpqFFbe&Kn*m|l!l6N))CA$wrM*sm>-88Zr_Lf{V* z5WbW+>TK^xy#$tXgquv5>x+lApdAc>!`w$Z>*r9~YPMFt~EdZ5vftYx!p`=BfE48r<5?unzC z1}m)qg{+RDDS^@ax7hP0H9*v0l7zIHx#xj1kWRzq>E~`saz^Jx;h{5&vv^)*w*e2! z4iwYmEhNpyzjTE?U%xwSHSu4o2NY>3!%C%W_4nt^>guLlT3CfTVa&(>>i6TItG|rAf=@DW*lxWE4;P0FV(UWALD;0=F=L8fHjVaOs z%Q!*2Lb`Oght6i%uknl`^ay1SEt{w_n*=e*Fkuxc49o^A*Jn$9%p1=RncEl{_resq_0{brsO`21#S%7=V!zGxi;l%AM9cN2 z0Q(Y58$C<}yk;7K7)mDY4U83VH3MOmDeKni8eWAvOh*)3u2H0)9yk*KSH^MX1YIc; zFw<4gYJbTJ!A03quV9#QYMf|Mx%X;FH&?SRNcYHdfy@;7#+~$IV zYXwb($E!P4WK4_@1jGf|j|pX+v2$#EK!__mz&#U(>Jmw2updv_7cS2^xj|zf8c1ie z7$bxfHp2xHb+faJ*w@dw3lE$HZQJE@$TOeg ztkLp%HxCH0jJP=OGWvwWVJwU=tYp-cCF!=_NB6tO*vjrjqc}HNhwiY>=nKSU$ws;G7$6t_YFF} z58!r9W7XxyN)-yGVCkp1%0vaB3)B_IQJ10w`k)LVbP38i$7{m%Z+(GGB#2k==Y}j9 zPa>AN#4?2r>6?LmH^X?SI5UueU>{IzRtFxLw_60+Pbri)#9lkl$s#dH$+ z+$uT50+14TA}(*#$kHaHo}0G-D2V~Z@y&YkA+AG>!}VA40(jLIKo|fY7#rySFgN}i zcxrXdO%L$Ybu#VrVtulD%@SSN2J)UGg#Q9TZmsNEXji6!l4E(jY})(#u2MO&V#H4k zM?t*y#j~ReYndGoSlJz2o!>R3Qk4ZSNGoR^wTAcmquwy;_B!;Ac6M&nDAWkflrNC* z{@C#a?ab5&K6-UKh{Q<f+T)j5G?U&^m(>FJE z`IxZ<9H)=C;B?V(YdPtEWmSa9f$tJ!z$r&&?eLWDhOSukO`RS`rp?IT<;DC;bCcho z3gkY(hxEa8hrTl;&+Gus&!Ld(jk1S<}u0h@> zQj-x@R2HrSjjZafbxwo`bE?!Wdifk525&bWK+EFJ3VVdzpEI&wlO!&S>$9jylG{n& z%U%{oXy_+g3O84FWmj*yKxp^b1D~4eGhcja6*M+!0-*O8V)*@?h}gK8+5m;EJCd}$ zsXq7g(^nMB;K}uO^xe*$IWxsWdDCpJZppDIkVW&pwY_Z~e{vR=@r;cm(%KcHFv0I} zWnE6^XvSME-=7WiC*T`Gx%q*IrtSRgRW}EpF$YkC&6i8$GX6eTSls5i?RaPG^&Rx< z>@hhEdYu3~fj}NL6&Ltesn&culgMYe4j{(9GHlee{mq6`%Qp^+{y8rD`r!a zg(ImR7#n&m9hsMs$$(O#{R~Qr>r1=#p;JW+EqVIU328T@3lc{Ti)pC`A%Od>Iy;0^dIP zb?o|j6%M|-1)2(%Vtw0AOfq^bt#aZ~ZBVSa{M`;N&JbMD+BUqceXcLf?r)uL0Fiw* zfwMBcM{pK#^x)_Avfo3GtJf{63v6crm zkG~pwW!uFK9Dr7#UE&8Re*gHDx#V7Zzm+a|iPQ241U{#@o+iZaN5oOIk{sF+mH4zaO1q{C11xdRGk8zNX5Ko0ei`{Wlcfj|eys-9=! zfH%m`P2+yl+}-lB&K`h_M;<=!NB4t3ntT%Lj!pKn>w~P%YDu}*L+r$37G|OHx`tZFO6Ye@Pw=nl0xlS%e_Q_{vJ@toxIUfjhY%uNyGa(uUcGGmF%j;D; zQdGPwQ3JVDKUQn+Y&Bb-qif!NC9aHDYXM9S=F|WC{inwNA^vuO3nzztUMpyZ)!t7< zg`gT62f4zCz}+NYL5XZnUR~p$|9|bB32@}sUFY9x9WAx)Znf@vq|scS8IQ*vJ2tU% zkbp@D0m7MZY#>lTL2)cs7K*UTxyA0*rYZ$XrC4f12uaA2GO=^|itYHC`_f1n-DgW` zsar?4TIzoL`95WUr2=J`(H!x=lktqMR`>hA|L^^d??IWt#+t%jP;%f%hdYS`i1t&A z7e@+ZjDgWb7s_E5NwgBZNwVeAyTfeSPwFtC0oW`aCd><_s)DglLGry|M#-p{occip z`@^0)dl@c=ZUJ=uG9YDH6lw~#JU0hjuFlQDzeI1)0(1sVp99FubeM*T{Au<5 zMM$hHB-b@r6AJ?)b8Zd-IzdYi0!kQgd)zn~uv#jC1$$ACPj?iV9j7Tb7~>IX|IH)J zs(^y1$-k2}>r$gLpWZAfk`m4!Yxw5`!7(zu z2gv-?!t(9w3(XBq{PJa_N?d(hPPSM{i$aP#64aoyW$>lg$S{F}iRS?J0)&B| zQcX?Yk-cusiG6WzKqZbidmn*|GH+X;@kU3L)D@6%cA~sWva4Cg=gb znV|PKF`stKS8xu*#Ho<8^K*+b^>WQEjpnOgJ&Eia2^ejbl2E}@Fq4TT>{TPKHoRpR zIGXOBb`x!CBxztDr_#2l46Dxck^tPM8mcD(`iMZ_6U}Q^zga_(y=^rlED`){HW;uJ znLUjW>}281^Ka=Wu5yp{cQ?3>mS}u3k$y`!Qu-x^C`)?tH}i4Z1wc52h&8wU&51+Z z->GS3FXMVn5ZjDNOAlmnCQzoi3Ns$c%_I%j1x#dwy($V}8bcO$-~LAY7((9Z9`RBG zAk*yYj-uMJ9;zT)swOZo^#Co88ybB$sa&+hBTRuaV7vfvT+*CoR0SDm0>cgoQ>Ak(od|i!^30RP^f6Zun04O zL=-hCIb$?qhlZw^3P)svNQ;Nw9oawy&>ArUG#y5?0F6`ii|8Y5V1>zoWa$-hPk0rV zG6J5ag)+ph1u_HNKz%{oG!ixN-=o|l%T!pCOWy16lVN=DW^S-=rr!k{R~?88Bz9jR z_?_d=GX$!m#H)U;38wHn68q}^(F*ZBFyZ=iU~(P_B(2O`*^eao`5f+R3_QdgMt#RN z(WBE)e|atETB4=4j>El$AfiE6b?LS{Hbox9f2M$2mL1zbM&57d&J*T(?6hB)QDO3g>#X4N2VjVJ_J#AjvEmB!)+gs~x zX=t0^REq*p7+HN7O1%D61binZ6LxuVnSDt1ySifPnwz~)YP?pQVp7-K8utT|aj3-b zk-a!3ZabiVantETHjyagIn*OmY+^St#0ilRGRfYKlhwYnwZ7*0rS!(k&D=A$RRCnN z`9tXTeWJagu6uHB!F8i1kjn~3g%p+kt{AML>=7c%+XU+L@|Ui% zb{0WdS)Y1;8XYV11Nl$sO!mk7Op;YGhEkasdfGVvt~!$pkqD$6Q{-^8cWWmbgVo=2JvivLvMT zF)|vN->^koU>Vbo&#&69rfO6z5LadDAf+FPw*tGvt9Zp+#o4YN$HJxdOh*Zg}EUm~mn!U{ZRbB&EIiubr_IoHxgJrBGE0pTeRZQ4R*_#p7hXYv|6@7?PKluflVGc#DJI

8G`+>kwGRq3`lv^Q?9 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 Nw77y;jfi33{{iMxky-!% From 559fb5ca6c6bdc5658dcb1c21d1016378b35374f Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 22 Dec 2025 09:32:29 +0200 Subject: [PATCH 09/27] Saving progress... --- DesignContracts/CoreTests/AttributeFlavour.cs | 7 +++ DesignContracts/CoreTests/InvariantTests.cs | 41 ++++++++++++ .../Tests.Odin.DesignContracts.csproj | 3 + .../Odin.DesignContracts.Rewriter.csproj | 2 +- DesignContracts/Rewriter/Program.cs | 2 +- .../InvariantWeavingRewriterTests.cs | 62 +++++++++---------- .../RewriterTests/RewrittenAssemblyContext.cs | 2 +- ...Tests.Odin.DesignContracts.Rewriter.csproj | 5 +- .../BclInvariantTarget.cs} | 6 +- .../OdinInvariantTarget.cs} | 6 +- .../TargetsTooled/TargetsTooled.csproj | 15 +++++ .../TargetsUntooled/BclInvariantTarget.cs | 42 +++++++++++++ .../TargetsUntooled/OdinInvariantTarget.cs | 42 +++++++++++++ .../TargetsUntooled.csproj} | 0 .../Odin.DesignContracts.Tooling.csproj | 39 +++++++++--- .../Odin.DesignContracts.Tooling.targets | 13 +++- Odin.sln | 9 ++- 17 files changed, 241 insertions(+), 55 deletions(-) create mode 100644 DesignContracts/CoreTests/AttributeFlavour.cs create mode 100644 DesignContracts/CoreTests/InvariantTests.cs rename DesignContracts/{RewriterTestTargets/BclTarget.cs => TargetsTooled/BclInvariantTarget.cs} (88%) rename DesignContracts/{RewriterTestTargets/OdinTarget.cs => TargetsTooled/OdinInvariantTarget.cs} (86%) create mode 100644 DesignContracts/TargetsTooled/TargetsTooled.csproj create mode 100644 DesignContracts/TargetsUntooled/BclInvariantTarget.cs create mode 100644 DesignContracts/TargetsUntooled/OdinInvariantTarget.cs rename DesignContracts/{RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj => TargetsUntooled/TargetsUntooled.csproj} (100%) diff --git a/DesignContracts/CoreTests/AttributeFlavour.cs b/DesignContracts/CoreTests/AttributeFlavour.cs new file mode 100644 index 0000000..29638a7 --- /dev/null +++ b/DesignContracts/CoreTests/AttributeFlavour.cs @@ -0,0 +1,7 @@ +namespace Tests.Odin.DesignContracts; + +public enum AttributeFlavour +{ + Odin, + BaseClassLibrary +} \ No newline at end of file diff --git a/DesignContracts/CoreTests/InvariantTests.cs b/DesignContracts/CoreTests/InvariantTests.cs new file mode 100644 index 0000000..0cbc8f6 --- /dev/null +++ b/DesignContracts/CoreTests/InvariantTests.cs @@ -0,0 +1,41 @@ +using Odin.DesignContracts; +using NUnit.Framework; +using TargetsTooled; + +namespace Tests.Odin.DesignContracts +{ + [TestFixture] + public sealed class InvariantTests + { + [SetUp] + public void SetUp() + { + DesignContractOptions.Initialize(new DesignContractOptions + { + EnableInvariants = true, + EnablePostconditions = true + }); + } + + [Test] + public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour testCase) + { + Assert.That(DesignContractOptions.Current.EnableInvariants, Is.True); + Assert.That(DesignContractOptions.Current.EnablePostconditions, Is.True); + + ContractException? ex = Assert.Throws(() => + { + if (testCase == AttributeFlavour.Odin) + { + new OdinInvariantTarget(-1); + } + else if (testCase == AttributeFlavour.BaseClassLibrary) + { + new BclInvariantTarget(-1); + } + }); + Assert.That(ex, Is.Not.Null); + Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + } + } +} \ No newline at end of file diff --git a/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj b/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj index 18ee214..b2aab77 100644 --- a/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj +++ b/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj @@ -2,6 +2,8 @@ net8.0;net9.0;net10.0 enable + Tests where the target classes under test HAVE been weaved in post build + by the Design-by-Contract tooling. true true 1591; @@ -16,6 +18,7 @@ + diff --git a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj index 87fa4b3..423d361 100644 --- a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net8.0;net9.0;net10.0 true enable Odin.DesignContracts.Rewriter diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index f8f0b72..99379c0 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -9,7 +9,7 @@ namespace Odin.DesignContracts.Rewriter; /// as well as Design-by-Contract class invariant calls at both entry to and exit from all /// public members on the API surface, unless marked 'Pure'. ///

-internal static class RewriterProgram +internal static class Program { private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ClassInvariantMethodAttribute"; diff --git a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs index 3d8bddb..20f6c4e 100644 --- a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs +++ b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; -using Tests.Odin.DesignContracts.RewriterTargets; +using TargetsUntooled; using ContractFailureKind = Odin.DesignContracts.ContractFailureKind; namespace Tests.Odin.DesignContracts.Rewriter; @@ -23,32 +23,6 @@ public void SetUp() }); } - private Type GetTargetTestTypeFor(AttributeFlavour testCase) - { - if (testCase == AttributeFlavour.Odin) - { - return typeof(InvariantTarget); - } - if (testCase == AttributeFlavour.BaseClassLibrary) - { - return typeof(BclTarget); - } - throw new NotSupportedException(testCase.ToString()); - } - - private Type GetClassInvariantAttributeTypeFor(AttributeFlavour testCase) - { - if (testCase == AttributeFlavour.Odin) - { - return typeof(ClassInvariantMethodAttribute); - } - if (testCase == AttributeFlavour.BaseClassLibrary) - { - return typeof(ContractInvariantMethodAttribute); - } - throw new NotSupportedException(testCase.ToString()); - } - [Test] public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour testCase) { @@ -86,7 +60,7 @@ public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour test object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); - ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(InvariantTarget.Increment)); })!; + ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(OdinInvariantTarget.Increment)); })!; Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } @@ -101,7 +75,7 @@ public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testC object instance = Activator.CreateInstance(t, 1)!; - ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(InvariantTarget.MakeInvalid)); })!; + ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(OdinInvariantTarget.MakeInvalid)); })!; Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } @@ -184,7 +158,7 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); MethodDefinition increment = targetType.Methods - .First(m => m.Name == nameof(InvariantTarget.Increment)); + .First(m => m.Name == nameof(OdinInvariantTarget.Increment)); // Add a second invariant attribute to a different method. var ctor = invariantAttributeTestCaseType.GetConstructor(Type.EmptyTypes) @@ -197,7 +171,7 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute // Act + Assert string outputPath = Path.Combine(temp.Path, "out.dll"); - InvalidOperationException? expectedError = Assert.Throws(() => RewriterProgram.RewriteAssembly(inputPath, outputPath)); + InvalidOperationException? expectedError = Assert.Throws(() => Program.RewriteAssembly(inputPath, outputPath)); Assert.That(expectedError, Is.Not.Null); } @@ -222,4 +196,30 @@ private static void SetPrivateField(Type declaringType, object instance, string throw tie.InnerException; } } + + private Type GetTargetTestTypeFor(AttributeFlavour testCase) + { + if (testCase == AttributeFlavour.Odin) + { + return typeof(OdinInvariantTarget); + } + if (testCase == AttributeFlavour.BaseClassLibrary) + { + return typeof(BclInvariantTarget); + } + throw new NotSupportedException(testCase.ToString()); + } + + private Type GetClassInvariantAttributeTypeFor(AttributeFlavour testCase) + { + if (testCase == AttributeFlavour.Odin) + { + return typeof(ClassInvariantMethodAttribute); + } + if (testCase == AttributeFlavour.BaseClassLibrary) + { + return typeof(ContractInvariantMethodAttribute); + } + throw new NotSupportedException(testCase.ToString()); + } } \ No newline at end of file diff --git a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs index 4d7549a..8b0e84b 100644 --- a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs +++ b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs @@ -27,7 +27,7 @@ public RewrittenAssemblyContext(Assembly sourceAssembly) CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); string outputPath = Path.Combine(_tempDir, "rewritten.dll"); - RewriterProgram.RewriteAssembly(inputPath, outputPath); + Program.RewriteAssembly(inputPath, outputPath); _alc = new TestAssemblyLoadContext(outputPath); RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); diff --git a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj index dd25565..37e817c 100644 --- a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj @@ -4,6 +4,9 @@ enable true true + Tests where the target classes under test have not been weaved in post build + by the Design-by-Contract tooling. The Rewriter is explicitly run on a target assembly + for each test, and the types under test dynamically loaded. 1591; @@ -17,7 +20,7 @@ - + diff --git a/DesignContracts/RewriterTestTargets/BclTarget.cs b/DesignContracts/TargetsTooled/BclInvariantTarget.cs similarity index 88% rename from DesignContracts/RewriterTestTargets/BclTarget.cs rename to DesignContracts/TargetsTooled/BclInvariantTarget.cs index 7d9cb10..6ee2804 100644 --- a/DesignContracts/RewriterTestTargets/BclTarget.cs +++ b/DesignContracts/TargetsTooled/BclInvariantTarget.cs @@ -1,16 +1,16 @@ using Odin.DesignContracts; -namespace Tests.Odin.DesignContracts.RewriterTargets +namespace TargetsTooled { /// /// The rewriter is expected to inject /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. /// - public sealed class BclTarget + public sealed class BclInvariantTarget { private int _value; - public BclTarget(int value) + public BclInvariantTarget(int value) { _value = value; } diff --git a/DesignContracts/RewriterTestTargets/OdinTarget.cs b/DesignContracts/TargetsTooled/OdinInvariantTarget.cs similarity index 86% rename from DesignContracts/RewriterTestTargets/OdinTarget.cs rename to DesignContracts/TargetsTooled/OdinInvariantTarget.cs index ae7bd99..574d47a 100644 --- a/DesignContracts/RewriterTestTargets/OdinTarget.cs +++ b/DesignContracts/TargetsTooled/OdinInvariantTarget.cs @@ -1,16 +1,16 @@ using Odin.DesignContracts; -namespace Tests.Odin.DesignContracts.RewriterTargets +namespace TargetsTooled { /// /// The rewriter is expected to inject /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. /// - public sealed class InvariantTarget + public sealed class OdinInvariantTarget { private int _value; - public InvariantTarget(int value) + public OdinInvariantTarget(int value) { _value = value; } diff --git a/DesignContracts/TargetsTooled/TargetsTooled.csproj b/DesignContracts/TargetsTooled/TargetsTooled.csproj new file mode 100644 index 0000000..7e5326c --- /dev/null +++ b/DesignContracts/TargetsTooled/TargetsTooled.csproj @@ -0,0 +1,15 @@ + + + net8.0;net9.0;net10.0 + true + enable + Target classes for the Design Contract rewriter to rewrite + + 1591;1573; + + + + + + + \ No newline at end of file diff --git a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs b/DesignContracts/TargetsUntooled/BclInvariantTarget.cs new file mode 100644 index 0000000..a64d20f --- /dev/null +++ b/DesignContracts/TargetsUntooled/BclInvariantTarget.cs @@ -0,0 +1,42 @@ +using Odin.DesignContracts; + +namespace TargetsUntooled +{ + /// + /// The rewriter is expected to inject + /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// + public sealed class BclInvariantTarget + { + private int _value; + + public BclInvariantTarget(int value) + { + _value = value; + } + + [System.Diagnostics.Contracts.ContractInvariantMethod] + private void ObjectInvariant() + { + Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); + } + + public void Increment() + { + _value++; + } + + public void MakeInvalid() + { + _value = -1; + } + + [System.Diagnostics.Contracts.Pure] + public int PureGetValue() => _value; + + [System.Diagnostics.Contracts.Pure] + public int PureProperty => _value; + + public int NonPureProperty => _value; + } +} diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs new file mode 100644 index 0000000..ad9c45c --- /dev/null +++ b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs @@ -0,0 +1,42 @@ +using Odin.DesignContracts; + +namespace TargetsUntooled +{ + /// + /// The rewriter is expected to inject + /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// + public sealed class OdinInvariantTarget + { + private int _value; + + public OdinInvariantTarget(int value) + { + _value = value; + } + + [global::Odin.DesignContracts.ClassInvariantMethod] + private void ObjectInvariant() + { + Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); + } + + public void Increment() + { + _value++; + } + + public void MakeInvalid() + { + _value = -1; + } + + [global::Odin.DesignContracts.Pure] + public int PureGetValue() => _value; + + [global::Odin.DesignContracts.Pure] + public int PureProperty => _value; + + public int NonPureProperty => _value; + } +} diff --git a/DesignContracts/RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj b/DesignContracts/TargetsUntooled/TargetsUntooled.csproj similarity index 100% rename from DesignContracts/RewriterTestTargets/Tests.Odin.DesignContracts.RewriterTargets.csproj rename to DesignContracts/TargetsUntooled/TargetsUntooled.csproj diff --git a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj index 5c73585..4775483 100644 --- a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj +++ b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj @@ -1,6 +1,6 @@ - net8.0 + net8.0;net9.0;net10.0 true enable @@ -15,6 +15,16 @@ + + + + <_RewriterFiles Include="../Rewriter/bin/$(Configuration)/$(TargetFramework)/**/*.*" /> + + + + @@ -24,14 +34,23 @@ - - - - - - + + + + <_Tfms ToLower="true" Include="$(TargetFrameworks)" /> + <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.dll"> + tools/%(_Tfms.Identity)/any/ + + <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.runtimeconfig.json"> + tools/%(_Tfms.Identity)/any/ + + <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.deps.json"> + tools/%(_Tfms.Identity)/any/ + + + + + + diff --git a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets index 93cfc85..abf5df0 100644 --- a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets +++ b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets @@ -7,17 +7,24 @@ true - - <_OdinDcRewriterPath>$(MSBuildThisFileDirectory)..\tools\net8.0\any\Odin.DesignContracts.Rewriter.dll + <_OdinNetVersion>$(TargetFramework.Substring(3)) + <_OdinDcRewriterPathCandidate>$(MSBuildThisFileDirectory)..\tools\net$(_OdinNetVersion)\any\Odin.DesignContracts.Rewriter.dll + <_OdinDcRewriterPath Condition="Exists('$(_OdinDcRewriterPathCandidate)')">$(_OdinDcRewriterPathCandidate) + <_OdinDcRewriterPath Condition="'$(_OdinDcRewriterPath)'==''">$(MSBuildThisFileDirectory)..\tools\net10.0\any\Odin.DesignContracts.Rewriter.dll <_OdinDcWeavedAssembly>$(IntermediateOutputPath)$(AssemblyName).odindc.weaved$(TargetExt) <_OdinDcWeavedPdb>$(IntermediateOutputPath)$(AssemblyName).odindc.weaved.pdb - + + + + + diff --git a/Odin.sln b/Odin.sln index e1feb16..d2d2a9b 100644 --- a/Odin.sln +++ b/Odin.sln @@ -103,7 +103,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Toolin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts.Rewriter", "DesignContracts\RewriterTests\Tests.Odin.DesignContracts.Rewriter.csproj", "{320907F6-F4BA-4A1D-AC23-145A5AD06C14}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts.RewriterTargets", "DesignContracts\RewriterTestTargets\Tests.Odin.DesignContracts.RewriterTargets.csproj", "{7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetsUntooled", "DesignContracts\TargetsUntooled\TargetsUntooled.csproj", "{7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetsTooled", "DesignContracts\TargetsTooled\TargetsTooled.csproj", "{8138E8B4-E90D-45A3-BEB0-31F048948A91}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -247,6 +249,10 @@ Global {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Debug|Any CPU.Build.0 = Debug|Any CPU {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Release|Any CPU.ActiveCfg = Release|Any CPU {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}.Release|Any CPU.Build.0 = Release|Any CPU + {8138E8B4-E90D-45A3-BEB0-31F048948A91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8138E8B4-E90D-45A3-BEB0-31F048948A91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8138E8B4-E90D-45A3-BEB0-31F048948A91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8138E8B4-E90D-45A3-BEB0-31F048948A91}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE323D9C-635B-EFD3-5B3F-7CE371D8A86A} = {BF440C74-E223-3CBF-8FA7-83F7D164F7C3} @@ -284,5 +290,6 @@ Global {E450FC74-0DBE-320A-FE7A-87255CB4DFAB} = {73BA62BB-2B41-2DC4-C714-51B3D4C2A215} {320907F6-F4BA-4A1D-AC23-145A5AD06C14} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} {7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} + {8138E8B4-E90D-45A3-BEB0-31F048948A91} = {9E3E1A13-9A74-4895-98A9-D96F4E0ED4B7} EndGlobalSection EndGlobal From 090a31b40072c7043bf7d1f074747022f3a4b4ec Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 22 Dec 2025 09:35:40 +0200 Subject: [PATCH 10/27] Nuget publich DC.Tooling --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1cf6d4c..a2a352f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,6 +58,7 @@ jobs: dotnet pack ./Configuration/AzureBlobJson/Odin.Configuration.AzureBlobJson.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR dotnet pack ./Data/SqlScriptsRunner/Odin.Data.SqlScriptsRunner.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR dotnet pack ./DesignContracts/Core/Odin.DesignContracts.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR + dotnet pack ./DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR dotnet pack ./Email/Mailgun/Odin.Email.Mailgun.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR dotnet pack ./Email/Office365/Odin.Email.Office365.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR dotnet pack ./Email/Core/Odin.Email.csproj --configuration $CONFIGURATION --output $PACKAGE_DIR From e9fcc3b1a328234fd0494ce777ab7c62752452e4 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 22 Dec 2025 09:43:54 +0200 Subject: [PATCH 11/27] Exclude DC tests for now for publishing DC tooling --- TestsOnly.sln | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TestsOnly.sln b/TestsOnly.sln index 5e54937..fd54c0d 100644 --- a/TestsOnly.sln +++ b/TestsOnly.sln @@ -10,8 +10,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.Templating", "Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.Utility", "Utility\Tests\Tests.Odin.Utility.csproj", "{52AF2C63-2247-42FD-B3DD-4AD2AFCE0C6D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts", "DesignContracts\Tests\Tests.Odin.DesignContracts.csproj", "{177EDB52-3174-4037-80DD-235222F9817B}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.Messaging", "Messaging\Tests\Tests.Odin.Messaging.csproj", "{690312C5-1948-4530-832D-6332625D4E9C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.System", "System\Tests\Tests.Odin.System.csproj", "{957966BC-FE0E-4206-BDD8-F04591AB836E}" @@ -23,8 +21,6 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {B091589D-960C-40F6-822F-522D099828E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {B091589D-960C-40F6-822F-522D099828E6}.Release|Any CPU.Build.0 = Release|Any CPU - {177EDB52-3174-4037-80DD-235222F9817B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {177EDB52-3174-4037-80DD-235222F9817B}.Release|Any CPU.Build.0 = Release|Any CPU {EAD2138A-1553-43A9-B51F-55FC2A2CEFFD}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAD2138A-1553-43A9-B51F-55FC2A2CEFFD}.Release|Any CPU.Build.0 = Release|Any CPU {690312C5-1948-4530-832D-6332625D4E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU From 6b28e3e24ca60c4e241f5eb85462b4aa9a5c3ae8 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 22 Dec 2025 09:48:01 +0200 Subject: [PATCH 12/27] Pre release publishes don't require tests --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a2a352f..b6a6cbc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,6 +36,7 @@ jobs: dotnet-version: 10.x.x - name: Run Unit Tests on .Net 10 + if: github.ref == 'refs/heads/master' run: dotnet test TestsOnly.sln -f net10.0 --verbosity quiet --filter "TestCategory!=IntegrationTest" - name: Install GitVersion From bdb3b6de82502374203fb7d4be20660ea82464b3 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Mon, 22 Dec 2025 21:32:40 +0200 Subject: [PATCH 13/27] Saving progress --- DesignContracts/Core/Postcondition.cs | 3 - .../Tests.Odin.DesignContracts.csproj | 1 - DesignContracts/Rewriter/CecilExtensions.cs | 55 +++ .../Rewriter/InvariantWeavingRequirement.cs | 7 + DesignContracts/Rewriter/MemberHandler.cs | 274 +++++++++++++++ DesignContracts/Rewriter/Names.cs | 17 + .../Odin.DesignContracts.Rewriter.csproj | 4 + DesignContracts/Rewriter/Program.cs | 314 +----------------- DesignContracts/Rewriter/TypeHandler.cs | 84 +++++ .../InvariantWeavingRewriterTests.cs | 22 +- .../TargetsTooled/BclInvariantTarget.cs | 42 --- .../TargetsTooled/OdinInvariantTarget.cs | 42 --- .../TargetsTooled/TargetsTooled.csproj | 27 +- .../TargetsUntooled/BclInvariantTarget.cs | 15 +- .../TargetsUntooled/OdinInvariantTarget.cs | 15 +- .../Odin.DesignContracts.Tooling.targets | 15 +- 16 files changed, 527 insertions(+), 410 deletions(-) create mode 100644 DesignContracts/Rewriter/CecilExtensions.cs create mode 100644 DesignContracts/Rewriter/InvariantWeavingRequirement.cs create mode 100644 DesignContracts/Rewriter/MemberHandler.cs create mode 100644 DesignContracts/Rewriter/Names.cs create mode 100644 DesignContracts/Rewriter/TypeHandler.cs delete mode 100644 DesignContracts/TargetsTooled/BclInvariantTarget.cs delete mode 100644 DesignContracts/TargetsTooled/OdinInvariantTarget.cs diff --git a/DesignContracts/Core/Postcondition.cs b/DesignContracts/Core/Postcondition.cs index 1630b1b..2e10fe9 100644 --- a/DesignContracts/Core/Postcondition.cs +++ b/DesignContracts/Core/Postcondition.cs @@ -69,8 +69,5 @@ public static void Ensures(bool condition, string? userMessage = null, string? c conditionText); } } - - - } } \ No newline at end of file diff --git a/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj b/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj index b2aab77..00f5169 100644 --- a/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj +++ b/DesignContracts/CoreTests/Tests.Odin.DesignContracts.csproj @@ -17,7 +17,6 @@ - diff --git a/DesignContracts/Rewriter/CecilExtensions.cs b/DesignContracts/Rewriter/CecilExtensions.cs new file mode 100644 index 0000000..4fc29cc --- /dev/null +++ b/DesignContracts/Rewriter/CecilExtensions.cs @@ -0,0 +1,55 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Odin.DesignContracts.Rewriter; + +internal static class CecilExtensions +{ + internal static bool HasAnyAttributeIn(this ICustomAttributeProvider provider, string[] attributeFullNames) + => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => attributeFullNames.Contains(a.AttributeType.FullName)); + + internal static bool HasAttribute(this ICustomAttributeProvider provider, string attributeFullName) + => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => a.AttributeType.FullName == attributeFullName); + + public static bool IsPure(TypeDefinition declaringType, MethodDefinition method) + { + if (method.HasAnyAttributeIn(Names.PureAttributeFullNames)) + return true; + + // For accessors, also honour [Pure] on the property itself. + if (method.IsGetter || method.IsSetter) + { + PropertyDefinition? prop = declaringType.Properties.FirstOrDefault(p => p.GetMethod == method || p.SetMethod == method); + if (prop is not null && prop.HasAnyAttributeIn(Names.PureAttributeFullNames)) + return true; + } + return false; + } + + public static Instruction CloneInstruction(this Instruction instruction) + { + // Minimal cloning sufficient for contract block statements. + // We intentionally do not support cloning branch targets / exception handler operands in v1. + if (instruction.Operand is null) + return Instruction.Create(instruction.OpCode); + + return instruction.Operand switch + { + sbyte b => Instruction.Create(instruction.OpCode, b), + byte b => Instruction.Create(instruction.OpCode, (sbyte)b), // Cecil uses sbyte for short forms. + int i => Instruction.Create(instruction.OpCode, i), + long l => Instruction.Create(instruction.OpCode, l), + float f => Instruction.Create(instruction.OpCode, f), + double d => Instruction.Create(instruction.OpCode, d), + string s => Instruction.Create(instruction.OpCode, s), + MethodReference mr => Instruction.Create(instruction.OpCode, mr), + FieldReference fr => Instruction.Create(instruction.OpCode, fr), + TypeReference tr => Instruction.Create(instruction.OpCode, tr), + ParameterDefinition pd => Instruction.Create(instruction.OpCode, pd), + VariableDefinition vd => Instruction.Create(instruction.OpCode, vd), + _ => throw new NotSupportedException( + $"Unsupported operand type in contract block cloning: {instruction.Operand.GetType().FullName} (opcode {instruction.OpCode}).") + }; + } + +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/InvariantWeavingRequirement.cs b/DesignContracts/Rewriter/InvariantWeavingRequirement.cs new file mode 100644 index 0000000..112814f --- /dev/null +++ b/DesignContracts/Rewriter/InvariantWeavingRequirement.cs @@ -0,0 +1,7 @@ +namespace Odin.DesignContracts.Rewriter; + +internal record InvariantWeavingRequirement +{ + public required bool OnEntry { get; init; } + public required bool OnExit { get; init; } +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/MemberHandler.cs b/DesignContracts/Rewriter/MemberHandler.cs new file mode 100644 index 0000000..a0fd7c4 --- /dev/null +++ b/DesignContracts/Rewriter/MemberHandler.cs @@ -0,0 +1,274 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Odin.System; + +namespace Odin.DesignContracts.Rewriter; + +/// +/// Handles member-specific matters with respect to design contract rewriting. +/// Note: MemberHandler includes methods AND property accessors... +/// +internal class MemberHandler +{ + private readonly TypeHandler _parentHandler; + + public MemberHandler(MethodDefinition method, TypeHandler parentHandler) + { + Method = method; + _parentHandler = parentHandler; + } + + public MethodDefinition Method { get; } + + public bool TryRewrite() + { + // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. + // if (method.IsAsync) + // return false; + + // What about properties? Surely these can have no body? + if (!Method.HasBody) return false; + + Method.Body.SimplifyMacros(); + + InvariantWeavingRequirement invariantsToDo = IsInvariantToBeWeaved(); + + ResultValue> postconditionsExtracted = + TryExtractPostconditions(); + + if (!postconditionsExtracted.IsSuccess && + !invariantsToDo.OnEntry && !invariantsToDo.OnExit) + { + // Nothing to do. + Method.Body.OptimizeMacros(); + return false; + } + + ILProcessor il = Method.Body.GetILProcessor(); + + // Inject invariant call at entry (before any user code). + if (invariantsToDo.OnEntry) + { + Instruction first = Method.Body.Instructions.FirstOrDefault() ?? Instruction.Create(OpCodes.Nop); + if (Method.Body.Instructions.Count == 0) + il.Append(first); + + InsertInvariantCallBefore(il, first, _parentHandler.InvariantMethod!); + } + + // Remove the postconditions from the method entry when postconditions are present. + if (postconditionsExtracted.IsSuccess) + { + foreach (Instruction instruction in postconditionsExtracted.Value) + { + // If the instruction was already removed as part of a previous remove, skip. + if (Method.Body.Instructions.Contains(instruction)) + il.Remove(instruction); + } + } + + // If we need to inject postconditions and/or invariant calls at exit, do it per-return. + // Todo: If there are multiple returns, create a shadow method to execute the + // invariant and\or postconditions, calling it from each return. + if (postconditionsExtracted.IsSuccess || invariantsToDo.OnExit) + { + VariableDefinition? resultVar = null; + bool isVoid = IsVoidReturnType(); + if (!isVoid) + { + resultVar = new VariableDefinition(Method.ReturnType); + Method.Body.Variables.Add(resultVar); + Method.Body.InitLocals = true; + } + + List returns = Method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToList(); + foreach (Instruction returnInst in returns) + { + // For non-void methods we must preserve the return value while we call extra code. + if (!isVoid) + { + il.InsertBefore(returnInst, Instruction.Create(OpCodes.Stloc, resultVar!)); + } + + if (postconditionsExtracted.IsSuccess) + { + foreach (Instruction inst in postconditionsExtracted.Value) + { + if (IsEndContractBlockCall(inst)) + continue; + + Instruction cloned = inst.CloneInstruction(); + + if (!isVoid && IsResultCall(cloned)) + { + // Replace call Contract.Result() with ldloc resultVar. + cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); + } + il.InsertBefore(returnInst, cloned); + } + } + + if (invariantsToDo.OnExit) + { + InsertInvariantCallBefore(il, returnInst, _parentHandler.InvariantMethod!); + } + + if (!isVoid) + { + il.InsertBefore(returnInst, Instruction.Create(OpCodes.Ldloc, resultVar!)); + } + } + } + + Method.Body.OptimizeMacros(); + return true; + } + + public InvariantWeavingRequirement IsInvariantToBeWeaved() + { + bool canWeaveInvariant = _parentHandler.HasInvariant && IsPublicInstanceMethod(); + bool isInvariantMethodItself = _parentHandler.InvariantMethod is not null && Method == _parentHandler.InvariantMethod; + + // Per requirements: + // - Constructors: invariants at exit only + // - Other public instance methods + public instance property accessors: invariants at entry and exit + + bool weaveInvariantOnEntry = canWeaveInvariant && !isInvariantMethodItself && !IsInstanceConstructor(); + bool weaveInvariantOnExit = canWeaveInvariant && !isInvariantMethodItself; + + // Exclude [Pure] methods and [Pure] properties (for accessors). + // Ignore [Pure] on constructors... + if (!IsInstanceConstructor() && weaveInvariantOnEntry && IsPure()) + { + weaveInvariantOnEntry = false; + weaveInvariantOnExit = false; + } + + return new InvariantWeavingRequirement() + { + OnEntry = weaveInvariantOnEntry, + OnExit = weaveInvariantOnExit + }; + } + + public ResultValue> TryExtractPostconditions() + { + // v1: contract block must be explicitly terminated by Contract.EndContractBlock(). + // This makes extraction deterministic without needing sequence points. + List postconditions = new List(); + + IList instructions = Method.Body.Instructions; + if (instructions.Count == 0) + return ResultValue>.Failure("Method has no instructions."); + + int endIndex = -1; + for (int i = 0; i < instructions.Count; i++) + { + Instruction inst = instructions[i]; + postconditions.Add(inst); + + if (IsEndContractBlockCall(inst)) + { + endIndex = i; + break; + } + } + + if (endIndex < 0) + { + // No explicit contract block end. + postconditions.Clear(); + return false; + } + + // Also include trailing nops immediately after EndContractBlock, as they often belong + // to the same source statement / sequence point. + for (int i = endIndex + 1; i < instructions.Count; i++) + { + if (instructions[i].OpCode != OpCodes.Nop) + break; + postconditions.Add(instructions[i]); + } + + return true; + } + + + public bool IsEnsuresCall(Instruction inst) + => IsStaticCallToContractMethod(inst, "Ensures"); + + public bool IsEndContractBlockCall(Instruction inst) + => IsStaticCallToContractMethod(inst, "EndContractBlock"); + + public bool IsResultCall(Instruction inst) + => IsStaticCallToContractMethod(inst, "Result"); + + + private static bool IsStaticCallToContractMethod(Instruction inst, string methodName) + { + if (inst.OpCode != OpCodes.Call) + return false; + + if (inst.Operand is not MethodReference mr) + return false; + + if (mr.Name != methodName) + return false; + + // Handle generic instance method as well. + string declaringType = mr.DeclaringType.FullName; + return declaringType == Names.OdinContractTypeFullName; + } + + /// + /// True if Method is a public instance method. + /// + /// + public bool IsPublicInstanceMethod() + { + return Method is { IsPublic: true, IsStatic: false }; + } + + /// + /// True if Method is marked as [Pure] or if it is a property accessor of a [Pure] property. + /// + /// + public bool IsPure() + { + if (Method.HasAnyAttributeIn(Names.PureAttributeFullNames)) + return true; + + // For accessors, also honour [Pure] on the property itself. + if (Method.IsGetter || Method.IsSetter) + { + PropertyDefinition? prop = _parentHandler.Type.Properties. + FirstOrDefault(p => p.GetMethod == Method || p.SetMethod == Method); + if (prop is not null && prop.HasAnyAttributeIn(Names.PureAttributeFullNames)) + return true; + } + return false; + } + + /// + /// True if Method returns void. + /// + /// + public bool IsVoidReturnType() + { + return Method.ReturnType.MetadataType == MetadataType.Void; + } + + public bool IsInstanceConstructor() + { + return Method.IsConstructor && !Method.IsStatic; + } + + private void InsertInvariantCallBefore(ILProcessor il, Instruction before, MethodDefinition invariantMethod) + { + // instance.Invariant(); + il.InsertBefore(before, Instruction.Create(OpCodes.Ldarg_0)); + il.InsertBefore(before, Instruction.Create(OpCodes.Call, invariantMethod)); + } + +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/Names.cs b/DesignContracts/Rewriter/Names.cs new file mode 100644 index 0000000..1ccc1a1 --- /dev/null +++ b/DesignContracts/Rewriter/Names.cs @@ -0,0 +1,17 @@ +namespace Odin.DesignContracts.Rewriter; + +/// +/// Full names of all the attributes used in Design Contracts +/// +internal static class Names +{ + internal const string BclContractNamespace = "System.Diagnostics.Contracts"; + internal const string OdinContractNamespace = "Odin.DesignContracts"; + internal const string OdinContractTypeFullName = OdinContractNamespace + ".Contract"; + internal const string OdinInvariantAttributeFullName = OdinContractNamespace + ".ClassInvariantMethodAttribute"; + internal const string BclInvariantAttributeFullName = BclContractNamespace + ".ContractInvariantMethodAttribute"; + internal const string OdinPureAttributeFullName = OdinContractNamespace + ".PureAttribute"; + internal const string BclPureAttributeFullName = BclContractNamespace + ".PureAttribute"; + internal static readonly string[] PureAttributeFullNames = [OdinPureAttributeFullName, BclPureAttributeFullName]; + internal static readonly string[] InvariantAttributeFullNames = [OdinInvariantAttributeFullName, BclInvariantAttributeFullName]; +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj index 423d361..e613d2b 100644 --- a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj @@ -11,5 +11,9 @@ + + + + diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 99379c0..c94be1c 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -11,20 +11,14 @@ namespace Odin.DesignContracts.Rewriter; ///
internal static class Program { - private const string ContractTypeFullName = "Odin.DesignContracts.Contract"; - private const string OdinInvariantAttributeFullName = "Odin.DesignContracts.ClassInvariantMethodAttribute"; - private const string OdinPureAttributeFullName = "Odin.DesignContracts.PureAttribute"; - private const string BclInvariantAttributeFullName = "System.Diagnostics.Contracts.ContractInvariantMethodAttribute"; - private const string BclPureAttributeFullName = "System.Diagnostics.Contracts.PureAttribute"; - private static readonly string[] PureAttributeFullNames = [OdinPureAttributeFullName, BclPureAttributeFullName]; - private static readonly string[] InvariantAttributeFullNames = [OdinInvariantAttributeFullName, BclInvariantAttributeFullName]; + private const string Rewriter = "Odin.DesignContracts.Rewriter"; private static int Main(string[] args) { if (args.Length < 2) { - Console.Error.WriteLine("Usage: Odin.DesignContracts.Rewriter "); - return 2; + Console.Error.WriteLine($"{Rewriter}: Usage 'Odin.DesignContracts.Rewriter '"); + return 3; } string assemblyPath = args[0]; @@ -32,7 +26,7 @@ private static int Main(string[] args) if (!File.Exists(assemblyPath)) { - Console.Error.WriteLine($"Assembly not found: {assemblyPath}"); + Console.Error.WriteLine($"{Rewriter}: Input assembly not found: {assemblyPath}"); return 2; } @@ -43,7 +37,8 @@ private static int Main(string[] args) } catch (Exception ex) { - Console.Error.WriteLine(ex); + Console.Error.WriteLine($"{Rewriter}: Unexpected error while rewriting assembly..."); + Console.Error.WriteLine($"{Rewriter}: {ex.Message}"); return 1; } } @@ -52,11 +47,11 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) { string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(assemblyPath))!; - var resolver = new DefaultAssemblyResolver(); + DefaultAssemblyResolver resolver = new(); resolver.AddSearchDirectory(assemblyDir); // Portable PDBs are optional. If present, Cecil will pick them up with ReadSymbols = true. - var readerParameters = new ReaderParameters + ReaderParameters readerParameters = new() { AssemblyResolver = resolver, ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")) @@ -69,16 +64,12 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) { foreach (TypeDefinition type in module.GetTypes()) { - MethodDefinition? invariantMethod = FindInvariantMethodOrThrow(type); + TypeHandler currentType = new(type); - foreach (MethodDefinition method in type.Methods) + foreach (var member in currentType.GetMembersToTryRewrite()) { - if (!method.HasBody) - continue; - - if (!TryRewriteMethod(type, method, invariantMethod)) + if (!member.TryRewrite()) continue; - rewritten++; } } @@ -86,7 +77,7 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))!); - var writerParameters = new WriterParameters + WriterParameters writerParameters = new() { WriteSymbols = readerParameters.ReadSymbols }; @@ -95,283 +86,6 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) Console.WriteLine($"Rewriter: rewritten methods: {rewritten}"); } - private static bool TryRewriteMethod(TypeDefinition declaringType, MethodDefinition method, MethodDefinition? invariantMethod) - { - // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. - // if (method.IsAsync) - // return false; - - method.Body.SimplifyMacros(); - - bool hasInvariant = invariantMethod is not null; - bool canWeaveInvariant = hasInvariant && IsPublicInstanceMethodOrAccessor(method); - bool isInvariantMethodItself = invariantMethod is not null && method == invariantMethod; - - // Per requirements: - // - Constructors: invariants at exit only - // - Other public instance methods + public instance property accessors: invariants at entry and exit - bool isConstructor = method.IsConstructor && !method.IsStatic; - - bool weaveInvariantOnEntry = canWeaveInvariant && !isInvariantMethodItself && !isConstructor; - bool weaveInvariantOnExit = canWeaveInvariant && !isInvariantMethodItself; - - // Exclude [Pure] methods and [Pure] properties (for accessors). - if (!isConstructor && weaveInvariantOnEntry && IsPure(declaringType, method)) - { - weaveInvariantOnEntry = false; - weaveInvariantOnExit = false; - } - - // If we have no invariant weaving and no postconditions, skip quickly. - if (!weaveInvariantOnEntry && !weaveInvariantOnExit) - { - // Still allow postconditions rewriting. - } - - bool hasContractBlock = TryExtractContractBlock(method, out List contractBlockInstructions); - bool hasEnsures = hasContractBlock && contractBlockInstructions.Any(IsEnsuresCall); - - if (!hasEnsures && !weaveInvariantOnEntry && !weaveInvariantOnExit) - { - method.Body.OptimizeMacros(); - return false; - } - - var il = method.Body.GetILProcessor(); - - // Inject invariant call at entry (before any user code). - if (weaveInvariantOnEntry) - { - Instruction first = method.Body.Instructions.FirstOrDefault() ?? Instruction.Create(OpCodes.Nop); - if (method.Body.Instructions.Count == 0) - il.Append(first); - - InsertInvariantCallBefore(il, first, invariantMethod!); - } - - // Remove the contract block from the method entry when postconditions are present. - if (hasEnsures) - { - foreach (Instruction inst in contractBlockInstructions) - { - // If the instruction was already removed as part of a previous remove, skip. - if (method.Body.Instructions.Contains(inst)) - il.Remove(inst); - } - } - - // If we need to inject postconditions and/or invariant calls at exit, do it per-ret. - if (hasEnsures || weaveInvariantOnExit) - { - bool isVoid = method.ReturnType.MetadataType == MetadataType.Void; - VariableDefinition? resultVar = null; - - if (!isVoid) - { - resultVar = new VariableDefinition(method.ReturnType); - method.Body.Variables.Add(resultVar); - method.Body.InitLocals = true; - } - - List rets = method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToList(); - foreach (Instruction ret in rets) - { - // For non-void methods we must preserve the return value while we call extra code. - if (!isVoid) - { - il.InsertBefore(ret, Instruction.Create(OpCodes.Stloc, resultVar!)); - } - - if (hasEnsures) - { - foreach (Instruction inst in contractBlockInstructions) - { - if (IsEndContractBlockCall(inst)) - continue; - - Instruction cloned = CloneInstruction(inst); - - if (!isVoid && IsResultCall(cloned)) - { - // Replace call Contract.Result() with ldloc resultVar. - cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); - } - - il.InsertBefore(ret, cloned); - } - } - - if (weaveInvariantOnExit) - { - InsertInvariantCallBefore(il, ret, invariantMethod!); - } - - if (!isVoid) - { - il.InsertBefore(ret, Instruction.Create(OpCodes.Ldloc, resultVar!)); - } - } - } - - method.Body.OptimizeMacros(); - return true; - } - - private static bool IsPublicInstanceMethodOrAccessor(MethodDefinition method) - => method.IsPublic && !method.IsStatic; - - private static bool IsPure(TypeDefinition declaringType, MethodDefinition method) - { - if (method.HasAnyAttributeIn(PureAttributeFullNames)) - return true; - - // For accessors, also honour [Pure] on the property itself. - if (method.IsGetter || method.IsSetter) - { - PropertyDefinition? prop = declaringType.Properties.FirstOrDefault(p => p.GetMethod == method || p.SetMethod == method); - if (prop is not null && prop.HasAnyAttributeIn(PureAttributeFullNames)) - return true; - } - - return false; - } - - // private static bool HasAttribute(ICustomAttributeProvider provider, string attributeFullName) - // => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => a.AttributeType.FullName == attributeFullName); - - private static bool HasAnyAttributeIn(this ICustomAttributeProvider provider, string[] attributeFullNames) - => provider.HasCustomAttributes && provider.CustomAttributes.Any(a => attributeFullNames.Contains(a.AttributeType.FullName)); - - private static MethodDefinition? FindInvariantMethodOrThrow(TypeDefinition type) - { - List candidates = type.Methods - .Where(m => m.HasAnyAttributeIn(InvariantAttributeFullNames)) - .ToList(); - - if (candidates.Count == 0) - return null; - - if (candidates.Count > 1) - { - string names = string.Join(", ", candidates.Select(m => m.FullName)); - throw new InvalidOperationException( - $"Type '{type.FullName}' has multiple invariant methods. Exactly one method may be marked with ClassInvariantMethodAttribute. Candidates: {names}"); - } - - MethodDefinition invariant = candidates[0]; - - if (invariant.IsStatic) - throw new InvalidOperationException($"Invariant method must be an instance method: {invariant.FullName}"); - - if (invariant.Parameters.Count != 0) - throw new InvalidOperationException($"Invariant method must be parameterless: {invariant.FullName}"); - - if (invariant.ReturnType.MetadataType != MetadataType.Void) - throw new InvalidOperationException($"Invariant method must return void: {invariant.FullName}"); - - if (!invariant.HasBody) - throw new InvalidOperationException($"Invariant method must have a body: {invariant.FullName}"); - - return invariant; - } - - private static void InsertInvariantCallBefore(ILProcessor il, Instruction before, MethodDefinition invariantMethod) - { - // instance.Invariant(); - il.InsertBefore(before, Instruction.Create(OpCodes.Ldarg_0)); - il.InsertBefore(before, Instruction.Create(OpCodes.Call, invariantMethod)); - } - - private static bool TryExtractContractBlock(MethodDefinition method, out List contractBlock) - { - // v1: contract block must be explicitly terminated by Contract.EndContractBlock(). - // This makes extraction deterministic without needing sequence points. - contractBlock = new List(); - - IList instructions = method.Body.Instructions; - if (instructions.Count == 0) - return false; - - int endIndex = -1; - for (int i = 0; i < instructions.Count; i++) - { - Instruction inst = instructions[i]; - contractBlock.Add(inst); - - if (IsEndContractBlockCall(inst)) - { - endIndex = i; - break; - } - } - - if (endIndex < 0) - { - // No explicit contract block end. - contractBlock.Clear(); - return false; - } - - // Also include trailing nops immediately after EndContractBlock, as they often belong - // to the same source statement / sequence point. - for (int i = endIndex + 1; i < instructions.Count; i++) - { - if (instructions[i].OpCode != OpCodes.Nop) - break; - contractBlock.Add(instructions[i]); - } - - return true; - } - - private static bool IsEnsuresCall(Instruction inst) - => IsStaticCallToContractMethod(inst, "Ensures"); - - private static bool IsEndContractBlockCall(Instruction inst) - => IsStaticCallToContractMethod(inst, "EndContractBlock"); - - private static bool IsResultCall(Instruction inst) - => IsStaticCallToContractMethod(inst, "Result"); - - private static bool IsStaticCallToContractMethod(Instruction inst, string methodName) - { - if (inst.OpCode != OpCodes.Call) - return false; - - if (inst.Operand is not MethodReference mr) - return false; - - if (mr.Name != methodName) - return false; - - // Handle generic instance method as well. - string declaringType = mr.DeclaringType.FullName; - return declaringType == ContractTypeFullName; - } - - private static Instruction CloneInstruction(Instruction inst) - { - // Minimal cloning sufficient for contract block statements. - // We intentionally do not support cloning branch targets / exception handler operands in v1. - if (inst.Operand is null) - return Instruction.Create(inst.OpCode); - - return inst.Operand switch - { - sbyte b => Instruction.Create(inst.OpCode, b), - byte b => Instruction.Create(inst.OpCode, (sbyte)b), // Cecil uses sbyte for short forms. - int i => Instruction.Create(inst.OpCode, i), - long l => Instruction.Create(inst.OpCode, l), - float f => Instruction.Create(inst.OpCode, f), - double d => Instruction.Create(inst.OpCode, d), - string s => Instruction.Create(inst.OpCode, s), - MethodReference mr => Instruction.Create(inst.OpCode, mr), - FieldReference fr => Instruction.Create(inst.OpCode, fr), - TypeReference tr => Instruction.Create(inst.OpCode, tr), - ParameterDefinition pd => Instruction.Create(inst.OpCode, pd), - VariableDefinition vd => Instruction.Create(inst.OpCode, vd), - _ => throw new NotSupportedException( - $"Unsupported operand type in contract block cloning: {inst.Operand.GetType().FullName} (opcode {inst.OpCode}).") - }; - } + + } \ No newline at end of file diff --git a/DesignContracts/Rewriter/TypeHandler.cs b/DesignContracts/Rewriter/TypeHandler.cs new file mode 100644 index 0000000..ad81b2b --- /dev/null +++ b/DesignContracts/Rewriter/TypeHandler.cs @@ -0,0 +1,84 @@ +using Mono.Cecil; + +namespace Odin.DesignContracts.Rewriter; + +/// +/// Handles type-specific matters with respect to design contract rewriting. +/// +internal class TypeHandler +{ + private readonly TypeDefinition _target; + private MethodDefinition? _invariant; + private bool _invariantSearched = false; + + /// + /// Constructor + /// + /// + public TypeHandler(TypeDefinition target) + { + _target = target; + } + + /// + /// The enclosed Type being handled. + /// + public TypeDefinition Type => _target; + + public bool HasInvariant => InvariantMethod!=null; + + /// + /// Null if none was found. + /// + public MethodDefinition? InvariantMethod + { + get + { + if (!_invariantSearched) + { + FindInvariantMethodOrThrow(); + } + return _invariant; + } + } + + internal IReadOnlyList GetMembersToTryRewrite() + { + return _target.Methods.Select(c => new MemberHandler(c,this)).ToList(); + } + + internal void FindInvariantMethodOrThrow() + { + List candidates = _target.Methods + .Where(m => m.HasAnyAttributeIn(Names.InvariantAttributeFullNames)) + .ToList(); + + _invariantSearched = true; + + if (candidates.Count == 0) + return; + + if (candidates.Count > 1) + { + throw new InvalidOperationException( + $"Type '{_target.FullName}' has multiple invariant methods. " + + $"Exactly 1 method may be marked with either of: {string.Join(" | ", Names.InvariantAttributeFullNames)}."); + } + + MethodDefinition invariant = candidates[0]; + + if (invariant.IsStatic) + throw new InvalidOperationException($"Invariant method must be an instance method: {invariant.FullName}"); + + if (invariant.Parameters.Count != 0) + throw new InvalidOperationException($"Invariant method must be parameterless: {invariant.FullName}"); + + if (invariant.ReturnType.MetadataType != MetadataType.Void) + throw new InvalidOperationException($"Invariant method must return void: {invariant.FullName}"); + + if (!invariant.HasBody) + throw new InvalidOperationException($"Invariant method must have a body: {invariant.FullName}"); + + _invariant = invariant; + } +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs index 20f6c4e..72083ff 100644 --- a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs +++ b/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; -using TargetsUntooled; +using Targets; using ContractFailureKind = Odin.DesignContracts.ContractFailureKind; namespace Tests.Odin.DesignContracts.Rewriter; @@ -27,7 +27,7 @@ public void SetUp() public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour testCase) { Type targetType = GetTargetTestTypeFor(testCase); - using var context = new RewrittenAssemblyContext(targetType.Assembly); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); Assert.That(DesignContractOptions.Current.EnableInvariants, Is.True); Assert.That(DesignContractOptions.Current.EnablePostconditions, Is.True); @@ -54,7 +54,7 @@ public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour testCase) { Type targetType = GetTargetTestTypeFor(testCase); - using var context = new RewrittenAssemblyContext(targetType.Assembly); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; @@ -70,7 +70,7 @@ public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour test public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testCase) { Type targetType = GetTargetTestTypeFor(testCase); - using var context = new RewrittenAssemblyContext(targetType.Assembly); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; @@ -84,7 +84,7 @@ public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testC public void Pure_method_is_excluded_from_invariant_weaving([Values] AttributeFlavour testCase) { Type targetType = GetTargetTestTypeFor(testCase); - using var context = new RewrittenAssemblyContext(targetType.Assembly); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; @@ -100,7 +100,7 @@ public void Pure_method_is_excluded_from_invariant_weaving([Values] AttributeFla public void Pure_property_is_excluded_from_invariant_weaving([Values] AttributeFlavour testCase) { Type targetType = GetTargetTestTypeFor(testCase); - using var context = new RewrittenAssemblyContext(targetType.Assembly); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; @@ -115,7 +115,7 @@ public void Pure_property_is_excluded_from_invariant_weaving([Values] AttributeF public void Non_pure_property_is_woven_and_checks_invariants([Values] AttributeFlavour testCase) { Type targetType = GetTargetTestTypeFor(testCase); - using var context = new RewrittenAssemblyContext(targetType.Assembly); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; @@ -147,7 +147,7 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute Type invariantAttributeTestCaseType = GetClassInvariantAttributeTypeFor(firstInvariantFlavour); string originalPath = firstType.Assembly.Location; - using var temp = new TempDirectory(); + using TempDirectory temp = new TempDirectory(); string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); File.Copy(originalPath, inputPath, overwrite: true); @@ -161,9 +161,9 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute .First(m => m.Name == nameof(OdinInvariantTarget.Increment)); // Add a second invariant attribute to a different method. - var ctor = invariantAttributeTestCaseType.GetConstructor(Type.EmptyTypes) - ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); - var ctorRef = assemblyDefinition.MainModule.ImportReference(ctor); + ConstructorInfo ctor = invariantAttributeTestCaseType.GetConstructor(Type.EmptyTypes) + ?? throw new InvalidOperationException("Invariant attribute must have a parameterless ctor."); + MethodReference? ctorRef = assemblyDefinition.MainModule.ImportReference(ctor); increment.CustomAttributes.Add(new CustomAttribute(ctorRef)); assemblyDefinition.Write(inputPath); diff --git a/DesignContracts/TargetsTooled/BclInvariantTarget.cs b/DesignContracts/TargetsTooled/BclInvariantTarget.cs deleted file mode 100644 index 6ee2804..0000000 --- a/DesignContracts/TargetsTooled/BclInvariantTarget.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Odin.DesignContracts; - -namespace TargetsTooled -{ - /// - /// The rewriter is expected to inject - /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. - /// - public sealed class BclInvariantTarget - { - private int _value; - - public BclInvariantTarget(int value) - { - _value = value; - } - - [System.Diagnostics.Contracts.ContractInvariantMethod] - private void ObjectInvariant() - { - Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); - } - - public void Increment() - { - _value++; - } - - public void MakeInvalid() - { - _value = -1; - } - - [System.Diagnostics.Contracts.Pure] - public int PureGetValue() => _value; - - [System.Diagnostics.Contracts.Pure] - public int PureProperty => _value; - - public int NonPureProperty => _value; - } -} diff --git a/DesignContracts/TargetsTooled/OdinInvariantTarget.cs b/DesignContracts/TargetsTooled/OdinInvariantTarget.cs deleted file mode 100644 index 574d47a..0000000 --- a/DesignContracts/TargetsTooled/OdinInvariantTarget.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Odin.DesignContracts; - -namespace TargetsTooled -{ - /// - /// The rewriter is expected to inject - /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. - /// - public sealed class OdinInvariantTarget - { - private int _value; - - public OdinInvariantTarget(int value) - { - _value = value; - } - - [global::Odin.DesignContracts.ClassInvariantMethod] - private void ObjectInvariant() - { - Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); - } - - public void Increment() - { - _value++; - } - - public void MakeInvalid() - { - _value = -1; - } - - [global::Odin.DesignContracts.Pure] - public int PureGetValue() => _value; - - [global::Odin.DesignContracts.Pure] - public int PureProperty => _value; - - public int NonPureProperty => _value; - } -} diff --git a/DesignContracts/TargetsTooled/TargetsTooled.csproj b/DesignContracts/TargetsTooled/TargetsTooled.csproj index 7e5326c..b5060ba 100644 --- a/DesignContracts/TargetsTooled/TargetsTooled.csproj +++ b/DesignContracts/TargetsTooled/TargetsTooled.csproj @@ -1,15 +1,32 @@  - net8.0;net9.0;net10.0 + net10.0 true enable Target classes for the Design Contract rewriter to rewrite - 1591;1573; + 1591;1573; - - + + BclInvariantTarget.cs + + + OdinInvariantTarget.cs + - + + + + + + + + + \ No newline at end of file diff --git a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs b/DesignContracts/TargetsUntooled/BclInvariantTarget.cs index a64d20f..fed50af 100644 --- a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/BclInvariantTarget.cs @@ -1,6 +1,6 @@ using Odin.DesignContracts; -namespace TargetsUntooled +namespace Targets { /// /// The rewriter is expected to inject @@ -25,11 +25,23 @@ public void Increment() { _value++; } + + public async Task AsyncIncrement() + { + _value++; + return await Task.FromResult(_value); + } public void MakeInvalid() { _value = -1; } + + public async Task AsyncMakeInvalid() + { + _value = -1; + return await Task.FromResult(_value); + } [System.Diagnostics.Contracts.Pure] public int PureGetValue() => _value; @@ -38,5 +50,6 @@ public void MakeInvalid() public int PureProperty => _value; public int NonPureProperty => _value; + } } diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs index ad9c45c..60820e3 100644 --- a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs @@ -1,6 +1,6 @@ using Odin.DesignContracts; -namespace TargetsUntooled +namespace Targets { /// /// The rewriter is expected to inject @@ -25,11 +25,24 @@ public void Increment() { _value++; } + + public async Task AsyncIncrement() + { + _value++; + return await Task.FromResult(_value); + } + public void MakeInvalid() { _value = -1; } + + public async Task AsyncMakeInvalid() + { + _value = -1; + return await Task.FromResult(_value); + } [global::Odin.DesignContracts.Pure] public int PureGetValue() => _value; diff --git a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets index abf5df0..bf29273 100644 --- a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets +++ b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets @@ -9,6 +9,8 @@ @@ -16,17 +18,22 @@ <_OdinDcRewriterPathCandidate>$(MSBuildThisFileDirectory)..\tools\net$(_OdinNetVersion)\any\Odin.DesignContracts.Rewriter.dll <_OdinDcRewriterPath Condition="Exists('$(_OdinDcRewriterPathCandidate)')">$(_OdinDcRewriterPathCandidate) <_OdinDcRewriterPath Condition="'$(_OdinDcRewriterPath)'==''">$(MSBuildThisFileDirectory)..\tools\net10.0\any\Odin.DesignContracts.Rewriter.dll - <_OdinDcWeavedAssembly>$(IntermediateOutputPath)$(AssemblyName).odindc.weaved$(TargetExt) - <_OdinDcWeavedPdb>$(IntermediateOutputPath)$(AssemblyName).odindc.weaved.pdb + <_OdinDcWeavedAssembly>$(IntermediateOutputPath)$(AssemblyName).odin-design-contracts-weaved$(TargetExt) + <_OdinDcWeavedPdb>$(IntermediateOutputPath)$(AssemblyName).odin-design-contracts-weaved.pdb - + + + + + + - + From 93e4766ca09ffa980de3dc10c9382e4f6556bb37 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Tue, 23 Dec 2025 11:53:43 +0200 Subject: [PATCH 14/27] Saving progress... --- DesignContracts/Core/Postcondition.cs | 56 +++++++++-------------- DesignContracts/Rewriter/MemberHandler.cs | 18 ++++---- DesignContracts/Rewriter/Names.cs | 2 +- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/DesignContracts/Core/Postcondition.cs b/DesignContracts/Core/Postcondition.cs index 2e10fe9..e7b9430 100644 --- a/DesignContracts/Core/Postcondition.cs +++ b/DesignContracts/Core/Postcondition.cs @@ -7,41 +7,6 @@ namespace Odin.DesignContracts /// public static class Postcondition { - /// - /// Represents the return value of the enclosing method for use within postconditions. - /// - /// The enclosing method return type. - /// - /// The value returned by the enclosing method. - /// - /// - /// This API is intended to be used only inside postconditions expressed via - /// . - /// - /// When postconditions are enabled, it is expected that a build-time rewriter will - /// replace calls to this method with the actual method return value. - /// - /// Without rewriting, this method returns default. - /// - public static T Result() - { - return default!; - } - - /// - /// Marks the end of a contract block at the start of a method. - /// - /// - /// This method exists to support classic Design-by-Contract authoring styles and - /// build-time rewriting. - /// - /// A rewriter may use this as a hint to know where contract declarations end and - /// normal method logic begins. - /// - public static void EndContractBlock() - { - } - /// /// Specifies a postcondition that must hold true when the enclosing method returns. /// @@ -69,5 +34,26 @@ public static void Ensures(bool condition, string? userMessage = null, string? c conditionText); } } + + /// + /// Represents the return value of the enclosing method for use within postconditions. + /// + /// The enclosing method return type. + /// + /// The value returned by the enclosing method. + /// + /// + /// This API is intended to be used only inside postconditions expressed via + /// . + /// + /// When postconditions are enabled, it is expected that a build-time rewriter will + /// replace calls to this method with the actual method return value. + /// + /// Without rewriting, this method returns default. + /// + public static T Result() + { + return default!; + } } } \ No newline at end of file diff --git a/DesignContracts/Rewriter/MemberHandler.cs b/DesignContracts/Rewriter/MemberHandler.cs index a0fd7c4..deeabd0 100644 --- a/DesignContracts/Rewriter/MemberHandler.cs +++ b/DesignContracts/Rewriter/MemberHandler.cs @@ -154,8 +154,9 @@ public InvariantWeavingRequirement IsInvariantToBeWeaved() public ResultValue> TryExtractPostconditions() { - // v1: contract block must be explicitly terminated by Contract.EndContractBlock(). - // This makes extraction deterministic without needing sequence points. + // For V1 we will simply attempt to extract any Postcondition.Ensures() + // calls from the method body if they exist. MD. + // List postconditions = new List(); IList instructions = Method.Body.Instructions; @@ -195,17 +196,14 @@ public ResultValue> TryExtractPostconditions() } - public bool IsEnsuresCall(Instruction inst) - => IsStaticCallToContractMethod(inst, "Ensures"); - - public bool IsEndContractBlockCall(Instruction inst) - => IsStaticCallToContractMethod(inst, "EndContractBlock"); + public bool IsPostconditionEnsuresCall(Instruction inst) + => IsStaticCallToPostconditionMethod(inst, "Ensures"); public bool IsResultCall(Instruction inst) - => IsStaticCallToContractMethod(inst, "Result"); + => IsStaticCallToPostconditionMethod(inst, "Result"); - private static bool IsStaticCallToContractMethod(Instruction inst, string methodName) + public static bool IsStaticCallToPostconditionMethod(Instruction inst, string methodName) { if (inst.OpCode != OpCodes.Call) return false; @@ -218,7 +216,7 @@ private static bool IsStaticCallToContractMethod(Instruction inst, string method // Handle generic instance method as well. string declaringType = mr.DeclaringType.FullName; - return declaringType == Names.OdinContractTypeFullName; + return declaringType == Names.OdinPreconditionEnsuresTypeFullName; } /// diff --git a/DesignContracts/Rewriter/Names.cs b/DesignContracts/Rewriter/Names.cs index 1ccc1a1..5e695a8 100644 --- a/DesignContracts/Rewriter/Names.cs +++ b/DesignContracts/Rewriter/Names.cs @@ -7,7 +7,7 @@ internal static class Names { internal const string BclContractNamespace = "System.Diagnostics.Contracts"; internal const string OdinContractNamespace = "Odin.DesignContracts"; - internal const string OdinContractTypeFullName = OdinContractNamespace + ".Contract"; + internal const string OdinPreconditionEnsuresTypeFullName = OdinContractNamespace + ".Precondition"; internal const string OdinInvariantAttributeFullName = OdinContractNamespace + ".ClassInvariantMethodAttribute"; internal const string BclInvariantAttributeFullName = BclContractNamespace + ".ContractInvariantMethodAttribute"; internal const string OdinPureAttributeFullName = OdinContractNamespace + ".PureAttribute"; From 98e5edeafe914a1f329588229dac823197b1a0f0 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Tue, 23 Dec 2025 16:00:36 +0200 Subject: [PATCH 15/27] Saving progress... --- .../Abstractions/JobDetails.cs | 2 +- .../Core/BackgroundProcessingOptions.cs | 2 +- .../Core/DependencyInjectionExtensions.cs | 8 +- .../Hangfire/HangfireBackgroundProcessor.cs | 6 +- .../HangfirePolicyAuthorisationFilter.cs | 2 +- .../Hangfire/HangfireServiceInjector.cs | 4 +- Data/SqlScriptsRunner/SqlScriptsRunner.cs | 10 +- .../Analyzers/PostconditionsAnalyzer.cs | 2 +- DesignContracts/Core/Contract.cs | 130 +++++++++++++++++- DesignContracts/Core/ContractAttributes.cs | 2 +- .../Core/ContractFailedEventArgs.cs | 2 +- ...nContractOptions.cs => ContractOptions.cs} | 14 +- DesignContracts/Core/Postcondition.cs | 59 -------- DesignContracts/Core/Precondition.cs | 79 ----------- DesignContracts/CoreTests/InvariantTests.cs | 6 +- .../CoreTests/PreconditionTests.cs | 8 +- .../{MemberHandler.cs => MethodHandler.cs} | 57 +++----- DesignContracts/Rewriter/Names.cs | 2 +- DesignContracts/Rewriter/Program.cs | 3 +- DesignContracts/Rewriter/TypeHandler.cs | 7 +- .../RewriterTests/CecilAssemblyContext.cs | 62 +++++++++ .../RewriterTests/MethodHandlerTests.cs | 33 +++++ ...avingRewriterTests.cs => RewriterTests.cs} | 46 ++++--- ...Tests.Odin.DesignContracts.Rewriter.csproj | 2 +- .../RewriterTests/TypeHandlerTests.cs | 25 ++++ .../TargetsTooled/TargetsTooled.csproj | 9 +- .../TargetsUntooled/BclInvariantTarget.cs | 13 +- .../TargetsUntooled/NoInvariantTarget.cs | 18 +++ .../TargetsUntooled/OdinInvariantTarget.cs | 15 +- .../TargetsUntooled/TargetsUntooled.csproj | 3 +- Email/Core/Attachment.cs | 6 +- Email/Core/DependencyInjectionExtensions.cs | 2 +- Email/Core/EmailAddress.cs | 2 +- Email/Core/EmailMessage.cs | 6 +- Email/Core/EmailSendingOptions.cs | 2 +- Email/Mailgun/MailgunEmailSender.cs | 22 +-- Email/Mailgun/MailgunServiceInjector.cs | 2 +- Email/Office365/Office365EmailSender.cs | 10 +- Email/Office365/Office365ServiceInjector.cs | 2 +- Email/Tests/EmailTestConfiguration.cs | 4 +- .../Mailgun/MailgunEmailSenderTestBuilder.cs | 4 +- .../Office365EmailSenderTestBuilder.cs | 4 +- RemoteFiles/Abstractions/RemoteFileInfo.cs | 4 +- RemoteFiles/Core/ConnectionSettingsHelper.cs | 2 +- .../Core/DependencyInjectionExtensions.cs | 2 +- RemoteFiles/Core/RemoteFileSessionFactory.cs | 6 +- RemoteFiles/SFTP/SftpRemoteFileSession.cs | 20 +-- System/Activator2/Activator2.cs | 6 +- Utility/Tax/TaxUtility.cs | 2 +- .../VaryingValues/ValueChangesListProvider.cs | 10 +- 50 files changed, 443 insertions(+), 306 deletions(-) rename DesignContracts/Core/{DesignContractOptions.cs => ContractOptions.cs} (75%) delete mode 100644 DesignContracts/Core/Postcondition.cs delete mode 100644 DesignContracts/Core/Precondition.cs rename DesignContracts/Rewriter/{MemberHandler.cs => MethodHandler.cs} (86%) create mode 100644 DesignContracts/RewriterTests/CecilAssemblyContext.cs create mode 100644 DesignContracts/RewriterTests/MethodHandlerTests.cs rename DesignContracts/RewriterTests/{InvariantWeavingRewriterTests.cs => RewriterTests.cs} (81%) create mode 100644 DesignContracts/RewriterTests/TypeHandlerTests.cs create mode 100644 DesignContracts/TargetsUntooled/NoInvariantTarget.cs diff --git a/BackgroundProcessing/Abstractions/JobDetails.cs b/BackgroundProcessing/Abstractions/JobDetails.cs index 752c865..d11ba90 100644 --- a/BackgroundProcessing/Abstractions/JobDetails.cs +++ b/BackgroundProcessing/Abstractions/JobDetails.cs @@ -14,7 +14,7 @@ public sealed record JobDetails /// public JobDetails(string jobId, DateTimeOffset scheduledFor) { - Precondition.Requires(!string.IsNullOrWhiteSpace(jobId)); + Contract.Requires(!string.IsNullOrWhiteSpace(jobId)); JobId = jobId; ScheduledFor = scheduledFor; } diff --git a/BackgroundProcessing/Core/BackgroundProcessingOptions.cs b/BackgroundProcessing/Core/BackgroundProcessingOptions.cs index 9e25e1f..dbb913e 100644 --- a/BackgroundProcessing/Core/BackgroundProcessingOptions.cs +++ b/BackgroundProcessing/Core/BackgroundProcessingOptions.cs @@ -18,7 +18,7 @@ public string Provider get => _provider; set { - Precondition.Requires(!string.IsNullOrWhiteSpace(value)); + Contract.Requires(!string.IsNullOrWhiteSpace(value)); _provider = value.Replace("BackgroundProcessor", "", StringComparison.OrdinalIgnoreCase); } } diff --git a/BackgroundProcessing/Core/DependencyInjectionExtensions.cs b/BackgroundProcessing/Core/DependencyInjectionExtensions.cs index 27cf2d1..0cce666 100644 --- a/BackgroundProcessing/Core/DependencyInjectionExtensions.cs +++ b/BackgroundProcessing/Core/DependencyInjectionExtensions.cs @@ -80,7 +80,7 @@ public static void AddOdinBackgroundProcessing( this IServiceCollection serviceCollection, IConfiguration configuration, IConfigurationSection configurationSection, Func? sqlServerConnectionStringFactory = null) { - Precondition.RequiresNotNull(configurationSection); + Contract.RequiresNotNull(configurationSection); BackgroundProcessingOptions options = new BackgroundProcessingOptions(); configurationSection.Bind(options); @@ -131,8 +131,8 @@ public static void AddOdinBackgroundProcessing( /// public static IApplicationBuilder UseBackgroundProcessing(this IApplicationBuilder app, IServiceProvider appServices) { - Precondition.RequiresNotNull(appServices); - Precondition.RequiresNotNull(app); + Contract.RequiresNotNull(appServices); + Contract.RequiresNotNull(app); BackgroundProcessingOptions options = appServices.GetRequiredService(); if (options.Provider == BackgroundProcessingProviders.Null) @@ -142,7 +142,7 @@ public static IApplicationBuilder UseBackgroundProcessing(this IApplicationBuild string providerAssemblyName = $"{Constants.RootNamespace}.{options.Provider}"; ResultValue serviceInjectorCreation = - Odin.System.Activator2.TryCreate($"{providerAssemblyName}ServiceInjector",providerAssemblyName); + Activator2.TryCreate($"{providerAssemblyName}ServiceInjector",providerAssemblyName); if (serviceInjectorCreation.IsSuccess) { diff --git a/BackgroundProcessing/Hangfire/HangfireBackgroundProcessor.cs b/BackgroundProcessing/Hangfire/HangfireBackgroundProcessor.cs index 93bc6a9..bdb9a88 100644 --- a/BackgroundProcessing/Hangfire/HangfireBackgroundProcessor.cs +++ b/BackgroundProcessing/Hangfire/HangfireBackgroundProcessor.cs @@ -25,9 +25,9 @@ public sealed class HangfireBackgroundProcessor : IBackgroundProcessor /// public HangfireBackgroundProcessor(IRecurringJobManagerV2 recurringJobManager, IBackgroundJobClient jobClient, ILoggerWrapper logger) { - Precondition.RequiresNotNull(recurringJobManager); - Precondition.RequiresNotNull(jobClient); - Precondition.RequiresNotNull(logger); + Contract.RequiresNotNull(recurringJobManager); + Contract.RequiresNotNull(jobClient); + Contract.RequiresNotNull(logger); _recurringJobManager = recurringJobManager; _jobClient = jobClient; _logger = logger; diff --git a/BackgroundProcessing/Hangfire/HangfirePolicyAuthorisationFilter.cs b/BackgroundProcessing/Hangfire/HangfirePolicyAuthorisationFilter.cs index 8815666..8cc0171 100644 --- a/BackgroundProcessing/Hangfire/HangfirePolicyAuthorisationFilter.cs +++ b/BackgroundProcessing/Hangfire/HangfirePolicyAuthorisationFilter.cs @@ -20,7 +20,7 @@ public sealed class HangfirePolicyAuthorisationFilter : IDashboardAuthorizationF /// public HangfirePolicyAuthorisationFilter(string policyName) { - Precondition.Requires(!string.IsNullOrWhiteSpace(policyName)); + Contract.Requires(!string.IsNullOrWhiteSpace(policyName)); _policyName = policyName.Trim(); } diff --git a/BackgroundProcessing/Hangfire/HangfireServiceInjector.cs b/BackgroundProcessing/Hangfire/HangfireServiceInjector.cs index b7fac30..56a7317 100644 --- a/BackgroundProcessing/Hangfire/HangfireServiceInjector.cs +++ b/BackgroundProcessing/Hangfire/HangfireServiceInjector.cs @@ -16,8 +16,8 @@ public class HangfireServiceInjector : IBackgroundProcessorServiceInjector public void TryAddBackgroundProcessor(IServiceCollection serviceCollection, IConfiguration configuration, IConfigurationSection backgroundProcessingSection, Func? connectionStringFactory = null) { - Precondition.RequiresNotNull(serviceCollection); - Precondition.RequiresNotNull(backgroundProcessingSection); + Contract.RequiresNotNull(serviceCollection); + Contract.RequiresNotNull(backgroundProcessingSection); IConfigurationSection? providerSection = backgroundProcessingSection.GetSection(BackgroundProcessingProviders.Hangfire); if (providerSection == null) diff --git a/Data/SqlScriptsRunner/SqlScriptsRunner.cs b/Data/SqlScriptsRunner/SqlScriptsRunner.cs index 877122b..fd654a4 100644 --- a/Data/SqlScriptsRunner/SqlScriptsRunner.cs +++ b/Data/SqlScriptsRunner/SqlScriptsRunner.cs @@ -43,15 +43,15 @@ private SqlScriptsRunner(Assembly assemblyWithEmbeddedScripts) { // Contract.Requires(logger); // _logger = logger; - Precondition.Requires(assemblyWithEmbeddedScripts!=null!); + Contract.Requires(assemblyWithEmbeddedScripts!=null!); _assemblyWithEmbeddedScripts = assemblyWithEmbeddedScripts!; } public static ResultValue CreateFromConnectionStringName(string connectionStringName, Assembly assemblyWithEmbeddedScripts, IConfiguration configuration) { - Precondition.Requires(configuration!=null!); - Precondition.Requires(assemblyWithEmbeddedScripts!=null!); + Contract.Requires(configuration!=null!); + Contract.Requires(assemblyWithEmbeddedScripts!=null!); SqlScriptsRunner runner = new SqlScriptsRunner(assemblyWithEmbeddedScripts) { ConnectionString = configuration.GetConnectionString(connectionStringName)! @@ -76,8 +76,8 @@ public static ResultValue CreateFromConnectionStringName(strin public static ResultValue CreateFromConnectionString(string connectionString, Assembly assemblyWithEmbeddedScripts) { - Precondition.Requires(assemblyWithEmbeddedScripts!=null!); - Precondition.Requires(!string.IsNullOrWhiteSpace(connectionString)); + Contract.Requires(assemblyWithEmbeddedScripts!=null!); + Contract.Requires(!string.IsNullOrWhiteSpace(connectionString)); SqlScriptsRunner runner = new SqlScriptsRunner(assemblyWithEmbeddedScripts) { ConnectionString = connectionString diff --git a/DesignContracts/Analyzers/PostconditionsAnalyzer.cs b/DesignContracts/Analyzers/PostconditionsAnalyzer.cs index 7789411..72b21a7 100644 --- a/DesignContracts/Analyzers/PostconditionsAnalyzer.cs +++ b/DesignContracts/Analyzers/PostconditionsAnalyzer.cs @@ -173,7 +173,7 @@ private static bool IsRewriterEnabled(SyntaxNodeAnalysisContext context) if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions .TryGetValue("build_property.OdinDesignContractsRewriterEnabled", out string? enabledText)) { - return string.Equals(enabledText?.Trim(), "true", System.StringComparison.OrdinalIgnoreCase); + return string.Equals(enabledText?.Trim(), "true", StringComparison.OrdinalIgnoreCase); } return false; diff --git a/DesignContracts/Core/Contract.cs b/DesignContracts/Core/Contract.cs index 5ae4773..1513860 100644 --- a/DesignContracts/Core/Contract.cs +++ b/DesignContracts/Core/Contract.cs @@ -11,6 +11,105 @@ namespace Odin.DesignContracts /// public static class Contract { + /// + /// Specifies a precondition that must hold true when the enclosing method is called. + /// + /// The precondition that is required to be true. + /// Optional English description of what the precondition is. + /// Optional pseudo-code representation of the condition expression. + /// + /// Thrown when is false. + /// + public static void Requires(bool precondition, string? userMessage = null, string? conditionText = null) + { + if (!precondition) ReportFailure(ContractFailureKind.Precondition, userMessage, conditionText); + } + + /// + /// Specifies a precondition that must hold true when the enclosing method is called + /// and throws a specific exception type when the precondition fails. + /// + /// + /// The type of exception to throw when the precondition fails. + /// The type must have a public constructor that accepts a single parameter. + /// + /// The condition that must be true. + /// Optional user readable message describing the precondition. + /// Optional user readable message describing the precondition. + /// + /// Thrown when the specified exception type cannot be constructed. + /// + /// + /// An instance of when is false. + /// + public static void Requires(bool precondition, string? userMessage = null, + string? conditionText = null) + where TException : Exception + { + if (precondition) return; + + // Try to honor the requested exception type first. + string message = BuildFailureMessage(ContractFailureKind.Precondition, userMessage, conditionText); + + Exception? exception = null; + try + { + exception = (Exception?)Activator.CreateInstance(typeof(TException), message); + } + catch + { + // Swallow and fall back to ContractException. + } + + if (exception is not null) + { + throw exception; + } + + // Fall back to standard handling if we cannot construct TException. + ReportFailure(ContractFailureKind.Precondition, userMessage, conditionText: null); + } + + /// + /// Requires that argument be not null. If it is, raises an ArgumentNullException. + /// + /// + /// Defaults to 'Argument must not be null' + /// Optional pseudo-code representation of the not null expression. + public static void RequiresNotNull(object? argument, string? userMessage = "Argument must not be null" + , string? conditionText = null) + { + Requires(argument != null, userMessage, conditionText); + } + + /// + /// Specifies a postcondition that must hold true when the enclosing method returns. + /// + /// The condition that must be true. + /// An optional message describing the postcondition. + /// An optional text representation of the condition expression. + /// + /// Postconditions are evaluated only when is true. + /// Calls to this method become no-ops when postconditions are disabled. + /// It is expected that source-generated code will invoke this method at + /// appropriate points (typically immediately before method exit). + /// + public static void Ensures(bool condition, string? userMessage = null, string? conditionText = null) + { + if (!ContractOptions.Current.EnablePostconditions) + { + return; + } + + if (!condition) + { + ReportFailure( + ContractFailureKind.Postcondition, + userMessage, + conditionText); + } + } + /// /// Occurs when a contract fails and before a is thrown. /// @@ -27,7 +126,7 @@ internal static void ReportFailure(ContractFailureKind kind, string? userMessage ContractFailed?.Invoke(null, args); if (args.Handled) { - // A handler chose to manage the failure; do not throw by default. + // A handler chose to manage the failure; do not throw in this case... return; } @@ -87,14 +186,14 @@ internal static string BuildFailureMessage(ContractFailureKind kind, string? use /// public static void Invariant(bool condition, string? userMessage = null, string? conditionText = null) { - if (!DesignContractOptions.Current.EnableInvariants) + if (!ContractOptions.Current.EnableInvariants) { return; } if (!condition) { - Contract.ReportFailure( + ReportFailure( ContractFailureKind.Invariant, userMessage, conditionText); @@ -114,7 +213,7 @@ public static void Assert(bool condition, string? userMessage = null, string? co { if (!condition) { - Contract.ReportFailure( + ReportFailure( ContractFailureKind.Assertion, userMessage, conditionText); @@ -135,11 +234,32 @@ public static void Assume(bool condition, string? userMessage = null, string? co { if (!condition) { - Contract.ReportFailure( + ReportFailure( ContractFailureKind.Assumption, userMessage, conditionText); } } + + /// + /// Represents the return value of the enclosing method for use within postconditions. + /// + /// The enclosing method return type. + /// + /// The value returned by the enclosing method. + /// + /// + /// This API is intended to be used only inside postconditions expressed via + /// . + /// + /// When postconditions are enabled, it is expected that a build-time rewriter will + /// replace calls to this method with the actual method return value. + /// + /// Without rewriting, this method returns default. + /// + public static T Result() + { + return default!; + } } } \ No newline at end of file diff --git a/DesignContracts/Core/ContractAttributes.cs b/DesignContracts/Core/ContractAttributes.cs index 74d9893..be45fa1 100644 --- a/DesignContracts/Core/ContractAttributes.cs +++ b/DesignContracts/Core/ContractAttributes.cs @@ -5,7 +5,7 @@ namespace Odin.DesignContracts /// /// /// Methods marked with this attribute are expected to be private, parameterless, - /// and to invoke for each invariant. + /// and to invoke for each invariant. /// Source generators can use this attribute to discover and invoke invariant methods /// at appropriate points (for example, at the end of constructors and public methods). /// diff --git a/DesignContracts/Core/ContractFailedEventArgs.cs b/DesignContracts/Core/ContractFailedEventArgs.cs index 152d3d0..768d31b 100644 --- a/DesignContracts/Core/ContractFailedEventArgs.cs +++ b/DesignContracts/Core/ContractFailedEventArgs.cs @@ -1,7 +1,7 @@ namespace Odin.DesignContracts { /// - /// Provides data for the event. + /// Provides data for the event. /// public sealed class ContractFailedEventArgs : EventArgs { diff --git a/DesignContracts/Core/DesignContractOptions.cs b/DesignContracts/Core/ContractOptions.cs similarity index 75% rename from DesignContracts/Core/DesignContractOptions.cs rename to DesignContracts/Core/ContractOptions.cs index 58888fe..fa0663a 100644 --- a/DesignContracts/Core/DesignContractOptions.cs +++ b/DesignContracts/Core/ContractOptions.cs @@ -7,23 +7,23 @@ namespace Odin.DesignContracts /// Preconditions are always evaluated. This configuration controls whether runtime /// evaluation of postconditions and invariants are skipped or not. /// - public sealed class DesignContractOptions + public sealed class ContractOptions { - private static DesignContractOptions? _current; + private static ContractOptions? _current; /// /// Static facade to the current runtime instance of DesignContractOptions, which must be set /// early on in application startup by calling Initialize. /// /// - public static DesignContractOptions Current + public static ContractOptions Current { get { // Temporary hack so I can continue with testing the actual rewriting... if (_current is null) { - _current = new DesignContractOptions() { EnableInvariants = true, EnablePostconditions = true }; + _current = new ContractOptions() { EnableInvariants = true, EnablePostconditions = true }; } return _current; } @@ -36,14 +36,14 @@ public static DesignContractOptions Current /// /// /// - public static void Initialize(DesignContractOptions options) + public static void Initialize(ContractOptions options) => _current = options; /// /// Gets or sets a value indicating whether postconditions should be evaluated at runtime. /// /// - /// When false, calls to become no-ops. + /// When false, calls to become no-ops. /// public bool EnablePostconditions { get; init; } = true; @@ -51,7 +51,7 @@ public static void Initialize(DesignContractOptions options) /// Gets or sets a value indicating whether object invariants should be evaluated at runtime. ///
/// - /// When false, calls to become no-ops. + /// When false, calls to become no-ops. /// public bool EnableInvariants { get; init; } = true; } diff --git a/DesignContracts/Core/Postcondition.cs b/DesignContracts/Core/Postcondition.cs deleted file mode 100644 index e7b9430..0000000 --- a/DesignContracts/Core/Postcondition.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Odin.DesignContracts -{ - - /// - /// Provides methods for runtime validation and enforcement of postconditions, - /// ensuring that the supplier class has met their advertised\agreed obligations. - /// - public static class Postcondition - { - /// - /// Specifies a postcondition that must hold true when the enclosing method returns. - /// - /// The condition that must be true. - /// An optional message describing the postcondition. - /// An optional text representation of the condition expression. - /// - /// Postconditions are evaluated only when is true. - /// Calls to this method become no-ops when postconditions are disabled. - /// It is expected that source-generated code will invoke this method at - /// appropriate points (typically immediately before method exit). - /// - public static void Ensures(bool condition, string? userMessage = null, string? conditionText = null) - { - if (!DesignContractOptions.Current.EnablePostconditions) - { - return; - } - - if (!condition) - { - Contract.ReportFailure( - ContractFailureKind.Postcondition, - userMessage, - conditionText); - } - } - - /// - /// Represents the return value of the enclosing method for use within postconditions. - /// - /// The enclosing method return type. - /// - /// The value returned by the enclosing method. - /// - /// - /// This API is intended to be used only inside postconditions expressed via - /// . - /// - /// When postconditions are enabled, it is expected that a build-time rewriter will - /// replace calls to this method with the actual method return value. - /// - /// Without rewriting, this method returns default. - /// - public static T Result() - { - return default!; - } - } -} \ No newline at end of file diff --git a/DesignContracts/Core/Precondition.cs b/DesignContracts/Core/Precondition.cs deleted file mode 100644 index 6a0e68f..0000000 --- a/DesignContracts/Core/Precondition.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace Odin.DesignContracts; - -/// -/// Provides methods for runtime validation and enforcement of preconditions. -/// ensuring that the calling consumer has met the agreed\advertised requirements. -/// -public static class Precondition -{ - /// - /// Specifies a precondition that must hold true when the enclosing method is called. - /// - /// The precondition that is required to be true. - /// Optional English description of what the precondition is. - /// Optional pseudo-code representation of the condition expression. - /// - /// Thrown when is false. - /// - public static void Requires(bool precondition, string? userMessage = null, string? conditionText = null) - { - if (!precondition) Contract.ReportFailure(ContractFailureKind.Precondition, userMessage, conditionText); - } - - /// - /// Requires that argument be not null. If it is, raises an ArgumentNullException. - /// - /// - /// Defaults to 'Argument must not be null' - /// Optional pseudo-code representation of the not null expression. - public static void RequiresNotNull(object? argument, string? userMessage = "Argument must not be null" - , string? conditionText = null) - { - Requires(argument != null, userMessage, conditionText); - } - - /// - /// Specifies a precondition that must hold true when the enclosing method is called - /// and throws a specific exception type when the precondition fails. - /// - /// - /// The type of exception to throw when the precondition fails. - /// The type must have a public constructor that accepts a single parameter. - /// - /// The condition that must be true. - /// Optional user readable message describing the precondition. - /// Optional user readable message describing the precondition. - /// - /// Thrown when the specified exception type cannot be constructed. - /// - /// - /// An instance of when is false. - /// - public static void Requires(bool precondition, string? userMessage = null, - string? conditionText = null) - where TException : Exception - { - if (precondition) return; - - // Try to honor the requested exception type first. - string message = Contract.BuildFailureMessage(ContractFailureKind.Precondition, userMessage, conditionText); - - Exception? exception = null; - try - { - exception = (Exception?)Activator.CreateInstance(typeof(TException), message); - } - catch - { - // Swallow and fall back to ContractException. - } - - if (exception is not null) - { - throw exception; - } - - // Fall back to standard handling if we cannot construct TException. - Contract.ReportFailure(ContractFailureKind.Precondition, userMessage, conditionText: null); - } -} \ No newline at end of file diff --git a/DesignContracts/CoreTests/InvariantTests.cs b/DesignContracts/CoreTests/InvariantTests.cs index 0cbc8f6..dcf3659 100644 --- a/DesignContracts/CoreTests/InvariantTests.cs +++ b/DesignContracts/CoreTests/InvariantTests.cs @@ -10,7 +10,7 @@ public sealed class InvariantTests [SetUp] public void SetUp() { - DesignContractOptions.Initialize(new DesignContractOptions + ContractOptions.Initialize(new ContractOptions { EnableInvariants = true, EnablePostconditions = true @@ -20,8 +20,8 @@ public void SetUp() [Test] public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour testCase) { - Assert.That(DesignContractOptions.Current.EnableInvariants, Is.True); - Assert.That(DesignContractOptions.Current.EnablePostconditions, Is.True); + Assert.That(ContractOptions.Current.EnableInvariants, Is.True); + Assert.That(ContractOptions.Current.EnablePostconditions, Is.True); ContractException? ex = Assert.Throws(() => { diff --git a/DesignContracts/CoreTests/PreconditionTests.cs b/DesignContracts/CoreTests/PreconditionTests.cs index 1f1e680..ad1aed0 100644 --- a/DesignContracts/CoreTests/PreconditionTests.cs +++ b/DesignContracts/CoreTests/PreconditionTests.cs @@ -20,7 +20,7 @@ public sealed class PreconditionTests [TestCase(null, "(arg==0)", "Precondition not met: (arg==0)")] public void Requires_throws_exception_with_correct_message_on_precondition_failure(string conditionDescription, string? conditionText, string expectedExceptionMessage) { - ContractException? ex = Assert.Throws(() => Precondition.Requires(false, conditionDescription,conditionText)); + ContractException? ex = Assert.Throws(() => Contract.Requires(false, conditionDescription,conditionText)); Assert.That(ex, Is.Not.Null); Assert.That(ex!.Message, Is.EqualTo(expectedExceptionMessage), "Exception message is incorrect"); } @@ -28,14 +28,14 @@ public void Requires_throws_exception_with_correct_message_on_precondition_failu [Test] public void Requires_does_not_throw_exception_on_precondition_success() { - Assert.DoesNotThrow(() => Precondition.Requires(true, "Message"), "Precondition success must not throw an Exception"); + Assert.DoesNotThrow(() => Contract.Requires(true, "Message"), "Precondition success must not throw an Exception"); } [Test] public void Requires_not_null_throws_contract_exception_if_argument_null() { ContractException? exception = Assert.Throws(() => - Precondition.RequiresNotNull(null as string, "myArg is required.")); + Contract.RequiresNotNull(null as string, "myArg is required.")); Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Is.EqualTo("Precondition not met: myArg is required.")); @@ -51,7 +51,7 @@ public sealed class PreconditionGenericTests where TException : Exce [Test] public void Requires_throws_specific_exception_on_precondition_failure() { - TException? ex = Assert.Throws(() => Precondition.Requires(false, "msg")); + TException? ex = Assert.Throws(() => Contract.Requires(false, "msg")); Assert.That(ex, Is.Not.Null); Assert.That(ex, Is.InstanceOf()); diff --git a/DesignContracts/Rewriter/MemberHandler.cs b/DesignContracts/Rewriter/MethodHandler.cs similarity index 86% rename from DesignContracts/Rewriter/MemberHandler.cs rename to DesignContracts/Rewriter/MethodHandler.cs index deeabd0..3742815 100644 --- a/DesignContracts/Rewriter/MemberHandler.cs +++ b/DesignContracts/Rewriter/MethodHandler.cs @@ -9,11 +9,11 @@ namespace Odin.DesignContracts.Rewriter; /// Handles member-specific matters with respect to design contract rewriting. /// Note: MemberHandler includes methods AND property accessors... ///
-internal class MemberHandler +internal class MethodHandler { private readonly TypeHandler _parentHandler; - public MemberHandler(MethodDefinition method, TypeHandler parentHandler) + public MethodHandler(MethodDefinition method, TypeHandler parentHandler) { Method = method; _parentHandler = parentHandler; @@ -35,7 +35,7 @@ public bool TryRewrite() InvariantWeavingRequirement invariantsToDo = IsInvariantToBeWeaved(); ResultValue> postconditionsExtracted = - TryExtractPostconditions(); + TryExtractPostconditionCalls(); if (!postconditionsExtracted.IsSuccess && !invariantsToDo.OnEntry && !invariantsToDo.OnExit) @@ -95,9 +95,6 @@ public bool TryRewrite() { foreach (Instruction inst in postconditionsExtracted.Value) { - if (IsEndContractBlockCall(inst)) - continue; - Instruction cloned = inst.CloneInstruction(); if (!isVoid && IsResultCall(cloned)) @@ -152,12 +149,18 @@ public InvariantWeavingRequirement IsInvariantToBeWeaved() }; } - public ResultValue> TryExtractPostconditions() + /// + /// Returns success if 1 or more postcondition Ensures() calls were found. + /// + /// + public ResultValue> TryExtractPostconditionCalls() { - // For V1 we will simply attempt to extract any Postcondition.Ensures() - // calls from the method body if they exist. MD. - // - List postconditions = new List(); + // For V1 we will attempt to simply extract any Postcondition.Ensures() + // calls from the method body if they exist. I am a total noob at IL so have no clue + // about finding and moving postcondition calls inside conditionals. + // Another v1 option would be to require all postconditions to be before a postcondition contract block, + // and then preconditions after that. + List postconditionEnsuresCalls = new List(); IList instructions = Method.Body.Instructions; if (instructions.Count == 0) @@ -167,42 +170,26 @@ public ResultValue> TryExtractPostconditions() for (int i = 0; i < instructions.Count; i++) { Instruction inst = instructions[i]; - postconditions.Add(inst); - - if (IsEndContractBlockCall(inst)) + if (IsPostconditionEnsuresCall(inst)) { - endIndex = i; - break; + postconditionEnsuresCalls.Add(inst); } } - if (endIndex < 0) - { - // No explicit contract block end. - postconditions.Clear(); - return false; - } - - // Also include trailing nops immediately after EndContractBlock, as they often belong - // to the same source statement / sequence point. - for (int i = endIndex + 1; i < instructions.Count; i++) + if (postconditionEnsuresCalls.Count ==0) { - if (instructions[i].OpCode != OpCodes.Nop) - break; - postconditions.Add(instructions[i]); + return ResultValue>.Failure("No Ensures() calls."); } - return true; + return ResultValue>.Success(postconditionEnsuresCalls);; } - - public bool IsPostconditionEnsuresCall(Instruction inst) + public static bool IsPostconditionEnsuresCall(Instruction inst) => IsStaticCallToPostconditionMethod(inst, "Ensures"); - public bool IsResultCall(Instruction inst) + public static bool IsResultCall(Instruction inst) => IsStaticCallToPostconditionMethod(inst, "Result"); - public static bool IsStaticCallToPostconditionMethod(Instruction inst, string methodName) { if (inst.OpCode != OpCodes.Call) @@ -216,7 +203,7 @@ public static bool IsStaticCallToPostconditionMethod(Instruction inst, string me // Handle generic instance method as well. string declaringType = mr.DeclaringType.FullName; - return declaringType == Names.OdinPreconditionEnsuresTypeFullName; + return declaringType == Names.OdinPostconditionEnsuresTypeFullName; } /// diff --git a/DesignContracts/Rewriter/Names.cs b/DesignContracts/Rewriter/Names.cs index 5e695a8..81822f0 100644 --- a/DesignContracts/Rewriter/Names.cs +++ b/DesignContracts/Rewriter/Names.cs @@ -7,7 +7,7 @@ internal static class Names { internal const string BclContractNamespace = "System.Diagnostics.Contracts"; internal const string OdinContractNamespace = "Odin.DesignContracts"; - internal const string OdinPreconditionEnsuresTypeFullName = OdinContractNamespace + ".Precondition"; + internal const string OdinPostconditionEnsuresTypeFullName = OdinContractNamespace + ".Contract"; internal const string OdinInvariantAttributeFullName = OdinContractNamespace + ".ClassInvariantMethodAttribute"; internal const string BclInvariantAttributeFullName = BclContractNamespace + ".ContractInvariantMethodAttribute"; internal const string OdinPureAttributeFullName = OdinContractNamespace + ".PureAttribute"; diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index c94be1c..034cae1 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -54,7 +54,8 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) ReaderParameters readerParameters = new() { AssemblyResolver = resolver, - ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")) + ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")), + ReadingMode = ReadingMode.Immediate }; using AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); diff --git a/DesignContracts/Rewriter/TypeHandler.cs b/DesignContracts/Rewriter/TypeHandler.cs index ad81b2b..4022313 100644 --- a/DesignContracts/Rewriter/TypeHandler.cs +++ b/DesignContracts/Rewriter/TypeHandler.cs @@ -17,6 +17,7 @@ internal class TypeHandler /// public TypeHandler(TypeDefinition target) { + if (target == null!) throw new ArgumentNullException(nameof(target)); _target = target; } @@ -42,13 +43,14 @@ public MethodDefinition? InvariantMethod } } - internal IReadOnlyList GetMembersToTryRewrite() + internal IReadOnlyList GetMembersToTryRewrite() { - return _target.Methods.Select(c => new MemberHandler(c,this)).ToList(); + return _target.Methods.Select(c => new MethodHandler(c,this)).ToList(); } internal void FindInvariantMethodOrThrow() { + var allAttributes = _target.Methods.SelectMany(m => m.CustomAttributes).ToList(); List candidates = _target.Methods .Where(m => m.HasAnyAttributeIn(Names.InvariantAttributeFullNames)) .ToList(); @@ -81,4 +83,5 @@ internal void FindInvariantMethodOrThrow() _invariant = invariant; } + } \ No newline at end of file diff --git a/DesignContracts/RewriterTests/CecilAssemblyContext.cs b/DesignContracts/RewriterTests/CecilAssemblyContext.cs new file mode 100644 index 0000000..81fb2d4 --- /dev/null +++ b/DesignContracts/RewriterTests/CecilAssemblyContext.cs @@ -0,0 +1,62 @@ +using System.Reflection; +using Mono.Cecil; +using Targets; + +namespace Tests.Odin.DesignContracts.Rewriter; + +/// +/// Encapsulates access to a Cecil Assembly context for a given assembly for testing... +/// +internal sealed class CecilAssemblyContext : IDisposable +{ + public AssemblyDefinition Assembly { get; } + + public static CecilAssemblyContext GetTargetsUntooledAssemblyContext() + { + return new CecilAssemblyContext(typeof(OdinInvariantTarget).Assembly); + } + + public CecilAssemblyContext(Assembly sourceAssembly) + { + string assemblyPath = sourceAssembly.Location; + string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(sourceAssembly.Location))!; + DefaultAssemblyResolver resolver = new(); + resolver.AddSearchDirectory(assemblyDir); + ReaderParameters readerParameters = new() + { + AssemblyResolver = resolver, + ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")) + }; + + Assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); + } + + public TypeDefinition? FindType(string fullName) + { + return AllTypes.FirstOrDefault(t => t.FullName == fullName); + } + + public TypeDefinition? FindType(string nameSpace, string typeName) + { + return FindType($"{nameSpace}.{typeName}"); + } + + private List? _allTypes; + public IReadOnlyList AllTypes + { + get + { + if (_allTypes == null) + { + _allTypes = Assembly.Modules.SelectMany(m => m.GetTypes()).ToList(); + } + return _allTypes; + } + } + + public void Dispose() + { + Assembly.Dispose(); + } + +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/MethodHandlerTests.cs b/DesignContracts/RewriterTests/MethodHandlerTests.cs new file mode 100644 index 0000000..53119d9 --- /dev/null +++ b/DesignContracts/RewriterTests/MethodHandlerTests.cs @@ -0,0 +1,33 @@ +using Mono.Cecil; +using NUnit.Framework; +using Odin.DesignContracts.Rewriter; +using Targets; + +namespace Tests.Odin.DesignContracts.Rewriter; + +[TestFixture] +public sealed class MethodHandlerTests +{ + [Test] + [TestCase(typeof(OdinInvariantTarget),"get_" + nameof(OdinInvariantTarget.PureProperty), true)] + [TestCase(typeof(BclInvariantTarget), "get_" + nameof(BclInvariantTarget.PureProperty),true)] + [TestCase(typeof(BclInvariantTarget), nameof(BclInvariantTarget.PureGetValue),true)] + [TestCase(typeof(BclInvariantTarget), "get_" + nameof(BclInvariantTarget.NonPureProperty),false)] + public void Pure_methods_are_recognised(Type type, string methodName, bool isPure) + { + CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); + MethodHandler? sut = GetMethodHandlerFor(context, type, methodName); + + Assert.That(sut, Is.Not.Null); + Assert.That(sut!.IsPure, Is.EqualTo(isPure)); + } + + private MethodHandler? GetMethodHandlerFor(CecilAssemblyContext context, Type type, string methodName) + { + TypeDefinition? typeDef = context.FindType(type.FullName!); + TypeHandler handler = new TypeHandler(typeDef!); + MethodDefinition? def = handler.Type.Methods.FirstOrDefault(n => n.Name == methodName); + return new MethodHandler(def!, handler); + } + +} diff --git a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs similarity index 81% rename from DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs rename to DesignContracts/RewriterTests/RewriterTests.cs index 72083ff..c6a2732 100644 --- a/DesignContracts/RewriterTests/InvariantWeavingRewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -5,18 +5,17 @@ using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; using Targets; -using ContractFailureKind = Odin.DesignContracts.ContractFailureKind; namespace Tests.Odin.DesignContracts.Rewriter; [TestFixture] -public sealed class InvariantWeavingRewriterTests +public sealed class RewriterTests { [SetUp] public void SetUp() { // Ensure invariants are enabled even if the test environment sets env vars. - DesignContractOptions.Initialize(new DesignContractOptions + ContractOptions.Initialize(new ContractOptions { EnableInvariants = true, EnablePostconditions = true @@ -28,13 +27,13 @@ public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour { Type targetType = GetTargetTestTypeFor(testCase); using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetType.Assembly); - Assert.That(DesignContractOptions.Current.EnableInvariants, Is.True); - Assert.That(DesignContractOptions.Current.EnablePostconditions, Is.True); + Assert.That(ContractOptions.Current.EnableInvariants, Is.True); + Assert.That(ContractOptions.Current.EnablePostconditions, Is.True); Type t = context.GetTypeOrThrow(targetType.FullName!); // - ContractException? ex = Assert.Throws(() => + Exception? exception = Assert.Catch(() => { try { @@ -45,8 +44,13 @@ public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour throw tie.InnerException; } }); - Assert.That(ex, Is.Not.Null); - Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Is.EqualTo("Invariant broken: _value must be non-negative [Condition: _value >= 0]")); + // Because the ContractException thrown is from the dynamically loaded RewrittenAssemblyContext + // it does not seem castable to ContractException.... + // Assert.That(exception, Is.InstanceOf()); + // ContractException ex = (ContractException)exception!; + // Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Invariant)); } [Test] @@ -60,9 +64,10 @@ public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour test object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); - ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(OdinInvariantTarget.Increment)); })!; + Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTarget.Increment)); })!; - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Contains.Substring("Invariant broken:")); } [Test] @@ -75,9 +80,10 @@ public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testC object instance = Activator.CreateInstance(t, 1)!; - ContractException ex = Assert.Throws(() => { Invoke(t, instance, nameof(OdinInvariantTarget.MakeInvalid)); })!; + Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTarget.MakeInvalid)); })!; - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Contains.Substring("Invariant broken:")); } [Test] @@ -106,7 +112,7 @@ public void Pure_property_is_excluded_from_invariant_weaving([Values] AttributeF object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); - object? value = Invoke(t, instance, "PureProperty"); + object? value = Invoke(t, instance, "get_PureProperty"); Assert.That(value, Is.EqualTo(-1)); } @@ -123,7 +129,7 @@ public void Non_pure_property_is_woven_and_checks_invariants([Values] AttributeF PropertyInfo p = t.GetProperty("NonPureProperty")!; - ContractException ex = Assert.Throws(() => + Exception? ex = Assert.Catch(() => { try { @@ -134,11 +140,11 @@ public void Non_pure_property_is_woven_and_checks_invariants([Values] AttributeF throw tie.InnerException; } })!; - - Assert.That(ex.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Contains.Substring("Invariant broken:")); } - [Test][Description("Test all combinations of having 2 Invariant methods")] + [Test] public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] AttributeFlavour firstInvariantFlavour, [Values] AttributeFlavour secondInvariantFlavour) { @@ -152,7 +158,11 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute string inputPath = Path.Combine(temp.Path, Path.GetFileName(originalPath)); File.Copy(originalPath, inputPath, overwrite: true); - using (AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(inputPath, new ReaderParameters { ReadWrite = false })) + // Read the bytes into memory first to fully decouple the reader from the file on disk. + byte[] assemblyBytes = File.ReadAllBytes(inputPath); + using (MemoryStream ms = new MemoryStream(assemblyBytes)) + using (AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, + new ReaderParameters { ReadWrite = false, ReadingMode = ReadingMode.Immediate })) { TypeDefinition targetType = assemblyDefinition.MainModule.GetType(firstType.FullName!) ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); diff --git a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj index 37e817c..3a76ead 100644 --- a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj @@ -20,8 +20,8 @@ - + diff --git a/DesignContracts/RewriterTests/TypeHandlerTests.cs b/DesignContracts/RewriterTests/TypeHandlerTests.cs new file mode 100644 index 0000000..47b5065 --- /dev/null +++ b/DesignContracts/RewriterTests/TypeHandlerTests.cs @@ -0,0 +1,25 @@ +using Mono.Cecil; +using NUnit.Framework; +using Odin.DesignContracts.Rewriter; +using Targets; + +namespace Tests.Odin.DesignContracts.Rewriter; + +[TestFixture] +public sealed class TypeHandlerTests +{ + [Test] + [TestCase(typeof(OdinInvariantTarget), true)] + [TestCase(typeof(BclInvariantTarget), true)] + [TestCase(typeof(NoInvariantTarget), false)] + public void Invariant_method_is_found(Type type, bool invariantExpected) + { + CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); + TypeDefinition? typeDef = context.FindType(type.FullName!); + TypeHandler sut = new TypeHandler(typeDef!); + + Assert.That(sut.HasInvariant, Is.EqualTo(invariantExpected)); + if (invariantExpected) Assert.That(sut.InvariantMethod, Is.Not.Null); + } + +} diff --git a/DesignContracts/TargetsTooled/TargetsTooled.csproj b/DesignContracts/TargetsTooled/TargetsTooled.csproj index b5060ba..b1e495b 100644 --- a/DesignContracts/TargetsTooled/TargetsTooled.csproj +++ b/DesignContracts/TargetsTooled/TargetsTooled.csproj @@ -6,6 +6,7 @@ Target classes for the Design Contract rewriter to rewrite 1591;1573; + $(DefineConstants);CONTRACTS_FULL @@ -16,17 +17,17 @@ - - --> - + + \ No newline at end of file diff --git a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs b/DesignContracts/TargetsUntooled/BclInvariantTarget.cs index fed50af..d44fc78 100644 --- a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/BclInvariantTarget.cs @@ -3,8 +3,9 @@ namespace Targets { /// - /// The rewriter is expected to inject - /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// IMPORTANT: 'CONTRACTS_FULL' needs to be defined as a preprocessor symbol + /// if one wishes to use the Bcl System.Diagnostics.Contracts attributes... + /// They are conditional. /// public sealed class BclInvariantTarget { @@ -16,7 +17,7 @@ public BclInvariantTarget(int value) } [System.Diagnostics.Contracts.ContractInvariantMethod] - private void ObjectInvariant() + public void ObjectInvariant() { Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); } @@ -43,6 +44,12 @@ public async Task AsyncMakeInvalid() return await Task.FromResult(_value); } + [System.Diagnostics.Contracts.Pure] + public void PureCommand() + { + + } + [System.Diagnostics.Contracts.Pure] public int PureGetValue() => _value; diff --git a/DesignContracts/TargetsUntooled/NoInvariantTarget.cs b/DesignContracts/TargetsUntooled/NoInvariantTarget.cs new file mode 100644 index 0000000..8b65467 --- /dev/null +++ b/DesignContracts/TargetsUntooled/NoInvariantTarget.cs @@ -0,0 +1,18 @@ +using Odin.DesignContracts; + +namespace Targets +{ + /// + /// The rewriter is expected to inject + /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// + public sealed class NoInvariantTarget + { + private int _value; + + public NoInvariantTarget(int value) + { + _value = value; + } + } +} diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs index 60820e3..ef0ebf8 100644 --- a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs @@ -15,8 +15,8 @@ public OdinInvariantTarget(int value) _value = value; } - [global::Odin.DesignContracts.ClassInvariantMethod] - private void ObjectInvariant() + [ClassInvariantMethod] + public void ObjectInvariant() { Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); } @@ -44,10 +44,17 @@ public async Task AsyncMakeInvalid() return await Task.FromResult(_value); } - [global::Odin.DesignContracts.Pure] + [Pure] + public void PureCommand() + { + + } + + + [Pure] public int PureGetValue() => _value; - [global::Odin.DesignContracts.Pure] + [Pure] public int PureProperty => _value; public int NonPureProperty => _value; diff --git a/DesignContracts/TargetsUntooled/TargetsUntooled.csproj b/DesignContracts/TargetsUntooled/TargetsUntooled.csproj index 7520565..11cde0c 100644 --- a/DesignContracts/TargetsUntooled/TargetsUntooled.csproj +++ b/DesignContracts/TargetsUntooled/TargetsUntooled.csproj @@ -5,7 +5,8 @@ enable Target classes for the Design Contract rewriter to rewrite - 1591;1573; + 1591;1573; + $(DefineConstants);CONTRACTS_FULL diff --git a/Email/Core/Attachment.cs b/Email/Core/Attachment.cs index 04162e5..1ed5df1 100644 --- a/Email/Core/Attachment.cs +++ b/Email/Core/Attachment.cs @@ -17,9 +17,9 @@ public sealed record Attachment /// public Attachment(string fileName, Stream data, string contentType) { - Precondition.Requires(!string.IsNullOrWhiteSpace(fileName)); - Precondition.Requires(!string.IsNullOrWhiteSpace(contentType)); - Precondition.RequiresNotNull(data); + Contract.Requires(!string.IsNullOrWhiteSpace(fileName)); + Contract.Requires(!string.IsNullOrWhiteSpace(contentType)); + Contract.RequiresNotNull(data); FileName = fileName; Data = data; ContentType = contentType; diff --git a/Email/Core/DependencyInjectionExtensions.cs b/Email/Core/DependencyInjectionExtensions.cs index c95ad4d..035ca4f 100644 --- a/Email/Core/DependencyInjectionExtensions.cs +++ b/Email/Core/DependencyInjectionExtensions.cs @@ -41,7 +41,7 @@ public static void AddOdinEmailSending( public static void AddOdinEmailSending( this IServiceCollection serviceCollection, IConfigurationSection configurationSection) { - Precondition.RequiresNotNull(configurationSection); + Contract.RequiresNotNull(configurationSection); EmailSendingOptions emailOptions = new EmailSendingOptions(); configurationSection.Bind(emailOptions); diff --git a/Email/Core/EmailAddress.cs b/Email/Core/EmailAddress.cs index a156070..041707a 100644 --- a/Email/Core/EmailAddress.cs +++ b/Email/Core/EmailAddress.cs @@ -24,7 +24,7 @@ public sealed class EmailAddress /// public EmailAddress(string emailAddress, string? displayName = null) { - Precondition.Requires(!string.IsNullOrWhiteSpace(emailAddress), $"{nameof(emailAddress)} is required"); + Contract.Requires(!string.IsNullOrWhiteSpace(emailAddress), $"{nameof(emailAddress)} is required"); if (string.IsNullOrWhiteSpace(displayName)) { DisplayName = null; diff --git a/Email/Core/EmailMessage.cs b/Email/Core/EmailMessage.cs index 1ded116..3920d3e 100644 --- a/Email/Core/EmailMessage.cs +++ b/Email/Core/EmailMessage.cs @@ -29,9 +29,9 @@ public EmailMessage() public EmailMessage(string toEmailAddress, string fromEmailAddress, string? subject, string? body, bool isHtml = false) { - Precondition.Requires(!string.IsNullOrWhiteSpace(toEmailAddress), + Contract.Requires(!string.IsNullOrWhiteSpace(toEmailAddress), $"{nameof(toEmailAddress)} is required"); - Precondition.Requires(!string.IsNullOrWhiteSpace(fromEmailAddress), + Contract.Requires(!string.IsNullOrWhiteSpace(fromEmailAddress), $"{nameof(fromEmailAddress)} is required"); if (string.IsNullOrWhiteSpace(subject)) { @@ -207,7 +207,7 @@ public Dictionary Headers /// public void Attach(Attachment attachment) { - Precondition.RequiresNotNull(attachment); + Contract.RequiresNotNull(attachment); Attachments.Add(attachment); } } diff --git a/Email/Core/EmailSendingOptions.cs b/Email/Core/EmailSendingOptions.cs index 6b5840a..20afb4c 100644 --- a/Email/Core/EmailSendingOptions.cs +++ b/Email/Core/EmailSendingOptions.cs @@ -52,7 +52,7 @@ public string Provider get => _provider; init { - Precondition.Requires(!string.IsNullOrWhiteSpace(value)); + Contract.Requires(!string.IsNullOrWhiteSpace(value)); // Ensure MailgunEmailSender is changed to Mailgun for backwards compatibility _provider = value.Replace("EmailSender", "", StringComparison.OrdinalIgnoreCase); } diff --git a/Email/Mailgun/MailgunEmailSender.cs b/Email/Mailgun/MailgunEmailSender.cs index 6f1383f..3366394 100644 --- a/Email/Mailgun/MailgunEmailSender.cs +++ b/Email/Mailgun/MailgunEmailSender.cs @@ -42,9 +42,9 @@ public sealed class MailgunEmailSender : IEmailSender public MailgunEmailSender(MailgunOptions mailgunSettings, EmailSendingOptions emailSettings, ILoggerWrapper logger) { - Precondition.RequiresNotNull(mailgunSettings); - Precondition.RequiresNotNull(emailSettings); - Precondition.RequiresNotNull(logger); + Contract.RequiresNotNull(mailgunSettings); + Contract.RequiresNotNull(emailSettings); + Contract.RequiresNotNull(logger); _mailgunSettings = mailgunSettings; _emailSettings = emailSettings; _logger = logger; @@ -59,7 +59,7 @@ public MailgunEmailSender(MailgunOptions mailgunSettings, endPoint += "/"; } - Precondition.Requires(!string.IsNullOrWhiteSpace(_mailgunSettings.Domain), "Domain missing in MailgunOptions"); + Contract.Requires(!string.IsNullOrWhiteSpace(_mailgunSettings.Domain), "Domain missing in MailgunOptions"); string subPath = $"{_mailgunSettings.Domain}/messages"; // Leading slash will replace the /v3 if (subPath[0] == '/') @@ -68,7 +68,7 @@ public MailgunEmailSender(MailgunOptions mailgunSettings, } _httpClient.BaseAddress = new Uri(new Uri(endPoint), subPath); - Precondition.Requires(!string.IsNullOrWhiteSpace(_mailgunSettings.ApiKey), "ApiKey missing in MailgunOptions"); + Contract.Requires(!string.IsNullOrWhiteSpace(_mailgunSettings.ApiKey), "ApiKey missing in MailgunOptions"); byte[] byteArray = Encoding.ASCII.GetBytes($"api:{_mailgunSettings.ApiKey}"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( @@ -85,9 +85,9 @@ private static string EncodeAsHtml(string input) private static ByteArrayContent ToByteArrayContent(Stream stream) { - Precondition.RequiresNotNull(stream); - Precondition.Requires(stream.CanRead, "Stream.CanRead must be true"); - Precondition.Requires(stream.CanSeek, "Stream.CanSeek must be true"); + Contract.RequiresNotNull(stream); + Contract.Requires(stream.CanRead, "Stream.CanRead must be true"); + Contract.Requires(stream.CanSeek, "Stream.CanSeek must be true"); try { @@ -112,9 +112,9 @@ private static ByteArrayContent ToByteArrayContent(Stream stream) /// public async Task> SendEmail(IEmailMessage email) { - Precondition.RequiresNotNull(email); - Precondition.Requires(email.To.Any(), "Mailgun requires one or more to addresses."); - Precondition.Requires(!string.IsNullOrWhiteSpace(email.Subject), "Mailgun requires an email subject"); + Contract.RequiresNotNull(email); + Contract.Requires(email.To.Any(), "Mailgun requires one or more to addresses."); + Contract.Requires(!string.IsNullOrWhiteSpace(email.Subject), "Mailgun requires an email subject"); try { diff --git a/Email/Mailgun/MailgunServiceInjector.cs b/Email/Mailgun/MailgunServiceInjector.cs index 11e4977..4477c45 100644 --- a/Email/Mailgun/MailgunServiceInjector.cs +++ b/Email/Mailgun/MailgunServiceInjector.cs @@ -15,7 +15,7 @@ public class MailgunServiceInjector : IEmailSenderServiceInjector public void TryAddEmailSender(IServiceCollection serviceCollection, IConfigurationSection emailConfigurationSection) { - Precondition.RequiresNotNull(emailConfigurationSection); + Contract.RequiresNotNull(emailConfigurationSection); MailgunOptions mailGunSenderSettings = new MailgunOptions(); emailConfigurationSection.Bind(MailgunOptions.MailgunName, diff --git a/Email/Office365/Office365EmailSender.cs b/Email/Office365/Office365EmailSender.cs index 4c408ea..5eef849 100644 --- a/Email/Office365/Office365EmailSender.cs +++ b/Email/Office365/Office365EmailSender.cs @@ -3,9 +3,9 @@ using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Graph.Users.Item.SendMail; -using Odin.DesignContracts; using Odin.Logging; using Odin.System; +using Contract = Odin.DesignContracts.Contract; namespace Odin.Email; @@ -27,9 +27,9 @@ public class Office365EmailSender : IEmailSender /// Microsoft UserId public Office365EmailSender(Office365Options office365Options, EmailSendingOptions emailSettings , ILoggerWrapper logger) { - Precondition.RequiresNotNull(office365Options); - Precondition.RequiresNotNull(emailSettings); - Precondition.RequiresNotNull(logger); + Contract.RequiresNotNull(office365Options); + Contract.RequiresNotNull(emailSettings); + Contract.RequiresNotNull(logger); _emailSettings = emailSettings; _logger = logger; @@ -59,7 +59,7 @@ public Office365EmailSender(Office365Options office365Options, EmailSendingOptio { if (email.From is null) { - Precondition.Requires(!string.IsNullOrWhiteSpace(_emailSettings.DefaultFromAddress), "Cannot fall back to the default from address, since it is missing."); + Contract.Requires(!string.IsNullOrWhiteSpace(_emailSettings.DefaultFromAddress), "Cannot fall back to the default from address, since it is missing."); email.From = new EmailAddress(_emailSettings.DefaultFromAddress!, _emailSettings.DefaultFromName); } email.Subject = string.Concat(_emailSettings.SubjectPrefix, email.Subject, diff --git a/Email/Office365/Office365ServiceInjector.cs b/Email/Office365/Office365ServiceInjector.cs index 99fe8d3..6462ae8 100644 --- a/Email/Office365/Office365ServiceInjector.cs +++ b/Email/Office365/Office365ServiceInjector.cs @@ -11,7 +11,7 @@ public class Office365ServiceInjector : IEmailSenderServiceInjector /// public void TryAddEmailSender(IServiceCollection serviceCollection, IConfigurationSection emailConfigurationSection) { - Precondition.RequiresNotNull(emailConfigurationSection); + Contract.RequiresNotNull(emailConfigurationSection); EmailSendingOptions emailOptions = new(); emailConfigurationSection.Bind(emailOptions); diff --git a/Email/Tests/EmailTestConfiguration.cs b/Email/Tests/EmailTestConfiguration.cs index 90aca14..dcc2c2b 100644 --- a/Email/Tests/EmailTestConfiguration.cs +++ b/Email/Tests/EmailTestConfiguration.cs @@ -7,13 +7,13 @@ public static class EmailTestConfiguration { public static string GetTestEmailAddressFromConfig(IConfiguration config) { - Precondition.RequiresNotNull(config); + Contract.RequiresNotNull(config); return config["Email-TestToAddress"]!; } public static string GetTestFromNameFromConfig(IConfiguration config) { - Precondition.RequiresNotNull(config); + Contract.RequiresNotNull(config); return config["Email-TestFromName"]!; } } \ No newline at end of file diff --git a/Email/Tests/Mailgun/MailgunEmailSenderTestBuilder.cs b/Email/Tests/Mailgun/MailgunEmailSenderTestBuilder.cs index 9859ab0..c46d27d 100644 --- a/Email/Tests/Mailgun/MailgunEmailSenderTestBuilder.cs +++ b/Email/Tests/Mailgun/MailgunEmailSenderTestBuilder.cs @@ -44,7 +44,7 @@ public MailgunEmailSenderTestBuilder EnsureNullDependenciesAreMocked() public MailgunEmailSenderTestBuilder WithEmailSendingOptionsFromTestConfiguration(IConfiguration configuration) { - Precondition.RequiresNotNull(configuration); + Contract.RequiresNotNull(configuration); string testerEmail = EmailTestConfiguration.GetTestEmailAddressFromConfig(configuration); string testerName = EmailTestConfiguration.GetTestFromNameFromConfig(configuration); EmailSendingOptions = new EmailSendingOptions() @@ -67,7 +67,7 @@ public MailgunEmailSenderTestBuilder WithMailgunOptionsFromTestConfiguration(ICo public static MailgunOptions GetMailgunOptionsFromConfig(IConfiguration config) { - Precondition.RequiresNotNull(config); + Contract.RequiresNotNull(config); IConfigurationSection section = config.GetSection("Email-MailgunOptions"); MailgunOptions options = new MailgunOptions(); section.Bind(options); diff --git a/Email/Tests/Office365/Office365EmailSenderTestBuilder.cs b/Email/Tests/Office365/Office365EmailSenderTestBuilder.cs index 1ce3ace..f3e1407 100644 --- a/Email/Tests/Office365/Office365EmailSenderTestBuilder.cs +++ b/Email/Tests/Office365/Office365EmailSenderTestBuilder.cs @@ -43,7 +43,7 @@ public Office365EmailSenderTestBuilder EnsureNullDependenciesAreMocked() public Office365EmailSenderTestBuilder WithEmailSendingOptionsFromTestConfiguration(IConfiguration configuration) { - Precondition.RequiresNotNull(configuration); + Contract.RequiresNotNull(configuration); string testerEmail = EmailTestConfiguration.GetTestEmailAddressFromConfig(configuration); string testerName = EmailTestConfiguration.GetTestFromNameFromConfig(configuration); EmailSendingOptions = new EmailSendingOptions() @@ -66,7 +66,7 @@ public Office365EmailSenderTestBuilder WithOffice365OptionsFromTestConfiguration public static Office365Options GetOffice365OptionsFromConfig(IConfiguration config) { - Precondition.RequiresNotNull(config); + Contract.RequiresNotNull(config); IConfigurationSection section = config.GetSection("Email-Office365"); Office365Options options = new Office365Options(); section.Bind(options); diff --git a/RemoteFiles/Abstractions/RemoteFileInfo.cs b/RemoteFiles/Abstractions/RemoteFileInfo.cs index e84c4b3..6e96a31 100644 --- a/RemoteFiles/Abstractions/RemoteFileInfo.cs +++ b/RemoteFiles/Abstractions/RemoteFileInfo.cs @@ -15,8 +15,8 @@ public sealed class RemoteFileInfo : IRemoteFileInfo /// public RemoteFileInfo(string fullName, string name, DateTime lastWriteTimeUtc) { - Precondition.Requires(!string.IsNullOrWhiteSpace(name), nameof(name)); - Precondition.Requires(!string.IsNullOrWhiteSpace(fullName), nameof(fullName)); + Contract.Requires(!string.IsNullOrWhiteSpace(name), nameof(name)); + Contract.Requires(!string.IsNullOrWhiteSpace(fullName), nameof(fullName)); FullName = fullName; Name = name; // Use overload for DateTimeOffset set from DateTime diff --git a/RemoteFiles/Core/ConnectionSettingsHelper.cs b/RemoteFiles/Core/ConnectionSettingsHelper.cs index a0f56e3..353eb0f 100644 --- a/RemoteFiles/Core/ConnectionSettingsHelper.cs +++ b/RemoteFiles/Core/ConnectionSettingsHelper.cs @@ -27,7 +27,7 @@ public static class ConnectionSettingsHelper /// public static Dictionary ParseConnectionString(string connectionString, char delimiter) { - Precondition.Requires(!string.IsNullOrEmpty(connectionString), "connectionString cannot be null."); + Contract.Requires(!string.IsNullOrEmpty(connectionString), "connectionString cannot be null."); Dictionary result = new Dictionary(); string[] keyValuePairs = connectionString.Trim(delimiter).Split(delimiter) diff --git a/RemoteFiles/Core/DependencyInjectionExtensions.cs b/RemoteFiles/Core/DependencyInjectionExtensions.cs index 3ea191d..5e6ae2a 100644 --- a/RemoteFiles/Core/DependencyInjectionExtensions.cs +++ b/RemoteFiles/Core/DependencyInjectionExtensions.cs @@ -33,7 +33,7 @@ public static IServiceCollection AddRemoteFiles(this IServiceCollection services public static IServiceCollection AddRemoteFiles(this IServiceCollection services, IConfigurationSection configurationSection) { - Precondition.Requires(configurationSection!=null!, "Configuration Section for RemoteFiles cannot be null."); + Contract.Requires(configurationSection!=null!, "Configuration Section for RemoteFiles cannot be null."); if (!configurationSection.Exists()) throw new ApplicationException( diff --git a/RemoteFiles/Core/RemoteFileSessionFactory.cs b/RemoteFiles/Core/RemoteFileSessionFactory.cs index a5fe96f..d0e74af 100644 --- a/RemoteFiles/Core/RemoteFileSessionFactory.cs +++ b/RemoteFiles/Core/RemoteFileSessionFactory.cs @@ -17,8 +17,8 @@ public class RemoteFileSessionFactory : IRemoteFileSessionFactory /// public RemoteFileSessionFactory(RemoteFilesOptions remoteFilesOptions) { - Precondition.Requires(remoteFilesOptions != null, "remoteFileConfiguration cannot be null"); - Precondition.Requires(remoteFilesOptions.ConnectionStrings != null, "remoteFileConfiguration connection strings cannot null"); + Contract.Requires(remoteFilesOptions != null, "remoteFileConfiguration cannot be null"); + Contract.Requires(remoteFilesOptions.ConnectionStrings != null, "remoteFileConfiguration connection strings cannot null"); _fileSourceConnections = remoteFilesOptions.ConnectionStrings.ToDictionary( kv => kv.Key, @@ -37,7 +37,7 @@ public RemoteFileSessionFactory(RemoteFilesOptions remoteFilesOptions) /// public ResultValue CreateRemoteFileSession(string connectionName) { - Precondition.Requires(!string.IsNullOrEmpty(connectionName), "connectionName cannot be null"); + Contract.Requires(!string.IsNullOrEmpty(connectionName), "connectionName cannot be null"); if (!_fileSourceConnections.ContainsKey(connectionName)) return ResultValue.Failure($"Connection name not supported or configured: {connectionName}"); diff --git a/RemoteFiles/SFTP/SftpRemoteFileSession.cs b/RemoteFiles/SFTP/SftpRemoteFileSession.cs index 3596bb4..226e36f 100644 --- a/RemoteFiles/SFTP/SftpRemoteFileSession.cs +++ b/RemoteFiles/SFTP/SftpRemoteFileSession.cs @@ -21,7 +21,7 @@ public sealed class SftpRemoteFileSession : IRemoteFileSession /// public SftpRemoteFileSession(SftpConnectionSettings connectionInfo) { - Precondition.Requires(connectionInfo!=null!); + Contract.Requires(connectionInfo!=null!); _connectionInfo = connectionInfo!; } @@ -98,8 +98,8 @@ public void Disconnect() /// public void UploadFile(string textFileContents, string fileName) { - Precondition.Requires(textFileContents != null, nameof(textFileContents)); - Precondition.Requires(!string.IsNullOrWhiteSpace(fileName), nameof(fileName)); + Contract.Requires(textFileContents != null, nameof(textFileContents)); + Contract.Requires(!string.IsNullOrWhiteSpace(fileName), nameof(fileName)); EnsureConnected(); MemoryStream stream = new MemoryStream(Encoding.ASCII.GetBytes(textFileContents)); @@ -115,7 +115,7 @@ public void UploadFile(string textFileContents, string fileName) /// public void DownloadFile(string fileName, in Stream output) { - Precondition.Requires(!string.IsNullOrWhiteSpace(fileName), nameof(fileName)); + Contract.Requires(!string.IsNullOrWhiteSpace(fileName), nameof(fileName)); EnsureConnected(); _client!.BufferSize = 4096; @@ -148,7 +148,7 @@ public string DownloadTextFile(string fileName) /// public void ChangeDirectory(string path) { - Precondition.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); + Contract.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); EnsureConnected(); _client.ChangeDirectory(path); } @@ -159,7 +159,7 @@ public void ChangeDirectory(string path) /// public void CreateDirectory(string path) { - Precondition.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); + Contract.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); EnsureConnected(); _client!.CreateDirectory(path); } @@ -170,7 +170,7 @@ public void CreateDirectory(string path) /// public void Delete(string filePath) { - Precondition.Requires(!string.IsNullOrWhiteSpace(filePath), nameof(filePath)); + Contract.Requires(!string.IsNullOrWhiteSpace(filePath), nameof(filePath)); EnsureConnected(); _client!.DeleteFile(filePath); } @@ -183,8 +183,8 @@ public void Delete(string filePath) /// public IEnumerable GetFiles(string path, string? searchPattern = null) { - Precondition.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); - Precondition.Requires(!(path!.Contains('*') || path.Contains('?'))); + Contract.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); + Contract.Requires(!(path!.Contains('*') || path.Contains('?'))); EnsureConnected(); //return results IEnumerable files = _client.ListDirectory(path); @@ -210,7 +210,7 @@ public IEnumerable GetFiles(string path, string? searchPattern /// public bool Exists(string path, int? timeoutInSeconds = null) { - Precondition.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); + Contract.Requires(!string.IsNullOrWhiteSpace(path), nameof(path)); EnsureConnected(timeoutInSeconds); return _client.Exists(path); } diff --git a/System/Activator2/Activator2.cs b/System/Activator2/Activator2.cs index e90ccf9..c50ddfa 100644 --- a/System/Activator2/Activator2.cs +++ b/System/Activator2/Activator2.cs @@ -16,8 +16,8 @@ public static class Activator2 /// public static ResultValue TryCreate(string typeName, string assemblyName) where T : class { - Precondition.Requires(!string.IsNullOrWhiteSpace(typeName)); - Precondition.Requires(!string.IsNullOrWhiteSpace(assemblyName)); + Contract.Requires(!string.IsNullOrWhiteSpace(typeName)); + Contract.Requires(!string.IsNullOrWhiteSpace(assemblyName)); ObjectHandle? handle; try { @@ -50,7 +50,7 @@ private static ResultValue CreateInstanceFailure(string typeName, string a /// public static ResultValue TryCreate(Type typeToCreate) where T : class { - Precondition.Requires(typeToCreate!=null!); + Contract.Requires(typeToCreate!=null!); try { object? obj = Activator.CreateInstance(typeToCreate); diff --git a/Utility/Tax/TaxUtility.cs b/Utility/Tax/TaxUtility.cs index 44d2619..069349f 100644 --- a/Utility/Tax/TaxUtility.cs +++ b/Utility/Tax/TaxUtility.cs @@ -25,7 +25,7 @@ public TaxUtility(decimal singleTaxRate) /// Note that tax rates must be expressed as a percentage number, not a fraction. public TaxUtility(IEnumerable> taxRatesAsPercentageHistory) { - Precondition.Requires(taxRatesAsPercentageHistory!=null!); + Contract.Requires(taxRatesAsPercentageHistory!=null!); _taxValues = new ValueChangesListProvider(taxRatesAsPercentageHistory); } diff --git a/Utility/VaryingValues/ValueChangesListProvider.cs b/Utility/VaryingValues/ValueChangesListProvider.cs index 2f5db4d..0b4ae6d 100644 --- a/Utility/VaryingValues/ValueChangesListProvider.cs +++ b/Utility/VaryingValues/ValueChangesListProvider.cs @@ -19,7 +19,7 @@ public class ValueChangesListProvider : IVaryingValuePro /// public ValueChangesListProvider(IEnumerable> valuesAcrossRange) { - Precondition.Requires(valuesAcrossRange!=null!); + Contract.Requires(valuesAcrossRange!=null!); InitialiseFrom(valuesAcrossRange.ToList()); } @@ -29,7 +29,7 @@ public ValueChangesListProvider(IEnumerable> /// public ValueChangesListProvider(ValueChange singleValue) { - Precondition.Requires(singleValue!=null!); + Contract.Requires(singleValue!=null!); InitialiseFrom( new List> { singleValue } ); } @@ -40,7 +40,7 @@ public ValueChangesListProvider(ValueChange singleValue) /// The name of the configuration section, eg 'TaxHistory' public ValueChangesListProvider(IConfiguration configuration, string sectionName) { - Precondition.Requires(configuration!=null!); + Contract.Requires(configuration!=null!); List> valuesInConfig = new List>(); configuration.Bind(sectionName, valuesInConfig); InitialiseFrom(valuesInConfig); @@ -52,7 +52,7 @@ public ValueChangesListProvider(IConfiguration configuration, string sectionName /// public ValueChangesListProvider(IConfigurationSection valueChangesSection) { - Precondition.Requires(valueChangesSection!=null!); + Contract.Requires(valueChangesSection!=null!); List> valuesInConfig = new List>(); valueChangesSection.Bind(valuesInConfig); InitialiseFrom(valuesInConfig); @@ -60,7 +60,7 @@ public ValueChangesListProvider(IConfigurationSection valueChangesSection) private void InitialiseFrom(List> valuesAcrossRange) { - Precondition.Requires(valuesAcrossRange!=null!); + Contract.Requires(valuesAcrossRange!=null!); _valueChangesInOrder = valuesAcrossRange.OrderBy(c => c.From).ToList(); } From 9d8c5fbd5c29fb9ea3b722a735f8802c43dcceb8 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Tue, 23 Dec 2025 16:16:10 +0200 Subject: [PATCH 16/27] Saving work... --- .../RewriterTests/RewriterTests.cs | 23 +++++++++++++++++++ .../TargetsUntooled/OdinInvariantTarget.cs | 10 ++++++++ 2 files changed, 33 insertions(+) diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index c6a2732..c6e1397 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -5,6 +5,7 @@ using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; using Targets; +using ContractFailureKind = Odin.DesignContracts.ContractFailureKind; namespace Tests.Odin.DesignContracts.Rewriter; @@ -185,6 +186,28 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute Assert.That(expectedError, Is.Not.Null); } + [Test] + [TestCase(3, true)] + [TestCase(13, false)] + public void Requires_throws_if_condition_broken(int testValue, bool shouldThrow) + { + OdinInvariantTarget sut = new OdinInvariantTarget(1); + + if (shouldThrow) + { + Exception? exception = Assert.Catch(() => sut.RequiresYGreaterThan10(testValue)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception, Is.InstanceOf()); + ContractException ex = (ContractException)exception!; + Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Precondition)); + Assert.That(ex.Message, Is.EqualTo("Precondition not met: y must be greater than 10")); + } + else + { + Assert.DoesNotThrow(() => sut.RequiresYGreaterThan10(testValue)); + } + } + private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) { FieldInfo f = declaringType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs index ef0ebf8..6c290ed 100644 --- a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs @@ -58,5 +58,15 @@ public void PureCommand() public int PureProperty => _value; public int NonPureProperty => _value; + + public void RequiresYGreaterThan10(int y) + { + Contract.Requires(y > 10, "y must be greater than 10"); + Console.WriteLine("Instruction 1"); + Console.WriteLine("Instruction 2"); + Console.WriteLine("Instruction 2"); + } + + } } From ee8e606f6d4c96e2a0244a6228439fbaa0dc01d9 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Wed, 24 Dec 2025 14:30:01 +0200 Subject: [PATCH 17/27] Saving progress... --- .../RewriterTests/CecilAssemblyContext.cs | 2 +- .../RewriterTests/MethodHandlerTests.cs | 8 +- .../RewriterTests/RewriterTests.cs | 12 +-- .../RewriterTests/TypeHandlerTests.cs | 6 +- .../TargetsTooled/TargetsTooled.csproj | 9 ++- ...antTarget.cs => BclInvariantTestTarget.cs} | 4 +- .../TargetsUntooled/EnsuresTarget.cs | 23 ++++++ ...iantTarget.cs => NoInvariantTestTarget.cs} | 4 +- .../TargetsUntooled/OdinInvariantTarget.cs | 12 ++- .../OdinInvariantTestTarget.cs | 80 +++++++++++++++++++ .../TargetsUntooled/TargetsUntooled.csproj | 1 + 11 files changed, 137 insertions(+), 24 deletions(-) rename DesignContracts/TargetsUntooled/{BclInvariantTarget.cs => BclInvariantTestTarget.cs} (93%) create mode 100644 DesignContracts/TargetsUntooled/EnsuresTarget.cs rename DesignContracts/TargetsUntooled/{NoInvariantTarget.cs => NoInvariantTestTarget.cs} (77%) create mode 100644 DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs diff --git a/DesignContracts/RewriterTests/CecilAssemblyContext.cs b/DesignContracts/RewriterTests/CecilAssemblyContext.cs index 81fb2d4..4a98138 100644 --- a/DesignContracts/RewriterTests/CecilAssemblyContext.cs +++ b/DesignContracts/RewriterTests/CecilAssemblyContext.cs @@ -13,7 +13,7 @@ internal sealed class CecilAssemblyContext : IDisposable public static CecilAssemblyContext GetTargetsUntooledAssemblyContext() { - return new CecilAssemblyContext(typeof(OdinInvariantTarget).Assembly); + return new CecilAssemblyContext(typeof(OdinInvariantTestTarget).Assembly); } public CecilAssemblyContext(Assembly sourceAssembly) diff --git a/DesignContracts/RewriterTests/MethodHandlerTests.cs b/DesignContracts/RewriterTests/MethodHandlerTests.cs index 53119d9..d6457b3 100644 --- a/DesignContracts/RewriterTests/MethodHandlerTests.cs +++ b/DesignContracts/RewriterTests/MethodHandlerTests.cs @@ -9,10 +9,10 @@ namespace Tests.Odin.DesignContracts.Rewriter; public sealed class MethodHandlerTests { [Test] - [TestCase(typeof(OdinInvariantTarget),"get_" + nameof(OdinInvariantTarget.PureProperty), true)] - [TestCase(typeof(BclInvariantTarget), "get_" + nameof(BclInvariantTarget.PureProperty),true)] - [TestCase(typeof(BclInvariantTarget), nameof(BclInvariantTarget.PureGetValue),true)] - [TestCase(typeof(BclInvariantTarget), "get_" + nameof(BclInvariantTarget.NonPureProperty),false)] + [TestCase(typeof(OdinInvariantTestTarget),"get_" + nameof(OdinInvariantTestTarget.PureProperty), true)] + [TestCase(typeof(BclInvariantTestTarget), "get_" + nameof(BclInvariantTestTarget.PureProperty),true)] + [TestCase(typeof(BclInvariantTestTarget), nameof(BclInvariantTestTarget.PureGetValue),true)] + [TestCase(typeof(BclInvariantTestTarget), "get_" + nameof(BclInvariantTestTarget.NonPureProperty),false)] public void Pure_methods_are_recognised(Type type, string methodName, bool isPure) { CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index c6e1397..4751041 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -65,7 +65,7 @@ public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour test object instance = Activator.CreateInstance(t, 1)!; SetPrivateField(t, instance, "_value", -1); - Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTarget.Increment)); })!; + Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTestTarget.Increment)); })!; Assert.That(ex, Is.Not.Null); Assert.That(ex.Message, Contains.Substring("Invariant broken:")); @@ -81,7 +81,7 @@ public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testC object instance = Activator.CreateInstance(t, 1)!; - Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTarget.MakeInvalid)); })!; + Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTestTarget.MakeInvalid)); })!; Assert.That(ex, Is.Not.Null); Assert.That(ex.Message, Contains.Substring("Invariant broken:")); @@ -169,7 +169,7 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute ?? throw new InvalidOperationException("Failed to locate target type in temp assembly."); MethodDefinition increment = targetType.Methods - .First(m => m.Name == nameof(OdinInvariantTarget.Increment)); + .First(m => m.Name == nameof(OdinInvariantTestTarget.Increment)); // Add a second invariant attribute to a different method. ConstructorInfo ctor = invariantAttributeTestCaseType.GetConstructor(Type.EmptyTypes) @@ -191,7 +191,7 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute [TestCase(13, false)] public void Requires_throws_if_condition_broken(int testValue, bool shouldThrow) { - OdinInvariantTarget sut = new OdinInvariantTarget(1); + OdinInvariantTestTarget sut = new OdinInvariantTestTarget(1); if (shouldThrow) { @@ -234,11 +234,11 @@ private Type GetTargetTestTypeFor(AttributeFlavour testCase) { if (testCase == AttributeFlavour.Odin) { - return typeof(OdinInvariantTarget); + return typeof(OdinInvariantTestTarget); } if (testCase == AttributeFlavour.BaseClassLibrary) { - return typeof(BclInvariantTarget); + return typeof(BclInvariantTestTarget); } throw new NotSupportedException(testCase.ToString()); } diff --git a/DesignContracts/RewriterTests/TypeHandlerTests.cs b/DesignContracts/RewriterTests/TypeHandlerTests.cs index 47b5065..b5134ee 100644 --- a/DesignContracts/RewriterTests/TypeHandlerTests.cs +++ b/DesignContracts/RewriterTests/TypeHandlerTests.cs @@ -9,9 +9,9 @@ namespace Tests.Odin.DesignContracts.Rewriter; public sealed class TypeHandlerTests { [Test] - [TestCase(typeof(OdinInvariantTarget), true)] - [TestCase(typeof(BclInvariantTarget), true)] - [TestCase(typeof(NoInvariantTarget), false)] + [TestCase(typeof(OdinInvariantTestTarget), true)] + [TestCase(typeof(BclInvariantTestTarget), true)] + [TestCase(typeof(NoInvariantTestTarget), false)] public void Invariant_method_is_found(Type type, bool invariantExpected) { CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); diff --git a/DesignContracts/TargetsTooled/TargetsTooled.csproj b/DesignContracts/TargetsTooled/TargetsTooled.csproj index b1e495b..fd4b545 100644 --- a/DesignContracts/TargetsTooled/TargetsTooled.csproj +++ b/DesignContracts/TargetsTooled/TargetsTooled.csproj @@ -6,6 +6,7 @@ Target classes for the Design Contract rewriter to rewrite 1591;1573; + Targets $(DefineConstants);CONTRACTS_FULL @@ -17,17 +18,17 @@ - + - - --> + \ No newline at end of file diff --git a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs b/DesignContracts/TargetsUntooled/BclInvariantTestTarget.cs similarity index 93% rename from DesignContracts/TargetsUntooled/BclInvariantTarget.cs rename to DesignContracts/TargetsUntooled/BclInvariantTestTarget.cs index d44fc78..2a82bc2 100644 --- a/DesignContracts/TargetsUntooled/BclInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/BclInvariantTestTarget.cs @@ -7,11 +7,11 @@ namespace Targets /// if one wishes to use the Bcl System.Diagnostics.Contracts attributes... /// They are conditional. /// - public sealed class BclInvariantTarget + public sealed class BclInvariantTestTarget { private int _value; - public BclInvariantTarget(int value) + public BclInvariantTestTarget(int value) { _value = value; } diff --git a/DesignContracts/TargetsUntooled/EnsuresTarget.cs b/DesignContracts/TargetsUntooled/EnsuresTarget.cs new file mode 100644 index 0000000..7d2361c --- /dev/null +++ b/DesignContracts/TargetsUntooled/EnsuresTarget.cs @@ -0,0 +1,23 @@ + + +using Odin.DesignContracts; + +namespace Targets +{ + public sealed class EnsuresTestTarget + { + public int EnsuresPlus5A(int y) + { + Contract.Ensures(Contract.Result() == y + 5); + if (y > 100) return y + 2; // Break the contract for y > 100... + return y + 5; + } + public int EnsuresPlus5B(int y) + { + if (y > 100) return y + 2; // Break the contract for y > 100... + Contract.Ensures(Contract.Result() == y + 5); + return y + 5; + } + + } +} diff --git a/DesignContracts/TargetsUntooled/NoInvariantTarget.cs b/DesignContracts/TargetsUntooled/NoInvariantTestTarget.cs similarity index 77% rename from DesignContracts/TargetsUntooled/NoInvariantTarget.cs rename to DesignContracts/TargetsUntooled/NoInvariantTestTarget.cs index 8b65467..d6e9f3d 100644 --- a/DesignContracts/TargetsUntooled/NoInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/NoInvariantTestTarget.cs @@ -6,11 +6,11 @@ namespace Targets /// The rewriter is expected to inject /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. ///
- public sealed class NoInvariantTarget + public sealed class NoInvariantTestTarget { private int _value; - public NoInvariantTarget(int value) + public NoInvariantTestTarget(int value) { _value = value; } diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs index 6c290ed..4ab070a 100644 --- a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs +++ b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs @@ -6,11 +6,11 @@ namespace Targets /// The rewriter is expected to inject /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. ///
- public sealed class OdinInvariantTarget + public sealed class OdinInvariantTestTarget { private int _value; - public OdinInvariantTarget(int value) + public OdinInvariantTestTarget(int value) { _value = value; } @@ -67,6 +67,14 @@ public void RequiresYGreaterThan10(int y) Console.WriteLine("Instruction 2"); } + public void EnsuresResultIsZero(int y) + { + Contract.Requires(y > 10, "y must be greater than 10"); + Console.WriteLine("Instruction 1"); + Console.WriteLine("Instruction 2"); + Console.WriteLine("Instruction 2"); + } + } } diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs new file mode 100644 index 0000000..96acc4e --- /dev/null +++ b/DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs @@ -0,0 +1,80 @@ +using Odin.DesignContracts; + +namespace Targets +{ + /// + /// The rewriter is expected to inject + /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. + /// + public sealed class OdinInvariantTarget + { + private int _value; + + public OdinInvariantTarget(int value) + { + _value = value; + } + + [ClassInvariantMethod] + public void ObjectInvariant() + { + Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); + } + + public void Increment() + { + _value++; + } + + public async Task AsyncIncrement() + { + _value++; + return await Task.FromResult(_value); + } + + + public void MakeInvalid() + { + _value = -1; + } + + public async Task AsyncMakeInvalid() + { + _value = -1; + return await Task.FromResult(_value); + } + + [Pure] + public void PureCommand() + { + + } + + + [Pure] + public int PureGetValue() => _value; + + [Pure] + public int PureProperty => _value; + + public int NonPureProperty => _value; + + public void RequiresYGreaterThan10(int y) + { + Contract.Requires(y > 10, "y must be greater than 10"); + Console.WriteLine("Instruction 1"); + Console.WriteLine("Instruction 2"); + Console.WriteLine("Instruction 2"); + } + + public void EnsuresResultIsZero(int y) + { + Contract.Requires(y > 10, "y must be greater than 10"); + Console.WriteLine("Instruction 1"); + Console.WriteLine("Instruction 2"); + Console.WriteLine("Instruction 2"); + } + + + } +} diff --git a/DesignContracts/TargetsUntooled/TargetsUntooled.csproj b/DesignContracts/TargetsUntooled/TargetsUntooled.csproj index 11cde0c..d776c3b 100644 --- a/DesignContracts/TargetsUntooled/TargetsUntooled.csproj +++ b/DesignContracts/TargetsUntooled/TargetsUntooled.csproj @@ -5,6 +5,7 @@ enable Target classes for the Design Contract rewriter to rewrite + Targets 1591;1573; $(DefineConstants);CONTRACTS_FULL From 4695d2d78b08067ca513cfefc9b1b4990f60c9e5 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Wed, 24 Dec 2025 18:54:29 +0200 Subject: [PATCH 18/27] Assert and Assume added --- DesignContracts/Core/Contract.cs | 12 ++- DesignContracts/Core/ContractAttributes.cs | 6 +- DesignContracts/Core/ContractOptions.cs | 28 +++--- .../RewriterTests/RewriterTests.cs | 89 ++++++++++++++++++- .../TargetsTooled/TargetsTooled.csproj | 25 ++++-- .../TargetsUntooled/EnsuresTarget.cs | 23 ----- .../TargetsUntooled/EnsuresTestTarget.cs | 26 ++++++ .../TargetsUntooled/OdinInvariantTarget.cs | 12 +-- .../OdinInvariantTestTarget.cs | 10 --- 9 files changed, 168 insertions(+), 63 deletions(-) delete mode 100644 DesignContracts/TargetsUntooled/EnsuresTarget.cs create mode 100644 DesignContracts/TargetsUntooled/EnsuresTestTarget.cs diff --git a/DesignContracts/Core/Contract.cs b/DesignContracts/Core/Contract.cs index 1513860..2344f32 100644 --- a/DesignContracts/Core/Contract.cs +++ b/DesignContracts/Core/Contract.cs @@ -89,13 +89,15 @@ public static void RequiresNotNull(object? argument, string? userMessage = "Argu /// An optional message describing the postcondition. /// An optional text representation of the condition expression. /// - /// Postconditions are evaluated only when is true. - /// Calls to this method become no-ops when postconditions are disabled. + /// Postconditions are evaluated only when is true. + /// Calls to this method are no-ops when postconditions are disabled. /// It is expected that source-generated code will invoke this method at /// appropriate points (typically immediately before method exit). /// public static void Ensures(bool condition, string? userMessage = null, string? conditionText = null) { + // For V1 Ensures calls are the only calls that will be rewritten + // Because this method is a merely no-op marker if (!ContractOptions.Current.EnablePostconditions) { return; @@ -202,6 +204,7 @@ public static void Invariant(bool condition, string? userMessage = null, string? /// /// Specifies an assertion that must hold true at the given point in the code. + /// Only evaluated if ContractOptions.EnableAssertions is true. /// /// The condition that must be true. /// An optional message describing the assertion. @@ -211,6 +214,11 @@ public static void Invariant(bool condition, string? userMessage = null, string? /// public static void Assert(bool condition, string? userMessage = null, string? conditionText = null) { + if (!ContractOptions.Current.EnableAssertions) + { + return; + } + if (!condition) { ReportFailure( diff --git a/DesignContracts/Core/ContractAttributes.cs b/DesignContracts/Core/ContractAttributes.cs index be45fa1..22c25bd 100644 --- a/DesignContracts/Core/ContractAttributes.cs +++ b/DesignContracts/Core/ContractAttributes.cs @@ -28,9 +28,11 @@ public class ClassInvariantMethodAttribute : Attribute // } /// - /// Identifies a method, property or field that does not change any state in the class. + /// Methods and classes marked with this attribute can be used within calls to Contract methods. Such methods do not make any state changes. /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field, Inherited = false)] + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | + AttributeTargets.Event | AttributeTargets.Delegate | AttributeTargets.Class | + AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public sealed class PureAttribute : Attribute { } diff --git a/DesignContracts/Core/ContractOptions.cs b/DesignContracts/Core/ContractOptions.cs index fa0663a..4768de3 100644 --- a/DesignContracts/Core/ContractOptions.cs +++ b/DesignContracts/Core/ContractOptions.cs @@ -29,30 +29,34 @@ public static ContractOptions Current } } - // Unable to understand how to get around initializing 1 static instance of Current in NUnit test runs. - // throw new InvalidOperationException("Current DesignContractOptions not initialized."); - /// - /// + /// Unable to understand how to get around initializing 1 static instance of Current in NUnit test runs. + /// throw new InvalidOperationException("Current DesignContractOptions not initialized."); /// /// public static void Initialize(ContractOptions options) => _current = options; /// - /// Gets or sets a value indicating whether postconditions should be evaluated at runtime. + /// Configures whether method Ensures calls weaved into the class by the Contracts Rewriter + /// before each method return evaluate their conditions or are bypassed. /// - /// - /// When false, calls to become no-ops. - /// public bool EnablePostconditions { get; init; } = true; /// - /// Gets or sets a value indicating whether object invariants should be evaluated at runtime. + /// Configures whether calls to the class invariant method weaved into the class + /// by the Contracts Rewriter are either executed or bypassed. /// - /// - /// When false, calls to become no-ops. - /// public bool EnableInvariants { get; init; } = true; + + /// + /// Configures whether calls evaluate their conditions or are bypassed. + /// + public bool EnableAssumptions { get; set; } = true; + + /// + /// Configures whether calls evaluate their conditions or are bypassed. + /// + public bool EnableAssertions { get; set; } = true; } } \ No newline at end of file diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index 4751041..24157bb 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -19,7 +19,9 @@ public void SetUp() ContractOptions.Initialize(new ContractOptions { EnableInvariants = true, - EnablePostconditions = true + EnablePostconditions = true, + EnableAssertions = true, + EnableAssumptions = true }); } @@ -207,6 +209,84 @@ public void Requires_throws_if_condition_broken(int testValue, bool shouldThrow) Assert.DoesNotThrow(() => sut.RequiresYGreaterThan10(testValue)); } } + + [Test] + [TestCase(3, true)] + [TestCase(13, false)] + public void Assert_throws_if_condition_broken(int testValue, bool shouldThrow) + { + OdinInvariantTestTarget sut = new OdinInvariantTestTarget(1); + + if (shouldThrow) + { + Exception? exception = Assert.Catch(() => sut.AssertYGreaterThan10(testValue)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception, Is.InstanceOf()); + ContractException ex = (ContractException)exception!; + Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Assertion)); + Assert.That(ex.Message, Is.EqualTo("Assertion failed: y must be greater than 10")); + } + else + { + Assert.DoesNotThrow(() => sut.AssertYGreaterThan10(testValue)); + } + } + + [Test] + [TestCase(3, true)] + [TestCase(13, false)] + public void Assume_throws_if_condition_broken(int testValue, bool shouldThrow) + { + OdinInvariantTestTarget sut = new OdinInvariantTestTarget(1); + + if (shouldThrow) + { + Exception? exception = Assert.Catch(() => sut.AssumeYGreaterThan10(testValue)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception, Is.InstanceOf()); + ContractException ex = (ContractException)exception!; + Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Assumption)); + Assert.That(ex.Message, Is.EqualTo("Assumption failed: y must be greater than 10")); + } + else + { + Assert.DoesNotThrow(() => sut.AssumeYGreaterThan10(testValue)); + } + } + + + [Test] + public void Single_postcondition_is_woven([Values("EnsuresPlus5A", "EnsuresPlus5B")] string methodName, + [Values(3, 130)] int testValue) + { + bool exceptionExpected = testValue > 100; + Type targetUnwrittenType = typeof(EnsuresTestTarget); + using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetUnwrittenType.Assembly); + Type targetWrittenType = context.GetTypeOrThrow(targetUnwrittenType.FullName!); + + object ensuresTestTarget = Activator.CreateInstance(targetWrittenType)!; + + if (exceptionExpected) + { + Exception? ex = Assert.Catch(() => + { + try + { + CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testValue]); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + }); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Contains.Substring("Postcondition not honoured")); + } + else + { + Assert.DoesNotThrow(() => CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testValue])); + } + } private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) { @@ -214,6 +294,13 @@ private static void SetPrivateField(Type declaringType, object instance, string ?? throw new InvalidOperationException($"Missing field '{fieldName}'."); f.SetValue(instance, value); } + + private static void CallMethod(Type declaringType, object instance, string methodName, object[] parameters) + { + MethodInfo method = declaringType.GetMethods().FirstOrDefault(m => m.Name == methodName) + ?? throw new InvalidOperationException($"Missing method '{methodName}'."); + method.Invoke(instance, parameters); + } private static object? Invoke(Type declaringType, object instance, string methodName) { diff --git a/DesignContracts/TargetsTooled/TargetsTooled.csproj b/DesignContracts/TargetsTooled/TargetsTooled.csproj index fd4b545..0b7a26a 100644 --- a/DesignContracts/TargetsTooled/TargetsTooled.csproj +++ b/DesignContracts/TargetsTooled/TargetsTooled.csproj @@ -9,14 +9,6 @@ Targets $(DefineConstants);CONTRACTS_FULL - - - BclInvariantTarget.cs - - - OdinInvariantTarget.cs - - + + $(DefineConstants);CONTRACTS_FULL - - + BclInvariantTestTarget.cs - - EnsuresTestTarget.cs - - + NoInvariantTestTarget.cs - - OdinInvariantTarget.cs - - + OdinInvariantTestTarget.cs + + PostconditionsTestTarget.cs + - - \ No newline at end of file diff --git a/DesignContracts/TargetsUntooled/BclInvariantTestTarget.cs b/DesignContracts/TargetsUnrewritten/BclInvariantTestTarget.cs similarity index 100% rename from DesignContracts/TargetsUntooled/BclInvariantTestTarget.cs rename to DesignContracts/TargetsUnrewritten/BclInvariantTestTarget.cs diff --git a/DesignContracts/TargetsUntooled/NoInvariantTestTarget.cs b/DesignContracts/TargetsUnrewritten/NoInvariantTestTarget.cs similarity index 100% rename from DesignContracts/TargetsUntooled/NoInvariantTestTarget.cs rename to DesignContracts/TargetsUnrewritten/NoInvariantTestTarget.cs diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs b/DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs similarity index 79% rename from DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs rename to DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs index 34584ca..6a83f8c 100644 --- a/DesignContracts/TargetsUntooled/OdinInvariantTestTarget.cs +++ b/DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs @@ -6,11 +6,11 @@ namespace Targets /// The rewriter is expected to inject /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. ///
- public sealed class OdinInvariantTarget + public sealed class OdinInvariantTestTarget { private int _value; - public OdinInvariantTarget(int value) + public OdinInvariantTestTarget(int value) { _value = value; } @@ -66,5 +66,15 @@ public void RequiresYGreaterThan10(int y) Console.WriteLine("Instruction 2"); Console.WriteLine("Instruction 2"); } + + public void AssertYGreaterThan10(int y) + { + Contract.Assert(y > 10, "y must be greater than 10"); + } + + public void AssumeYGreaterThan10(int y) + { + Contract.Assume(y > 10, "y must be greater than 10"); + } } } diff --git a/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs b/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs new file mode 100644 index 0000000..4bd6704 --- /dev/null +++ b/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs @@ -0,0 +1,128 @@ + + +using Odin.DesignContracts; + +namespace Targets +{ + public sealed class PostconditionsTestTarget + { + public int Number { get; set; } + public string String { get; set; } = ""; + + /// + /// 1 + /// + /// + public void VoidSingleConditionSingleReturn(int number) + { + Contract.Ensures(Number > 0); + Number = number; + } + + /// + /// 2 + /// + /// + public void VoidSingleConditionMultipleReturn(int number) + { + Contract.Ensures(Number > 0); + if (number > 100) + { + Number = -100; + return; + } + Number = number; + } + + /// + /// 3 + /// + /// + /// + public void VoidMultipleConditionsSingleReturn(int number, string aString) + { + Contract.Ensures(Number > 0); + Contract.Ensures(String.Length == 2); + Contract.Ensures(Number > 0); + Number = number; + String = aString; + } + + /// + /// 4 + /// + /// + /// + public void VoidMultipleConditionsMultipleReturn(int number, string aString) + { + Contract.Ensures(Number > 0); + Contract.Ensures(String.Length == 2); + String = aString; + if (number > 100) + { + Number = -100; + return; + } + Number = number; + } + + /// + /// 5 + /// + /// + public int NotVoidSingleConditionSingleReturn(int number) + { + Contract.Ensures(Number > 0); + Number = number; + return Number; + } + + /// + /// 6 + /// + /// + public int NotVoidSingleConditionMultipleReturn(int number) + { + Contract.Ensures(Number > 0); + if (number > 100) + { + Number = -100; + return Number; + } + Number = number; + return Number; + } + + /// + /// 7 + /// + /// + /// + public void NotVoidMultipleConditionsSingleReturn(int number, string aString) + { + Contract.Ensures(Number > 0); + Contract.Ensures(String.Length == 2); + Contract.Ensures(Number > 0); + Number = number; + String = aString; + } + + /// + /// 8 + /// + /// + /// + public void NotVoidMultipleConditionsMultipleReturn(int number, string aString) + { + Contract.Ensures(Number > 0); + Contract.Ensures(String.Length == 2); + String = aString; + if (number > 100) + { + Number = -100; + return; + } + Number = number; + } + } +} diff --git a/DesignContracts/TargetsUntooled/TargetsUntooled.csproj b/DesignContracts/TargetsUnrewritten/TargetsUnrewritten.csproj similarity index 100% rename from DesignContracts/TargetsUntooled/TargetsUntooled.csproj rename to DesignContracts/TargetsUnrewritten/TargetsUnrewritten.csproj diff --git a/DesignContracts/TargetsUntooled/EnsuresTestTarget.cs b/DesignContracts/TargetsUntooled/EnsuresTestTarget.cs deleted file mode 100644 index a6bad62..0000000 --- a/DesignContracts/TargetsUntooled/EnsuresTestTarget.cs +++ /dev/null @@ -1,26 +0,0 @@ - - -using Odin.DesignContracts; - -namespace Targets -{ - public sealed class EnsuresTestTarget - { - public int Number { get; set; } - public int EnsuresPlus5A(int testValue) - { - Contract.Ensures(Contract.Result() == testValue + 5); - if (testValue > 100) return testValue + 2; // Break the post-condition for testValue > 100... - return testValue + 5; - } - - public int EnsuresPlus5B(int testValue) - { - Contract.Ensures(Contract.Result() == testValue + 5); - Contract.Requires(testValue > 0); - if (testValue > 100) return testValue + 2; // Break the contract for testValue > 100... - return testValue + 5; - } - - } -} diff --git a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs b/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs deleted file mode 100644 index e7818dd..0000000 --- a/DesignContracts/TargetsUntooled/OdinInvariantTarget.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Odin.DesignContracts; - -namespace Targets -{ - /// - /// The rewriter is expected to inject - /// invariant calls at entry/exit of public methods and properties, except where [Pure] is applied. - /// - public sealed class OdinInvariantTestTarget - { - private int _value; - - public OdinInvariantTestTarget(int value) - { - _value = value; - } - - [ClassInvariantMethod] - public void ObjectInvariant() - { - Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); - } - - public void Increment() - { - _value++; - } - - public async Task AsyncIncrement() - { - _value++; - return await Task.FromResult(_value); - } - - - public void MakeInvalid() - { - _value = -1; - } - - public async Task AsyncMakeInvalid() - { - _value = -1; - return await Task.FromResult(_value); - } - - [Pure] - public void PureCommand() - { - - } - - - [Pure] - public int PureGetValue() => _value; - - [Pure] - public int PureProperty => _value; - - public int NonPureProperty => _value; - - public void RequiresYGreaterThan10(int y) - { - Contract.Requires(y > 10, "y must be greater than 10"); - Console.WriteLine("Instruction 1"); - Console.WriteLine("Instruction 2"); - Console.WriteLine("Instruction 2"); - } - - public void AssertYGreaterThan10(int y) - { - Contract.Assert(y > 10, "y must be greater than 10"); - } - - public void AssumeYGreaterThan10(int y) - { - Contract.Assume(y > 10, "y must be greater than 10"); - } - - - } -} diff --git a/Odin.sln b/Odin.sln index d2d2a9b..99cb194 100644 --- a/Odin.sln +++ b/Odin.sln @@ -103,9 +103,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.DesignContracts.Toolin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Odin.DesignContracts.Rewriter", "DesignContracts\RewriterTests\Tests.Odin.DesignContracts.Rewriter.csproj", "{320907F6-F4BA-4A1D-AC23-145A5AD06C14}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetsUntooled", "DesignContracts\TargetsUntooled\TargetsUntooled.csproj", "{7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetsUnrewritten", "DesignContracts\TargetsUnrewritten\TargetsUnrewritten.csproj", "{7BC62DE2-D0F7-4CD9-83D0-C47BE760F33D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetsTooled", "DesignContracts\TargetsTooled\TargetsTooled.csproj", "{8138E8B4-E90D-45A3-BEB0-31F048948A91}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetsRewritten", "DesignContracts\TargetsRewritten\TargetsRewritten.csproj", "{8138E8B4-E90D-45A3-BEB0-31F048948A91}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From ab4390126c3b146543c6e4b4067ec980595956b5 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Thu, 25 Dec 2025 21:11:43 +0200 Subject: [PATCH 21/27] Saving progress --- DesignContracts/Core/Contract.cs | 4 + DesignContracts/CoreTests/ContractTests.cs | 106 +++++++++++++-------- 2 files changed, 71 insertions(+), 39 deletions(-) diff --git a/DesignContracts/Core/Contract.cs b/DesignContracts/Core/Contract.cs index 738d550..22c704a 100644 --- a/DesignContracts/Core/Contract.cs +++ b/DesignContracts/Core/Contract.cs @@ -166,6 +166,10 @@ public static void Assume(bool condition, string? userMessage = null, string? co /// public static event EventHandler? ContractFailed; + internal static void ResetContractFailedEventHandlers() + { + ContractFailed = null; + } internal static void ReportFailure(ContractHandlingBehaviour handling, ContractKind kind, string? userMessage, string? conditionText) diff --git a/DesignContracts/CoreTests/ContractTests.cs b/DesignContracts/CoreTests/ContractTests.cs index bdf8928..57d53ba 100644 --- a/DesignContracts/CoreTests/ContractTests.cs +++ b/DesignContracts/CoreTests/ContractTests.cs @@ -6,62 +6,91 @@ namespace Tests.Odin.DesignContracts [TestFixture] public sealed class ContractTests { - [SetUp] - public void Setup() - { - } - [Test] - public void Contract_handling_escalates_on_failure_for_certain_behaviours([Values] ContractHandlingBehaviour behaviour, - [Values] ContractKind kind) + public void Contract_failure_handling_depends_on_configured_behaviours( + [Values] ContractHandlingBehaviour behaviour, [Values] ContractKind kind) { ContractOptions.Initialize(ContractOptions.All(behaviour)); - + AssertConfiguredHandlingFor(kind, behaviour); if (behaviour == ContractHandlingBehaviour.Bypass | behaviour == ContractHandlingBehaviour.EventHandlersOnly) { - Assert.DoesNotThrow(() => + Assert.DoesNotThrow(() => Contract.HandleContractCondition(kind, false, "userMessage", "conditionText")); } else { - ContractException? ex = Assert.Throws(() => + ContractException? ex = Assert.Throws(() => Contract.HandleContractCondition(kind, false, "userMessage", "conditionText")); Assert.That(ex, Is.Not.Null); Assert.That(ex!.Message, Contains.Substring("userMessage")); Assert.That(ex!.Message, Contains.Substring("conditionText")); } } - + [Test] - public void Contract_handling_fires_ContractFailed_delegates_for_certain_behaviours( - [Values] ContractHandlingBehaviour behaviour, [Values] ContractKind kind) + [TestCase(ContractHandlingBehaviour.Bypass, false, false)] + [TestCase(ContractHandlingBehaviour.EscalationOnly, false, true)] + [TestCase(ContractHandlingBehaviour.EventHandlersOnly, true, false)] + [TestCase(ContractHandlingBehaviour.EventHandlersAndEscalation, true, false)] + public void Contract_handling_fires_ContractFailed_delegates_depending_on_configured_behaviour( + ContractHandlingBehaviour behaviour, bool handlerShouldFire, bool exceptionShouldBeThrown) { ContractOptions.Initialize(ContractOptions.All(behaviour)); - AssertConfiguredHandlingFor(kind, behaviour); - bool contractFailedHandled = false; - - if (behaviour == ContractHandlingBehaviour.EscalationOnly || - behaviour == ContractHandlingBehaviour.Bypass) - { - Contract.ContractFailed += (sender, args) - => Assert.Fail("Contract failed event handler should not have been called"); - // Should not Assert.Fail - Contract.HandleContractCondition(kind, false, "userMessage", "conditionText"); - } - else + foreach (ContractKind kind in AllKinds()) { - Contract.ContractFailed += (sender, args) => + AssertConfiguredHandlingFor(kind, behaviour); + bool handlerFiredFlag = false; + EventHandler handler = (sender, args) => { - contractFailedHandled = true; + handlerFiredFlag = true; args.Handled = true; }; + Contract.ContractFailed += handler; - Contract.HandleContractCondition(kind, false, "userMessage", "conditionText"); - Assert.That(contractFailedHandled, Is.True, "Contract failed event handler was called"); + if (exceptionShouldBeThrown) + { + Assert.Throws(() => + Contract.HandleContractCondition(kind, false, "userMessage", "conditionText")); + } + else + { + Contract.HandleContractCondition(kind, false, "userMessage", "conditionText"); + } + + Assert.That(handlerFiredFlag, Is.EqualTo(handlerShouldFire)); + Contract.ContractFailed -= handler; } } + [Test] + [Description("For EventHandlersAndEscalation the event handler can set the Handled property")] + public void Contract_event_handler_can_handle_contract_failure( + [Values] ContractKind kind, [Values] bool handleFailure) + { + ContractOptions.Initialize(ContractOptions.All(ContractHandlingBehaviour.EventHandlersAndEscalation)); + AssertConfiguredHandlingFor(kind, ContractHandlingBehaviour.EventHandlersAndEscalation); + bool exceptionShouldBeThrown = !handleFailure; + EventHandler handler = (sender, args) => + { + args.Handled = handleFailure; + }; + Contract.ContractFailed += handler; + + if (exceptionShouldBeThrown) + { + Assert.Throws(() => + Contract.HandleContractCondition(kind, false, "msg")); + } + else + { + Assert.DoesNotThrow(() => + Contract.HandleContractCondition(kind, false, "msg")); + } + + Contract.ContractFailed -= handler; + } + [Test] [TestCase("userMessage", "conditionText", "{0}: userMessage [Condition: conditionText]")] [TestCase("userMessage", " ", "{0}: userMessage")] @@ -76,15 +105,15 @@ public void Contract_handling_fires_ContractFailed_delegates_for_certain_behavio [TestCase(null, "conditionText", "{0}: conditionText")] [TestCase("", "conditionText", "{0}: conditionText")] [TestCase(" ", "conditionText", "{0}: conditionText")] - public void Escalation_exception_message_formatting(string? userMessage, string? conditionText, + public void Escalation_exception_message_formatting(string? userMessage, string? conditionText, string messageFormat) { ContractOptions.Initialize(ContractOptions.On()); foreach (ContractKind kind in AllKinds()) { string messageExpected = string.Format(messageFormat, Contract.GetKindFailedText(kind)); - ContractException? ex = Assert.Throws(() => - Contract.HandleContractCondition(kind, false, userMessage,conditionText)); + ContractException? ex = Assert.Throws(() => + Contract.HandleContractCondition(kind, false, userMessage, conditionText)); Assert.That(ex, Is.Not.Null); Assert.That(ex!.Message, Is.EqualTo(messageExpected), "Exception message is incorrect"); } @@ -113,24 +142,23 @@ public void Requires_not_null_throws_contract_exception_if_argument_null() { ContractOptions.Initialize(ContractOptions.On()); - ContractException? exception = Assert.Throws(() => + ContractException? exception = Assert.Throws(() => Contract.RequiresNotNull(null as string, "myArg is required.")); - + Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Is.EqualTo("Precondition not met: myArg is required.")); Assert.That(exception.UserMessage, Is.EqualTo("myArg is required.")); } - - private void AssertConfiguredHandlingFor(ContractKind kind, ContractHandlingBehaviour handlingBehaviour) + + internal void AssertConfiguredHandlingFor(ContractKind kind, ContractHandlingBehaviour handlingBehaviour) { Assert.That(ContractOptions.Current.GetBehaviourFor(kind), Is.EqualTo(handlingBehaviour), $"Expected {kind} handling to be {handlingBehaviour}"); } - - private ContractKind[] AllKinds() + + internal ContractKind[] AllKinds() { return Enum.GetValues(); } - } [TestFixture(typeof(ArgumentNullException))] From 9d6285ea80dcbf7df80b0af4eee33512397f8eed Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Fri, 26 Dec 2025 12:06:15 +0200 Subject: [PATCH 22/27] Completed tests for ReporFailure --- .../CoreTests/ContractInvariantTests.cs | 3 +-- DesignContracts/Rewriter/MethodHandler.cs | 8 +++++++- DesignContracts/Rewriter/Program.cs | 2 +- DesignContracts/Rewriter/TypeHandler.cs | 16 ++++++++++++++++ DesignContracts/RewriterTests/RewriterTests.cs | 10 +++++----- .../TargetsRewritten/TargetsRewritten.csproj | 2 ++ .../PostconditionsTestTarget.cs | 5 +++-- .../Tooling/Odin.DesignContracts.Tooling.csproj | 1 + .../Odin.DesignContracts.Tooling.targets | 5 +++-- 9 files changed, 39 insertions(+), 13 deletions(-) diff --git a/DesignContracts/CoreTests/ContractInvariantTests.cs b/DesignContracts/CoreTests/ContractInvariantTests.cs index 4b840ec..057c7af 100644 --- a/DesignContracts/CoreTests/ContractInvariantTests.cs +++ b/DesignContracts/CoreTests/ContractInvariantTests.cs @@ -7,9 +7,8 @@ namespace Tests.Odin.DesignContracts [TestFixture][Ignore("Get tooling working first...")] public sealed class ContractInvariantTests { - [Test] - public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour testCase) + public void Public_constructor_has_invariant_weaved_on_exit([Values] AttributeFlavour testCase) { Assert.That(ContractOptions.Current.Invariants, Is.EqualTo(ContractHandlingBehaviour.EventHandlersAndEscalation)); diff --git a/DesignContracts/Rewriter/MethodHandler.cs b/DesignContracts/Rewriter/MethodHandler.cs index c96cfd2..9b3b431 100644 --- a/DesignContracts/Rewriter/MethodHandler.cs +++ b/DesignContracts/Rewriter/MethodHandler.cs @@ -21,7 +21,12 @@ public MethodHandler(MethodDefinition method, TypeHandler parentHandler) public MethodDefinition Method { get; } - public bool TryRewrite() + /// + /// Rewrites the type member returning true if any contract rewrites + /// were needed, and false if none were required. + /// + /// + public bool Rewrite() { // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. // if (method.IsAsync) @@ -50,6 +55,7 @@ public bool TryRewrite() // Inject invariant call at entry (before any user code). if (invariantsToDo.OnEntry) { + // An instructionless member body is possible don't understand how or why Instruction first = Method.Body.Instructions.FirstOrDefault() ?? Instruction.Create(OpCodes.Nop); if (Method.Body.Instructions.Count == 0) il.Append(first); diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 034cae1..b2f4c66 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -69,7 +69,7 @@ internal static void RewriteAssembly(string assemblyPath, string outputPath) foreach (var member in currentType.GetMembersToTryRewrite()) { - if (!member.TryRewrite()) + if (!member.Rewrite()) continue; rewritten++; } diff --git a/DesignContracts/Rewriter/TypeHandler.cs b/DesignContracts/Rewriter/TypeHandler.cs index 4022313..a2023b4 100644 --- a/DesignContracts/Rewriter/TypeHandler.cs +++ b/DesignContracts/Rewriter/TypeHandler.cs @@ -43,6 +43,22 @@ public MethodDefinition? InvariantMethod } } + /// + /// Rewrites the type returning the number of members rewritten. + /// + /// + public int Rewrite() + { + int rewritten = 0; + foreach (var member in GetMembersToTryRewrite()) + { + if (!member.Rewrite()) + continue; + rewritten++; + } + return rewritten; + } + internal IReadOnlyList GetMembersToTryRewrite() { return _target.Methods.Select(c => new MethodHandler(c,this)).ToList(); diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index 4b0baec..27b3ab2 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -249,10 +249,10 @@ public void Assume_throws_if_condition_broken(int testValue, bool shouldThrow) [Test] - public void Single_postcondition_is_woven([Values("EnsuresPlus5A", "EnsuresPlus5B")] string methodName, - [Values(3, 130)] int testValue) + public void Single_postcondition_is_woven([Values("VoidSingleConditionSingleReturn")] string methodName, + [Values(-1, 1)] int testNumber) { - bool exceptionExpected = testValue > 100; + bool exceptionExpected = testNumber < 0; Type targetUnwrittenType = typeof(PostconditionsTestTarget); using RewrittenAssemblyContext context = new RewrittenAssemblyContext(targetUnwrittenType.Assembly); Type targetWrittenType = context.GetTypeOrThrow(targetUnwrittenType.FullName!); @@ -265,7 +265,7 @@ public void Single_postcondition_is_woven([Values("EnsuresPlus5A", "EnsuresPlus5 { try { - CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testValue]); + CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testNumber]); } catch (TargetInvocationException tie) when (tie.InnerException is not null) { @@ -277,7 +277,7 @@ public void Single_postcondition_is_woven([Values("EnsuresPlus5A", "EnsuresPlus5 } else { - Assert.DoesNotThrow(() => CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testValue])); + Assert.DoesNotThrow(() => CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testNumber])); } } diff --git a/DesignContracts/TargetsRewritten/TargetsRewritten.csproj b/DesignContracts/TargetsRewritten/TargetsRewritten.csproj index d4f7b1f..166e5c2 100644 --- a/DesignContracts/TargetsRewritten/TargetsRewritten.csproj +++ b/DesignContracts/TargetsRewritten/TargetsRewritten.csproj @@ -21,6 +21,7 @@ + @@ -36,4 +37,5 @@ PostconditionsTestTarget.cs + \ No newline at end of file diff --git a/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs b/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs index 4bd6704..65dd6ad 100644 --- a/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs +++ b/DesignContracts/TargetsUnrewritten/PostconditionsTestTarget.cs @@ -10,7 +10,8 @@ public sealed class PostconditionsTestTarget public string String { get; set; } = ""; /// - /// 1 + /// number = -1 -> failure + /// number = 1 -> fine /// /// public void VoidSingleConditionSingleReturn(int number) @@ -26,7 +27,7 @@ public void VoidSingleConditionSingleReturn(int number) public void VoidSingleConditionMultipleReturn(int number) { Contract.Ensures(Number > 0); - if (number > 100) + if (number > 50) { Number = -100; return; diff --git a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj index 4775483..5731145 100644 --- a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj +++ b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj @@ -9,6 +9,7 @@ Design-by-Contract tooling for Odin.DesignContracts: Analyzers + IL rewriter integration. icon.png 1591;1573; + true diff --git a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets index bf29273..f93d61c 100644 --- a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets +++ b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets @@ -10,14 +10,15 @@ <_OdinNetVersion>$(TargetFramework.Substring(3)) <_OdinDcRewriterPathCandidate>$(MSBuildThisFileDirectory)..\tools\net$(_OdinNetVersion)\any\Odin.DesignContracts.Rewriter.dll <_OdinDcRewriterPath Condition="Exists('$(_OdinDcRewriterPathCandidate)')">$(_OdinDcRewriterPathCandidate) - <_OdinDcRewriterPath Condition="'$(_OdinDcRewriterPath)'==''">$(MSBuildThisFileDirectory)..\tools\net10.0\any\Odin.DesignContracts.Rewriter.dll + + <_OdinDcRewriterPath Condition="'$(_OdinDcRewriterPath)'==''">$(MSBuildThisFileDirectory)..\tools\net10.0\any\Odin.DesignContracts.Rewriter.dll <_OdinDcWeavedAssembly>$(IntermediateOutputPath)$(AssemblyName).odin-design-contracts-weaved$(TargetExt) <_OdinDcWeavedPdb>$(IntermediateOutputPath)$(AssemblyName).odin-design-contracts-weaved.pdb From 6e22b238cb6c5631858d204f72d2516c6677c0ee Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Fri, 26 Dec 2025 16:49:12 +0200 Subject: [PATCH 23/27] Saving progress --- .gitignore | 3 + DesignContracts/Rewriter/AssemblyRewriter.cs | 84 +++++++++++++++ .../Rewriter/InvariantWeavingRequirement.cs | 7 -- .../{MethodHandler.cs => MethodRewriter.cs} | 29 ++--- DesignContracts/Rewriter/Names.cs | 8 +- .../Odin.DesignContracts.Rewriter.csproj | 1 + DesignContracts/Rewriter/Program.cs | 59 ++--------- .../{TypeHandler.cs => TypeRewriter.cs} | 10 +- .../Rewriter/WeaveDesignContracts.cs | 36 +++++++ ...HandlerTests.cs => MethodRewriterTests.cs} | 12 +-- .../RewriterTests/RewriterTests.cs | 3 +- .../RewriterTests/RewrittenAssemblyContext.cs | 11 +- ...peHandlerTests.cs => TypeRewriterTests.cs} | 4 +- .../Odin.DesignContracts.Tooling.csproj | 100 +++++++++--------- .../Odin.DesignContracts.Tooling.targets | 59 ++++------- Odin.sln | 1 + 16 files changed, 249 insertions(+), 178 deletions(-) create mode 100644 DesignContracts/Rewriter/AssemblyRewriter.cs delete mode 100644 DesignContracts/Rewriter/InvariantWeavingRequirement.cs rename DesignContracts/Rewriter/{MethodHandler.cs => MethodRewriter.cs} (90%) rename DesignContracts/Rewriter/{TypeHandler.cs => TypeRewriter.cs} (90%) create mode 100644 DesignContracts/Rewriter/WeaveDesignContracts.cs rename DesignContracts/RewriterTests/{MethodHandlerTests.cs => MethodRewriterTests.cs} (69%) rename DesignContracts/RewriterTests/{TypeHandlerTests.cs => TypeRewriterTests.cs} (88%) diff --git a/.gitignore b/.gitignore index 90d6905..fe929b7 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,9 @@ bld/ [Oo]bj/ [Ll]og/ +# Locally provisioned tools +tools/ + # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot diff --git a/DesignContracts/Rewriter/AssemblyRewriter.cs b/DesignContracts/Rewriter/AssemblyRewriter.cs new file mode 100644 index 0000000..28ada14 --- /dev/null +++ b/DesignContracts/Rewriter/AssemblyRewriter.cs @@ -0,0 +1,84 @@ +using Mono.Cecil; + +namespace Odin.DesignContracts.Rewriter; + +/// +/// Handles Design-by-Contract rewriting of a given assembly. +/// +public class AssemblyRewriter +{ + /// + /// Constructor + /// + /// The input assembly path, + /// typically in the 'intermediate build output' under the 'obj' folder. + /// If omitted, defaults to 'InputAssembly.odin-design-contracts-weaved.dll' + /// in the same folder as the input assembly. + /// Thrown if the assembly to rewrite does not exist. + public AssemblyRewriter(string targetAssemblyPath, + string? outputPath = null) + { + TargetAssemblyPath = targetAssemblyPath; + OutputPath = outputPath; + if (!File.Exists(TargetAssemblyPath)) + { + throw new FileNotFoundException($"Assembly not found: {TargetAssemblyPath}"); + } + } + + /// + /// The assembly to rewrite. Note that if OutputPath is specified, + /// the rewritten assembly is saved to OutputPath, else the assembly located + /// as TargetPath is overwritten after a call to Rewrite() + /// + public string TargetAssemblyPath { get; } + + internal bool DoesTargetAssemblyHaveDebugSymbols() + => File.Exists(Path.ChangeExtension(TargetAssemblyPath, ".pdb")); + + /// + /// Optional path that the rewritten assembly should be written to, else TargetPath is overwritten. + /// + public string? OutputPath { get; } + + internal string GetOutputPath() + => OutputPath ?? TargetAssemblyPath; + + /// + /// Writes invariants and postconditions into the assembly at TargetPath, + /// or OutputPath if specified. + /// + public void Rewrite() + { + string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(TargetAssemblyPath))!; + DefaultAssemblyResolver resolver = new(); + resolver.AddSearchDirectory(assemblyDir); + + // Portable PDBs are optional. If present, Cecil will pick them up with ReadSymbols = true. + ReaderParameters readerParameters = new() + { + AssemblyResolver = resolver, + ReadSymbols = DoesTargetAssemblyHaveDebugSymbols(), + ReadingMode = ReadingMode.Immediate + }; + + // Read the bytes into memory first to fully decouple the reader from the file on disk. + using MemoryStream ms = new MemoryStream(File.ReadAllBytes(TargetAssemblyPath)); + using AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(ms, readerParameters); + int rewritten = 0; + foreach (ModuleDefinition module in assembly.Modules) + { + foreach (TypeDefinition type in module.GetTypes()) + { + TypeRewriter currentType = new(type); + rewritten += currentType.Rewrite(); + } + } + WriterParameters writerParameters = new() + { + WriteSymbols = readerParameters.ReadSymbols + }; + assembly.Write(GetOutputPath(), writerParameters); + Console.WriteLine($"Rewriter: {rewritten} methods rewritten."); + } +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/InvariantWeavingRequirement.cs b/DesignContracts/Rewriter/InvariantWeavingRequirement.cs deleted file mode 100644 index 112814f..0000000 --- a/DesignContracts/Rewriter/InvariantWeavingRequirement.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Odin.DesignContracts.Rewriter; - -internal record InvariantWeavingRequirement -{ - public required bool OnEntry { get; init; } - public required bool OnExit { get; init; } -} \ No newline at end of file diff --git a/DesignContracts/Rewriter/MethodHandler.cs b/DesignContracts/Rewriter/MethodRewriter.cs similarity index 90% rename from DesignContracts/Rewriter/MethodHandler.cs rename to DesignContracts/Rewriter/MethodRewriter.cs index 9b3b431..ca6873e 100644 --- a/DesignContracts/Rewriter/MethodHandler.cs +++ b/DesignContracts/Rewriter/MethodRewriter.cs @@ -6,17 +6,17 @@ namespace Odin.DesignContracts.Rewriter; /// -/// Handles member-specific matters with respect to design contract rewriting. -/// Note: MemberHandler includes methods AND property accessors... +/// Handles Design-by-Contract rewriting of a given type member, +/// including methods, property accessors and constructors. /// -internal class MethodHandler +internal class MethodRewriter { - private readonly TypeHandler _parentHandler; + private readonly TypeRewriter _parentRewriter; - public MethodHandler(MethodDefinition method, TypeHandler parentHandler) + public MethodRewriter(MethodDefinition method, TypeRewriter parentRewriter) { Method = method; - _parentHandler = parentHandler; + _parentRewriter = parentRewriter; } public MethodDefinition Method { get; } @@ -60,7 +60,7 @@ public bool Rewrite() if (Method.Body.Instructions.Count == 0) il.Append(first); - InsertInvariantCallBefore(il, first, _parentHandler.InvariantMethod!); + InsertInvariantCallBefore(il, first, _parentRewriter.InvariantMethod!); } // Remove the postconditions from the method when postconditions are present. @@ -115,7 +115,7 @@ public bool Rewrite() if (invariantsToDo.OnExit) { - InsertInvariantCallBefore(il, returnInst, _parentHandler.InvariantMethod!); + InsertInvariantCallBefore(il, returnInst, _parentRewriter.InvariantMethod!); } if (!isVoid) @@ -131,8 +131,8 @@ public bool Rewrite() public InvariantWeavingRequirement IsInvariantToBeWeaved() { - bool canWeaveInvariant = _parentHandler.HasInvariant && IsPublicInstanceMethod(); - bool isInvariantMethodItself = _parentHandler.InvariantMethod is not null && Method == _parentHandler.InvariantMethod; + bool canWeaveInvariant = _parentRewriter.HasInvariant && IsPublicInstanceMethod(); + bool isInvariantMethodItself = _parentRewriter.InvariantMethod is not null && Method == _parentRewriter.InvariantMethod; // Per requirements: // - Constructors: invariants at exit only @@ -234,7 +234,7 @@ public bool IsPure() // For accessors, also honour [Pure] on the property itself. if (Method.IsGetter || Method.IsSetter) { - PropertyDefinition? prop = _parentHandler.Type.Properties. + PropertyDefinition? prop = _parentRewriter.Type.Properties. FirstOrDefault(p => p.GetMethod == Method || p.SetMethod == Method); if (prop is not null && prop.HasAnyAttributeIn(Names.PureAttributeFullNames)) return true; @@ -262,5 +262,10 @@ private void InsertInvariantCallBefore(ILProcessor il, Instruction before, Metho il.InsertBefore(before, Instruction.Create(OpCodes.Ldarg_0)); il.InsertBefore(before, Instruction.Create(OpCodes.Call, invariantMethod)); } - + + internal record InvariantWeavingRequirement + { + public required bool OnEntry { get; init; } + public required bool OnExit { get; init; } + } } \ No newline at end of file diff --git a/DesignContracts/Rewriter/Names.cs b/DesignContracts/Rewriter/Names.cs index 81822f0..0f42d64 100644 --- a/DesignContracts/Rewriter/Names.cs +++ b/DesignContracts/Rewriter/Names.cs @@ -6,11 +6,11 @@ namespace Odin.DesignContracts.Rewriter; internal static class Names { internal const string BclContractNamespace = "System.Diagnostics.Contracts"; - internal const string OdinContractNamespace = "Odin.DesignContracts"; - internal const string OdinPostconditionEnsuresTypeFullName = OdinContractNamespace + ".Contract"; - internal const string OdinInvariantAttributeFullName = OdinContractNamespace + ".ClassInvariantMethodAttribute"; + internal const string OdinDesignContractsNamespace = "Odin.DesignContracts"; + internal const string OdinPostconditionEnsuresTypeFullName = OdinDesignContractsNamespace + ".Contract"; + internal const string OdinInvariantAttributeFullName = OdinDesignContractsNamespace + ".ClassInvariantMethodAttribute"; internal const string BclInvariantAttributeFullName = BclContractNamespace + ".ContractInvariantMethodAttribute"; - internal const string OdinPureAttributeFullName = OdinContractNamespace + ".PureAttribute"; + internal const string OdinPureAttributeFullName = OdinDesignContractsNamespace + ".PureAttribute"; internal const string BclPureAttributeFullName = BclContractNamespace + ".PureAttribute"; internal static readonly string[] PureAttributeFullNames = [OdinPureAttributeFullName, BclPureAttributeFullName]; internal static readonly string[] InvariantAttributeFullNames = [OdinInvariantAttributeFullName, BclInvariantAttributeFullName]; diff --git a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj index e613d2b..6e1cda6 100644 --- a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj @@ -9,6 +9,7 @@ + diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index b2f4c66..13e93ed 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -1,7 +1,3 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Cecil.Rocks; - namespace Odin.DesignContracts.Rewriter; /// @@ -15,14 +11,18 @@ internal static class Program private static int Main(string[] args) { - if (args.Length < 2) + if (args.Length < 1) { - Console.Error.WriteLine($"{Rewriter}: Usage 'Odin.DesignContracts.Rewriter '"); + Console.Error.WriteLine($"{Rewriter}: Usage 'dotnet {Rewriter}.dll '"); return 3; } string assemblyPath = args[0]; - string outputPath = args[1]; + string? outputPath = null; + if (args.Length >= 2) + { + outputPath = args[1]; + } if (!File.Exists(assemblyPath)) { @@ -32,7 +32,8 @@ private static int Main(string[] args) try { - RewriteAssembly(assemblyPath, outputPath); + AssemblyRewriter contractsRewriter = new AssemblyRewriter(assemblyPath, outputPath); + contractsRewriter.Rewrite(); return 0; } catch (Exception ex) @@ -43,49 +44,7 @@ private static int Main(string[] args) } } - internal static void RewriteAssembly(string assemblyPath, string outputPath) - { - string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(assemblyPath))!; - - DefaultAssemblyResolver resolver = new(); - resolver.AddSearchDirectory(assemblyDir); - - // Portable PDBs are optional. If present, Cecil will pick them up with ReadSymbols = true. - ReaderParameters readerParameters = new() - { - AssemblyResolver = resolver, - ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")), - ReadingMode = ReadingMode.Immediate - }; - - using AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); - int rewritten = 0; - foreach (ModuleDefinition module in assembly.Modules) - { - foreach (TypeDefinition type in module.GetTypes()) - { - TypeHandler currentType = new(type); - - foreach (var member in currentType.GetMembersToTryRewrite()) - { - if (!member.Rewrite()) - continue; - rewritten++; - } - } - } - - Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))!); - - WriterParameters writerParameters = new() - { - WriteSymbols = readerParameters.ReadSymbols - }; - - assembly.Write(outputPath, writerParameters); - Console.WriteLine($"Rewriter: rewritten methods: {rewritten}"); - } diff --git a/DesignContracts/Rewriter/TypeHandler.cs b/DesignContracts/Rewriter/TypeRewriter.cs similarity index 90% rename from DesignContracts/Rewriter/TypeHandler.cs rename to DesignContracts/Rewriter/TypeRewriter.cs index a2023b4..e3d0ec4 100644 --- a/DesignContracts/Rewriter/TypeHandler.cs +++ b/DesignContracts/Rewriter/TypeRewriter.cs @@ -3,9 +3,9 @@ namespace Odin.DesignContracts.Rewriter; /// -/// Handles type-specific matters with respect to design contract rewriting. +/// Handles Design-by-Contract rewriting of a given type. /// -internal class TypeHandler +internal class TypeRewriter { private readonly TypeDefinition _target; private MethodDefinition? _invariant; @@ -15,7 +15,7 @@ internal class TypeHandler /// Constructor /// /// - public TypeHandler(TypeDefinition target) + public TypeRewriter(TypeDefinition target) { if (target == null!) throw new ArgumentNullException(nameof(target)); _target = target; @@ -59,9 +59,9 @@ public int Rewrite() return rewritten; } - internal IReadOnlyList GetMembersToTryRewrite() + internal IReadOnlyList GetMembersToTryRewrite() { - return _target.Methods.Select(c => new MethodHandler(c,this)).ToList(); + return _target.Methods.Select(c => new MethodRewriter(c,this)).ToList(); } internal void FindInvariantMethodOrThrow() diff --git a/DesignContracts/Rewriter/WeaveDesignContracts.cs b/DesignContracts/Rewriter/WeaveDesignContracts.cs new file mode 100644 index 0000000..c870ac3 --- /dev/null +++ b/DesignContracts/Rewriter/WeaveDesignContracts.cs @@ -0,0 +1,36 @@ +using Microsoft.Build.Framework; + +namespace Odin.DesignContracts.Rewriter; + +/// +/// MSBuild task for running the Design-by-Contract rewriter. +/// +public class WeaveDesignContracts : Microsoft.Build.Utilities.Task +{ + /// + /// Runs the Design-by-Contract rewriter on the assembly to rewrite. + /// + /// + public override bool Execute() + { + try + { + AssemblyRewriter contractsRewriter = new AssemblyRewriter(AssemblyToRewritePath); + contractsRewriter.Rewrite(); + return true; + } + catch (Exception err) + { + Log.LogError($"{Names.OdinDesignContractsNamespace}: Unexpected error while rewriting assembly {AssemblyToRewritePath}."); + Log.LogErrorFromException(err,true,true,AssemblyToRewritePath); + return false; + } + + } + + /// + /// Path to the assembly to be rewritten. + /// + [Required] + public string AssemblyToRewritePath { get; set; } +} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/MethodHandlerTests.cs b/DesignContracts/RewriterTests/MethodRewriterTests.cs similarity index 69% rename from DesignContracts/RewriterTests/MethodHandlerTests.cs rename to DesignContracts/RewriterTests/MethodRewriterTests.cs index d6457b3..23dac90 100644 --- a/DesignContracts/RewriterTests/MethodHandlerTests.cs +++ b/DesignContracts/RewriterTests/MethodRewriterTests.cs @@ -6,7 +6,7 @@ namespace Tests.Odin.DesignContracts.Rewriter; [TestFixture] -public sealed class MethodHandlerTests +public sealed class MethodRewriterTests { [Test] [TestCase(typeof(OdinInvariantTestTarget),"get_" + nameof(OdinInvariantTestTarget.PureProperty), true)] @@ -16,18 +16,18 @@ public sealed class MethodHandlerTests public void Pure_methods_are_recognised(Type type, string methodName, bool isPure) { CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); - MethodHandler? sut = GetMethodHandlerFor(context, type, methodName); + MethodRewriter? sut = GetMethodHandlerFor(context, type, methodName); Assert.That(sut, Is.Not.Null); Assert.That(sut!.IsPure, Is.EqualTo(isPure)); } - private MethodHandler? GetMethodHandlerFor(CecilAssemblyContext context, Type type, string methodName) + private MethodRewriter? GetMethodHandlerFor(CecilAssemblyContext context, Type type, string methodName) { TypeDefinition? typeDef = context.FindType(type.FullName!); - TypeHandler handler = new TypeHandler(typeDef!); - MethodDefinition? def = handler.Type.Methods.FirstOrDefault(n => n.Name == methodName); - return new MethodHandler(def!, handler); + TypeRewriter rewriter = new TypeRewriter(typeDef!); + MethodDefinition? def = rewriter.Type.Methods.FirstOrDefault(n => n.Name == methodName); + return new MethodRewriter(def!, rewriter); } } diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index 27b3ab2..3fd6bf1 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -177,7 +177,8 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute // Act + Assert string outputPath = Path.Combine(temp.Path, "out.dll"); - InvalidOperationException? expectedError = Assert.Throws(() => Program.RewriteAssembly(inputPath, outputPath)); + InvalidOperationException? expectedError = Assert.Throws(() => + new AssemblyRewriter(inputPath, outputPath).Rewrite()); Assert.That(expectedError, Is.Not.Null); } diff --git a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs index 8b0e84b..c30bd4f 100644 --- a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs +++ b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs @@ -1,6 +1,5 @@ using System.Reflection; using System.Runtime.Loader; -using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; namespace Tests.Odin.DesignContracts.Rewriter; @@ -25,12 +24,12 @@ public RewrittenAssemblyContext(Assembly sourceAssembly) CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".pdb"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".pdb")))); CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); + + var rewriter = new AssemblyRewriter(inputPath); + rewriter.Rewrite(); - string outputPath = Path.Combine(_tempDir, "rewritten.dll"); - Program.RewriteAssembly(inputPath, outputPath); - - _alc = new TestAssemblyLoadContext(outputPath); - RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); + _alc = new TestAssemblyLoadContext(rewriter.OutputPath); + RewrittenAssembly = _alc.LoadFromAssemblyPath(rewriter.OutputPath); } public Type GetTypeOrThrow(string fullName) diff --git a/DesignContracts/RewriterTests/TypeHandlerTests.cs b/DesignContracts/RewriterTests/TypeRewriterTests.cs similarity index 88% rename from DesignContracts/RewriterTests/TypeHandlerTests.cs rename to DesignContracts/RewriterTests/TypeRewriterTests.cs index b5134ee..550e82b 100644 --- a/DesignContracts/RewriterTests/TypeHandlerTests.cs +++ b/DesignContracts/RewriterTests/TypeRewriterTests.cs @@ -6,7 +6,7 @@ namespace Tests.Odin.DesignContracts.Rewriter; [TestFixture] -public sealed class TypeHandlerTests +public sealed class TypeRewriterTests { [Test] [TestCase(typeof(OdinInvariantTestTarget), true)] @@ -16,7 +16,7 @@ public void Invariant_method_is_found(Type type, bool invariantExpected) { CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); TypeDefinition? typeDef = context.FindType(type.FullName!); - TypeHandler sut = new TypeHandler(typeDef!); + TypeRewriter sut = new TypeRewriter(typeDef!); Assert.That(sut.HasInvariant, Is.EqualTo(invariantExpected)); if (invariantExpected) Assert.That(sut.InvariantMethod, Is.Not.Null); diff --git a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj index 5731145..471128e 100644 --- a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj +++ b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj @@ -1,57 +1,61 @@ - - net8.0;net9.0;net10.0 - true - enable - - true - Odin.DesignContracts.Tooling - Design-by-Contract tooling for Odin.DesignContracts: Analyzers + IL rewriter integration. - icon.png - 1591;1573; - true - - - - - - - - + + net8.0;net9.0;net10.0 + true + enable + + true + Odin.DesignContracts.Tooling + Design-by-Contract tooling for Odin.DesignContracts: Analyzers + IL rewriter integration. + icon.png + 1591;1573; + true + + - <_RewriterFiles Include="../Rewriter/bin/$(Configuration)/$(TargetFramework)/**/*.*" /> + - - - - - - - - - - - - - - + + + + + + + + + + + - <_Tfms ToLower="true" Include="$(TargetFrameworks)" /> - <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.dll"> - tools/%(_Tfms.Identity)/any/ - - <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.runtimeconfig.json"> - tools/%(_Tfms.Identity)/any/ - - <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.deps.json"> - tools/%(_Tfms.Identity)/any/ - + + + - + - + + + + + <_Tfms ToLower="true" Include="$(TargetFrameworks)"/> + <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.dll"> + tools/%(_Tfms.Identity)/any/ + + <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.runtimeconfig.json"> + tools/%(_Tfms.Identity)/any/ + + <_PackageFiles Include="../Rewriter/bin/$(Configuration)/%(_Tfms.Identity)/Odin.DesignContracts.Rewriter.deps.json"> + tools/%(_Tfms.Identity)/any/ + + + + + + + diff --git a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets index f93d61c..b6ea8d5 100644 --- a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets +++ b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets @@ -1,46 +1,31 @@ - - - true - - - true - - - - <_OdinNetVersion>$(TargetFramework.Substring(3)) - <_OdinDcRewriterPathCandidate>$(MSBuildThisFileDirectory)..\tools\net$(_OdinNetVersion)\any\Odin.DesignContracts.Rewriter.dll - <_OdinDcRewriterPath Condition="Exists('$(_OdinDcRewriterPathCandidate)')">$(_OdinDcRewriterPathCandidate) - - <_OdinDcRewriterPath Condition="'$(_OdinDcRewriterPath)'==''">$(MSBuildThisFileDirectory)..\tools\net10.0\any\Odin.DesignContracts.Rewriter.dll - <_OdinDcWeavedAssembly>$(IntermediateOutputPath)$(AssemblyName).odin-design-contracts-weaved$(TargetExt) - <_OdinDcWeavedPdb>$(IntermediateOutputPath)$(AssemblyName).odin-design-contracts-weaved.pdb + + true + + + true - - - - - - - - - - + + - + + + + - - + + + - - - + + diff --git a/Odin.sln b/Odin.sln index 99cb194..9b46bc4 100644 --- a/Odin.sln +++ b/Odin.sln @@ -44,6 +44,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{4F52FD LICENSE.TXT = LICENSE.TXT README.md = README.md GitVersion.yml = GitVersion.yml + .gitignore = .gitignore EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{73BA62BB-2B41-2DC4-C714-51B3D4C2A215}" From b51ef9cb91ed9601c78aafb55e6c9aab3fc535cb Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Fri, 26 Dec 2025 21:09:50 +0200 Subject: [PATCH 24/27] Compile weaving working! --- DesignContracts/Core/ContractAttributes.cs | 13 --- DesignContracts/Rewriter/AssemblyRewriter.cs | 38 ++++++--- DesignContracts/Rewriter/MethodRewriter.cs | 82 ++++++++----------- .../Odin.DesignContracts.Rewriter.csproj | 4 - .../RewriterTests/CecilAssemblyContext.cs | 2 +- .../Odin.DesignContracts.Tooling.csproj | 21 +++-- .../Odin.DesignContracts.Tooling.targets | 13 +-- 7 files changed, 84 insertions(+), 89 deletions(-) diff --git a/DesignContracts/Core/ContractAttributes.cs b/DesignContracts/Core/ContractAttributes.cs index 22c25bd..e8cfe3f 100644 --- a/DesignContracts/Core/ContractAttributes.cs +++ b/DesignContracts/Core/ContractAttributes.cs @@ -14,19 +14,6 @@ public class ClassInvariantMethodAttribute : Attribute { } - // /// - // /// Indicates that the decorated method is intended to be used only by - // /// generated code for contract injection purposes. - // /// - // /// - // /// This attribute is provided primarily as a hint to analyzers and code - // /// generation tooling. It has no effect at runtime. - // /// - // [AttributeUsage(AttributeTargets.Method, Inherited = false)] - // public sealed class ContractGeneratedMethodAttribute : Attribute - // { - // } - /// /// Methods and classes marked with this attribute can be used within calls to Contract methods. Such methods do not make any state changes. /// diff --git a/DesignContracts/Rewriter/AssemblyRewriter.cs b/DesignContracts/Rewriter/AssemblyRewriter.cs index 28ada14..f8cd9d2 100644 --- a/DesignContracts/Rewriter/AssemblyRewriter.cs +++ b/DesignContracts/Rewriter/AssemblyRewriter.cs @@ -20,6 +20,7 @@ public AssemblyRewriter(string targetAssemblyPath, { TargetAssemblyPath = targetAssemblyPath; OutputPath = outputPath; + if (!File.Exists(TargetAssemblyPath)) { throw new FileNotFoundException($"Assembly not found: {TargetAssemblyPath}"); @@ -32,9 +33,9 @@ public AssemblyRewriter(string targetAssemblyPath, /// as TargetPath is overwritten after a call to Rewrite() ///
public string TargetAssemblyPath { get; } - - internal bool DoesTargetAssemblyHaveDebugSymbols() - => File.Exists(Path.ChangeExtension(TargetAssemblyPath, ".pdb")); + + internal string TargetAssemblyPdbPath + => Path.ChangeExtension(TargetAssemblyPath, ".pdb"); /// /// Optional path that the rewritten assembly should be written to, else TargetPath is overwritten. @@ -54,13 +55,29 @@ public void Rewrite() DefaultAssemblyResolver resolver = new(); resolver.AddSearchDirectory(assemblyDir); - // Portable PDBs are optional. If present, Cecil will pick them up with ReadSymbols = true. - ReaderParameters readerParameters = new() + // Debug symbols in a .pdb file may or may not be present. + // We'll read them via a stream if present to ensure no issues when + ReaderParameters readerParameters; + bool symbolsPresent = File.Exists(TargetAssemblyPdbPath); + if (symbolsPresent) + { + MemoryStream symbolStream = new MemoryStream(File.ReadAllBytes(TargetAssemblyPdbPath)); + symbolStream.Seek(0, SeekOrigin.Begin); + readerParameters = new() + { + AssemblyResolver = resolver, + ReadSymbols = true, + SymbolStream = symbolStream + }; + } + else { - AssemblyResolver = resolver, - ReadSymbols = DoesTargetAssemblyHaveDebugSymbols(), - ReadingMode = ReadingMode.Immediate - }; + readerParameters = new() + { + AssemblyResolver = resolver, + ReadSymbols = false + }; + } // Read the bytes into memory first to fully decouple the reader from the file on disk. using MemoryStream ms = new MemoryStream(File.ReadAllBytes(TargetAssemblyPath)); @@ -76,9 +93,8 @@ public void Rewrite() } WriterParameters writerParameters = new() { - WriteSymbols = readerParameters.ReadSymbols + WriteSymbols = symbolsPresent }; assembly.Write(GetOutputPath(), writerParameters); - Console.WriteLine($"Rewriter: {rewritten} methods rewritten."); } } \ No newline at end of file diff --git a/DesignContracts/Rewriter/MethodRewriter.cs b/DesignContracts/Rewriter/MethodRewriter.cs index ca6873e..ea314de 100644 --- a/DesignContracts/Rewriter/MethodRewriter.cs +++ b/DesignContracts/Rewriter/MethodRewriter.cs @@ -1,7 +1,6 @@ using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; -using Odin.System; namespace Odin.DesignContracts.Rewriter; @@ -18,9 +17,9 @@ public MethodRewriter(MethodDefinition method, TypeRewriter parentRewriter) Method = method; _parentRewriter = parentRewriter; } - + public MethodDefinition Method { get; } - + /// /// Rewrites the type member returning true if any contract rewrites /// were needed, and false if none were required. @@ -31,18 +30,18 @@ public bool Rewrite() // Only handle sync (v1). We rely on analyzers to enforce this, but be defensive. // if (method.IsAsync) // return false; - + // What about properties? Surely these can have no body? if (!Method.HasBody) return false; Method.Body.SimplifyMacros(); - + InvariantWeavingRequirement invariantsToDo = IsInvariantToBeWeaved(); - ResultValue> postconditionsFound = - TryFindPostconditionCalls(); - - if (!postconditionsFound.IsSuccess && + List postconditionsFound = + TryFindPostconditionInstructions(); + + if (!postconditionsFound.Any() && !invariantsToDo.OnEntry && !invariantsToDo.OnExit) { // Nothing to do. @@ -65,20 +64,17 @@ public bool Rewrite() // Remove the postconditions from the method when postconditions are present. // We could skip this as Contract.Ensures are all simply no-ops...? - if (postconditionsFound.IsSuccess) + foreach (Instruction instruction in postconditionsFound) { - foreach (Instruction instruction in postconditionsFound.Value) - { - // If the instruction was already removed as part of a previous remove, skip. - if (Method.Body.Instructions.Contains(instruction)) - il.Remove(instruction); - } + // If the instruction was already removed as part of a previous remove, skip. + if (Method.Body.Instructions.Contains(instruction)) + il.Remove(instruction); } // If we need to inject postconditions and/or invariant calls at exit, do it per-return. // Todo: If there are multiple returns, create a shadow method to execute the // invariant and\or postconditions, calling it from each return. - if (postconditionsFound.IsSuccess || invariantsToDo.OnExit) + if (postconditionsFound.Any() || invariantsToDo.OnExit) { VariableDefinition? resultVar = null; bool isVoid = IsVoidReturnType(); @@ -98,19 +94,18 @@ public bool Rewrite() il.InsertBefore(returnInst, Instruction.Create(OpCodes.Stloc, resultVar!)); } - if (postconditionsFound.IsSuccess) + + foreach (Instruction inst in postconditionsFound) { - foreach (Instruction inst in postconditionsFound.Value) + Instruction cloned = inst.CloneInstruction(); + + if (!isVoid && IsResultCall(cloned)) { - Instruction cloned = inst.CloneInstruction(); - - if (!isVoid && IsResultCall(cloned)) - { - // Replace call Contract.Result() with ldloc resultVar. - cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); - } - il.InsertBefore(returnInst, cloned); + // Replace call Contract.Result() with ldloc resultVar. + cloned = Instruction.Create(OpCodes.Ldloc, resultVar!); } + + il.InsertBefore(returnInst, cloned); } if (invariantsToDo.OnExit) @@ -155,12 +150,12 @@ public InvariantWeavingRequirement IsInvariantToBeWeaved() OnExit = weaveInvariantOnExit }; } - + /// - /// Returns success if 1 or more postcondition Ensures() calls were found. + /// Returns any postcondition Ensures() instruction calls found. /// /// - public ResultValue> TryFindPostconditionCalls() + public List TryFindPostconditionInstructions() { // For V1 we will attempt to simply extract any Postcondition.Ensures() // calls from the method body if they exist. I am a total noob at IL so have no clue @@ -171,7 +166,7 @@ public ResultValue> TryFindPostconditionCalls() IList instructions = Method.Body.Instructions; if (instructions.Count == 0) - return ResultValue>.Failure("Method has no instructions."); + return postconditionEnsuresCalls; int endIndex = -1; for (int i = 0; i < instructions.Count; i++) @@ -183,20 +178,15 @@ public ResultValue> TryFindPostconditionCalls() } } - if (postconditionEnsuresCalls.Count ==0) - { - return ResultValue>.Failure("No Ensures calls."); - } - - return ResultValue>.Success(postconditionEnsuresCalls);; + return postconditionEnsuresCalls; } - + public static bool IsPostconditionEnsuresCall(Instruction inst) => IsStaticCallToPostconditionMethod(inst, "Ensures"); public static bool IsResultCall(Instruction inst) => IsStaticCallToPostconditionMethod(inst, "Result"); - + public static bool IsStaticCallToPostconditionMethod(Instruction inst, string methodName) { if (inst.OpCode != OpCodes.Call) @@ -212,7 +202,7 @@ public static bool IsStaticCallToPostconditionMethod(Instruction inst, string me string declaringType = mr.DeclaringType.FullName; return declaringType == Names.OdinPostconditionEnsuresTypeFullName; } - + /// /// True if Method is a public instance method. /// @@ -221,7 +211,7 @@ public bool IsPublicInstanceMethod() { return Method is { IsPublic: true, IsStatic: false }; } - + /// /// True if Method is marked as [Pure] or if it is a property accessor of a [Pure] property. /// @@ -234,11 +224,11 @@ public bool IsPure() // For accessors, also honour [Pure] on the property itself. if (Method.IsGetter || Method.IsSetter) { - PropertyDefinition? prop = _parentRewriter.Type.Properties. - FirstOrDefault(p => p.GetMethod == Method || p.SetMethod == Method); + PropertyDefinition? prop = _parentRewriter.Type.Properties.FirstOrDefault(p => p.GetMethod == Method || p.SetMethod == Method); if (prop is not null && prop.HasAnyAttributeIn(Names.PureAttributeFullNames)) return true; } + return false; } @@ -248,21 +238,21 @@ public bool IsPure() /// public bool IsVoidReturnType() { - return Method.ReturnType.MetadataType == MetadataType.Void; + return Method.ReturnType.MetadataType == MetadataType.Void; } public bool IsInstanceConstructor() { return Method.IsConstructor && !Method.IsStatic; } - + private void InsertInvariantCallBefore(ILProcessor il, Instruction before, MethodDefinition invariantMethod) { // instance.Invariant(); il.InsertBefore(before, Instruction.Create(OpCodes.Ldarg_0)); il.InsertBefore(before, Instruction.Create(OpCodes.Call, invariantMethod)); } - + internal record InvariantWeavingRequirement { public required bool OnEntry { get; init; } diff --git a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj index 6e1cda6..9a7a514 100644 --- a/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/Rewriter/Odin.DesignContracts.Rewriter.csproj @@ -12,9 +12,5 @@ - - - - diff --git a/DesignContracts/RewriterTests/CecilAssemblyContext.cs b/DesignContracts/RewriterTests/CecilAssemblyContext.cs index 4a98138..1195d4c 100644 --- a/DesignContracts/RewriterTests/CecilAssemblyContext.cs +++ b/DesignContracts/RewriterTests/CecilAssemblyContext.cs @@ -25,7 +25,7 @@ public CecilAssemblyContext(Assembly sourceAssembly) ReaderParameters readerParameters = new() { AssemblyResolver = resolver, - ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")) + ReadSymbols = File.Exists(Path.ChangeExtension(assemblyPath, ".pdb")), }; Assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); diff --git a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj index 471128e..781e3e0 100644 --- a/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj +++ b/DesignContracts/Tooling/Odin.DesignContracts.Tooling.csproj @@ -18,15 +18,18 @@ - - - - - - + + $(MSBuildProjectDirectory)/../Rewriter/bin/$(Configuration)/$(TargetFramework)/ + $(MSBuildProjectDirectory)/tools/$(TargetFramework)/any/ + + + + + diff --git a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets index b6ea8d5..809602b 100644 --- a/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets +++ b/DesignContracts/Tooling/buildTransitive/Odin.DesignContracts.Tooling.targets @@ -16,16 +16,19 @@ AfterTargets="CoreCompile" BeforeTargets="CopyFilesToOutputDirectory" Inputs="@(IntermediateAssembly);@(Compile);$(MSBuildProjectFullPath);$(Configuration);$(TargetFramework)" - Outputs="$(IntermediateOutputPath)WeaveDesignContracts.stamp" + Outputs="$(IntermediateOutputPath)OdinDesignContracts.stamp" Condition="'$(OdinDesignContractsWeaveEnabled)'=='true' and '$(TargetPath)'!=''"> + + + $(MSBuildProjectDirectory)/$(IntermediateOutputPath)$(TargetFileName) + - - + - + - + From 386ee45f53656154edfa1cf703846dbfec8d11dd Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Fri, 26 Dec 2025 22:11:58 +0200 Subject: [PATCH 25/27] Saving... --- DesignContracts/CoreTests/AttributeFlavour.cs | 7 --- .../CoreTests/ContractInvariantTests.cs | 33 ----------- .../ContractInvariantWeavingTests.cs | 58 +++++++++++++++++++ DesignContracts/CoreTests/TestSupport.cs | 52 +++++++++++++++++ .../RewriterTests/AttributeFlavour.cs | 7 --- .../RewriterTests/RewriterTests.cs | 38 +++--------- ...Tests.Odin.DesignContracts.Rewriter.csproj | 6 ++ .../BclInvariantTestTarget.cs | 2 +- .../OdinInvariantTestTarget.cs | 2 +- 9 files changed, 126 insertions(+), 79 deletions(-) delete mode 100644 DesignContracts/CoreTests/AttributeFlavour.cs delete mode 100644 DesignContracts/CoreTests/ContractInvariantTests.cs create mode 100644 DesignContracts/CoreTests/ContractInvariantWeavingTests.cs create mode 100644 DesignContracts/CoreTests/TestSupport.cs delete mode 100644 DesignContracts/RewriterTests/AttributeFlavour.cs diff --git a/DesignContracts/CoreTests/AttributeFlavour.cs b/DesignContracts/CoreTests/AttributeFlavour.cs deleted file mode 100644 index 29638a7..0000000 --- a/DesignContracts/CoreTests/AttributeFlavour.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Tests.Odin.DesignContracts; - -public enum AttributeFlavour -{ - Odin, - BaseClassLibrary -} \ No newline at end of file diff --git a/DesignContracts/CoreTests/ContractInvariantTests.cs b/DesignContracts/CoreTests/ContractInvariantTests.cs deleted file mode 100644 index 057c7af..0000000 --- a/DesignContracts/CoreTests/ContractInvariantTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NUnit.Framework; -using Odin.DesignContracts; -using Targets; - -namespace Tests.Odin.DesignContracts -{ - [TestFixture][Ignore("Get tooling working first...")] - public sealed class ContractInvariantTests - { - [Test] - public void Public_constructor_has_invariant_weaved_on_exit([Values] AttributeFlavour testCase) - { - Assert.That(ContractOptions.Current.Invariants, - Is.EqualTo(ContractHandlingBehaviour.EventHandlersAndEscalation)); - Assert.That(ContractOptions.Current.Postconditions, - Is.EqualTo(ContractHandlingBehaviour.EventHandlersAndEscalation)); - - ContractException? ex = Assert.Throws(() => - { - if (testCase == AttributeFlavour.Odin) - { - new OdinInvariantTestTarget(-1); - } - else if (testCase == AttributeFlavour.BaseClassLibrary) - { - new BclInvariantTestTarget(-1); - } - }); - Assert.That(ex, Is.Not.Null); - Assert.That(ex!.Kind, Is.EqualTo(ContractKind.Invariant)); - } - } -} \ No newline at end of file diff --git a/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs b/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs new file mode 100644 index 0000000..787a983 --- /dev/null +++ b/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; +using Odin.DesignContracts; +using Targets; + +namespace Tests.Odin.DesignContracts +{ + [TestFixture] + public sealed class ContractInvariantWeavingTests + { + [SetUp] + public void Setup() + { + ContractOptions.Initialize(ContractOptions.All(ContractHandlingBehaviour.EscalationOnly)); + } + + [Test] + public void Public_constructor_has_invariant_woven_on_exit([Values] AttributeFlavour testCase) + { + ContractException? ex = Assert.Throws(() => + { + if (testCase == AttributeFlavour.Odin) + { + new OdinInvariantTestTarget(-1); + } + else if (testCase == AttributeFlavour.BaseClassLibrary) + { + new BclInvariantTestTarget(-1); + } + }); + AssertTestInvariantExceptionThrown(ex); + } + + [Test] + public void Public_method_has_invariant_call_woven_on_entry([Values] AttributeFlavour testCase) + { + Type testTypeFor = TestSupport.GetTargetTestTypeFor(testCase); + + object instance = Activator.CreateInstance(testTypeFor, 1)!; + TestSupport.SetPrivateField(testTypeFor, instance, "_value", -1); + + Exception? ex = Assert.Catch(() => + { + TestSupport.Invoke(testTypeFor, instance, nameof(OdinInvariantTestTarget.Increment)); + })!; + AssertTestInvariantExceptionThrown(ex); + } + + private void AssertTestInvariantExceptionThrown(Exception? ex) + { + Assert.That(ex, Is.Not.Null); + ContractException? contractException = ex as ContractException; + Assert.That(contractException, Is.Not.Null); + Assert.That(contractException.Kind, Is.EqualTo(ContractKind.Invariant)); + Assert.That(contractException.UserMessage, Is.EqualTo("value must be non-negative")); + Assert.That(contractException.ConditionText, Is.Null); + } + } +} \ No newline at end of file diff --git a/DesignContracts/CoreTests/TestSupport.cs b/DesignContracts/CoreTests/TestSupport.cs new file mode 100644 index 0000000..3a11b03 --- /dev/null +++ b/DesignContracts/CoreTests/TestSupport.cs @@ -0,0 +1,52 @@ +using System.Reflection; +using Targets; + +namespace Tests.Odin.DesignContracts; + +public enum AttributeFlavour +{ + Odin, + BaseClassLibrary +} + +public static class TestSupport +{ + public static Type GetTargetTestTypeFor(AttributeFlavour testCase) + { + if (testCase == AttributeFlavour.Odin) + { + return typeof(OdinInvariantTestTarget); + } + + if (testCase == AttributeFlavour.BaseClassLibrary) + { + return typeof(BclInvariantTestTarget); + } + + throw new NotSupportedException(testCase.ToString()); + } + + public static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) + { + FieldInfo f = declaringType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException($"Missing field '{fieldName}'."); + f.SetValue(instance, value); + } + + public static object? Invoke(Type declaringType, object instance, string methodName) + { + MethodInfo m = declaringType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public) + ?? throw new InvalidOperationException($"Missing method '{methodName}'."); + try + { + return m.Invoke(instance, null); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + throw tie.InnerException; + } + } + +} + + diff --git a/DesignContracts/RewriterTests/AttributeFlavour.cs b/DesignContracts/RewriterTests/AttributeFlavour.cs deleted file mode 100644 index 778b7ab..0000000 --- a/DesignContracts/RewriterTests/AttributeFlavour.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Tests.Odin.DesignContracts.Rewriter; - -public enum AttributeFlavour -{ - Odin, - BaseClassLibrary -} \ No newline at end of file diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index 3fd6bf1..bb5e41f 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -58,9 +58,9 @@ public void Public_method_runs_invariant_on_entry([Values] AttributeFlavour test Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); + TestSupport.SetPrivateField(t, instance, "_value", -1); - Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTestTarget.Increment)); })!; + Exception? ex = Assert.Catch(() => { TestSupport.Invoke(t, instance, nameof(OdinInvariantTestTarget.Increment)); })!; Assert.That(ex, Is.Not.Null); Assert.That(ex.Message, Contains.Substring("Invariant broken:")); @@ -76,7 +76,7 @@ public void Public_method_runs_invariant_on_exit([Values] AttributeFlavour testC object instance = Activator.CreateInstance(t, 1)!; - Exception? ex = Assert.Catch(() => { Invoke(t, instance, nameof(OdinInvariantTestTarget.MakeInvalid)); })!; + Exception? ex = Assert.Catch(() => { TestSupport.Invoke(t, instance, nameof(OdinInvariantTestTarget.MakeInvalid)); })!; Assert.That(ex, Is.Not.Null); Assert.That(ex.Message, Contains.Substring("Invariant broken:")); @@ -90,10 +90,10 @@ public void Pure_method_is_excluded_from_invariant_weaving([Values] AttributeFla Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); + TestSupport.SetPrivateField(t, instance, "_value", -1); // If [Pure] is honoured, this returns the invalid value without invariant checks throwing. - object? result = Invoke(t, instance, "PureGetValue"); + object? result = TestSupport.Invoke(t, instance, "PureGetValue"); Assert.That(result, Is.EqualTo(-1)); } @@ -106,9 +106,9 @@ public void Pure_property_is_excluded_from_invariant_weaving([Values] AttributeF Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); + TestSupport.SetPrivateField(t, instance, "_value", -1); - object? value = Invoke(t, instance, "get_PureProperty"); + object? value = TestSupport.Invoke(t, instance, "get_PureProperty"); Assert.That(value, Is.EqualTo(-1)); } @@ -121,7 +121,7 @@ public void Non_pure_property_is_woven_and_checks_invariants([Values] AttributeF Type t = context.GetTypeOrThrow(targetType.FullName!); object instance = Activator.CreateInstance(t, 1)!; - SetPrivateField(t, instance, "_value", -1); + TestSupport.SetPrivateField(t, instance, "_value", -1); PropertyInfo p = t.GetProperty("NonPureProperty")!; @@ -281,13 +281,6 @@ public void Single_postcondition_is_woven([Values("VoidSingleConditionSingleRetu Assert.DoesNotThrow(() => CallMethod(targetWrittenType, ensuresTestTarget, methodName, [testNumber])); } } - - private static void SetPrivateField(Type declaringType, object instance, string fieldName, object? value) - { - FieldInfo f = declaringType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) - ?? throw new InvalidOperationException($"Missing field '{fieldName}'."); - f.SetValue(instance, value); - } private static void CallMethod(Type declaringType, object instance, string methodName, object[] parameters) { @@ -295,21 +288,6 @@ private static void CallMethod(Type declaringType, object instance, string metho ?? throw new InvalidOperationException($"Missing method '{methodName}'."); method.Invoke(instance, parameters); } - - private static object? Invoke(Type declaringType, object instance, string methodName) - { - MethodInfo m = declaringType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public) - ?? throw new InvalidOperationException($"Missing method '{methodName}'."); - - try - { - return m.Invoke(instance, null); - } - catch (TargetInvocationException tie) when (tie.InnerException is not null) - { - throw tie.InnerException; - } - } private Type GetTargetTestTypeFor(AttributeFlavour testCase) { diff --git a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj index 4dcd52e..806819b 100644 --- a/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj +++ b/DesignContracts/RewriterTests/Tests.Odin.DesignContracts.Rewriter.csproj @@ -24,4 +24,10 @@ + + + TestSupport.cs + + + diff --git a/DesignContracts/TargetsUnrewritten/BclInvariantTestTarget.cs b/DesignContracts/TargetsUnrewritten/BclInvariantTestTarget.cs index 2a82bc2..6267146 100644 --- a/DesignContracts/TargetsUnrewritten/BclInvariantTestTarget.cs +++ b/DesignContracts/TargetsUnrewritten/BclInvariantTestTarget.cs @@ -19,7 +19,7 @@ public BclInvariantTestTarget(int value) [System.Diagnostics.Contracts.ContractInvariantMethod] public void ObjectInvariant() { - Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); + Contract.Invariant(_value >= 0, "value must be non-negative"); } public void Increment() diff --git a/DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs b/DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs index 6a83f8c..81921d3 100644 --- a/DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs +++ b/DesignContracts/TargetsUnrewritten/OdinInvariantTestTarget.cs @@ -18,7 +18,7 @@ public OdinInvariantTestTarget(int value) [ClassInvariantMethod] public void ObjectInvariant() { - Contract.Invariant(_value >= 0, "_value must be non-negative", "_value >= 0"); + Contract.Invariant(_value >= 0, "value must be non-negative"); } public void Increment() From 9c603e9a0d122bf6a788f71a2e228543dc965b99 Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Fri, 26 Dec 2025 22:20:28 +0200 Subject: [PATCH 26/27] Saving --- DesignContracts/CoreTests/ContractInvariantWeavingTests.cs | 1 + DesignContracts/RewriterTests/RewriterTests.cs | 7 +------ DesignContracts/RewriterTests/RewrittenAssemblyContext.cs | 7 ++++--- DesignContracts/RewriterTests/TypeRewriterTests.cs | 3 ++- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs b/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs index 787a983..0c791d6 100644 --- a/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs +++ b/DesignContracts/CoreTests/ContractInvariantWeavingTests.cs @@ -53,6 +53,7 @@ private void AssertTestInvariantExceptionThrown(Exception? ex) Assert.That(contractException.Kind, Is.EqualTo(ContractKind.Invariant)); Assert.That(contractException.UserMessage, Is.EqualTo("value must be non-negative")); Assert.That(contractException.ConditionText, Is.Null); + Assert.That(contractException.Message, Is.EqualTo("Invariant broken: value must be non-negative")); } } } \ No newline at end of file diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index bb5e41f..4b8ec1f 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -41,12 +41,7 @@ public void Public_constructor_runs_invariant_on_exit([Values] AttributeFlavour } }); Assert.That(exception, Is.Not.Null); - Assert.That(exception!.Message, Is.EqualTo("Invariant broken: _value must be non-negative [Condition: _value >= 0]")); - // Because the ContractException thrown is from the dynamically loaded RewrittenAssemblyContext - // it does not seem castable to ContractException.... - // Assert.That(exception, Is.InstanceOf()); - // ContractException ex = (ContractException)exception!; - // Assert.That(ex!.Kind, Is.EqualTo(ContractFailureKind.Invariant)); + Assert.That(exception!.Message, Is.EqualTo("Invariant broken: value must be non-negative")); } [Test] diff --git a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs index c30bd4f..8ffc833 100644 --- a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs +++ b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs @@ -25,11 +25,12 @@ public RewrittenAssemblyContext(Assembly sourceAssembly) CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".pdb"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".pdb")))); CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); - var rewriter = new AssemblyRewriter(inputPath); + string outputPath = Path.Combine(_tempDir, "rewritten.dll"); + var rewriter = new AssemblyRewriter(inputPath,outputPath); rewriter.Rewrite(); - _alc = new TestAssemblyLoadContext(rewriter.OutputPath); - RewrittenAssembly = _alc.LoadFromAssemblyPath(rewriter.OutputPath); + _alc = new TestAssemblyLoadContext(outputPath); + RewrittenAssembly = _alc.LoadFromAssemblyPath(outputPath); } public Type GetTypeOrThrow(string fullName) diff --git a/DesignContracts/RewriterTests/TypeRewriterTests.cs b/DesignContracts/RewriterTests/TypeRewriterTests.cs index 550e82b..99a54b3 100644 --- a/DesignContracts/RewriterTests/TypeRewriterTests.cs +++ b/DesignContracts/RewriterTests/TypeRewriterTests.cs @@ -19,7 +19,8 @@ public void Invariant_method_is_found(Type type, bool invariantExpected) TypeRewriter sut = new TypeRewriter(typeDef!); Assert.That(sut.HasInvariant, Is.EqualTo(invariantExpected)); - if (invariantExpected) Assert.That(sut.InvariantMethod, Is.Not.Null); + if (invariantExpected) + Assert.That(sut.InvariantMethod, Is.Not.Null); } } From baa98ece0a3e11c39efc81b19bc1e63b03e27f4a Mon Sep 17 00:00:00 2001 From: Mark Derman Date: Sat, 27 Dec 2025 10:37:59 +0200 Subject: [PATCH 27/27] Added build weave logging, but not working as expected yet. --- DesignContracts/Rewriter/AssemblyRewriter.cs | 34 +++++--- .../Rewriter/ConsoleLoggingAdaptor.cs | 36 ++++++++ DesignContracts/Rewriter/ILoggingAdaptor.cs | 82 +++++++++++++++++++ DesignContracts/Rewriter/MethodRewriter.cs | 67 ++++++++++----- .../Rewriter/MsBuildLoggingAdaptor.cs | 54 ++++++++++++ DesignContracts/Rewriter/Names.cs | 1 + DesignContracts/Rewriter/Program.cs | 15 ++-- DesignContracts/Rewriter/TypeRewriter.cs | 20 +++-- .../Rewriter/WeaveDesignContracts.cs | 5 +- .../RewriterTests/MethodRewriterTests.cs | 3 +- .../RewriterTests/RewriterTests.cs | 3 +- .../RewriterTests/RewrittenAssemblyContext.cs | 2 +- .../RewriterTests/TypeRewriterTests.cs | 3 +- .../TargetsRewritten/TargetsRewritten.csproj | 2 +- 14 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 DesignContracts/Rewriter/ConsoleLoggingAdaptor.cs create mode 100644 DesignContracts/Rewriter/ILoggingAdaptor.cs create mode 100644 DesignContracts/Rewriter/MsBuildLoggingAdaptor.cs diff --git a/DesignContracts/Rewriter/AssemblyRewriter.cs b/DesignContracts/Rewriter/AssemblyRewriter.cs index f8cd9d2..060b4ee 100644 --- a/DesignContracts/Rewriter/AssemblyRewriter.cs +++ b/DesignContracts/Rewriter/AssemblyRewriter.cs @@ -1,3 +1,4 @@ +using Microsoft.Build.Framework; using Mono.Cecil; namespace Odin.DesignContracts.Rewriter; @@ -5,51 +6,58 @@ namespace Odin.DesignContracts.Rewriter; /// /// Handles Design-by-Contract rewriting of a given assembly. /// -public class AssemblyRewriter +internal class AssemblyRewriter { /// /// Constructor /// /// The input assembly path, /// typically in the 'intermediate build output' under the 'obj' folder. - /// If omitted, defaults to 'InputAssembly.odin-design-contracts-weaved.dll' + /// + /// If omitted, defaults to 'InputAssembly.odin-design-contracts-weaved.dll' /// in the same folder as the input assembly. /// Thrown if the assembly to rewrite does not exist. - public AssemblyRewriter(string targetAssemblyPath, - string? outputPath = null) + internal AssemblyRewriter(string targetAssemblyPath, ILoggingAdaptor logger, + string? alternativeOutputPath = null) { TargetAssemblyPath = targetAssemblyPath; - OutputPath = outputPath; + _logger = logger; + AlternativeOutputPath = alternativeOutputPath; if (!File.Exists(TargetAssemblyPath)) { - throw new FileNotFoundException($"Assembly not found: {TargetAssemblyPath}"); + var msg = $"Assembly not found: {TargetAssemblyPath}"; + _logger.LogMessage(LogImportance.High,msg); + throw new FileNotFoundException(msg); } } + + private readonly ILoggingAdaptor _logger; /// /// The assembly to rewrite. Note that if OutputPath is specified, /// the rewritten assembly is saved to OutputPath, else the assembly located /// as TargetPath is overwritten after a call to Rewrite() /// - public string TargetAssemblyPath { get; } + internal string TargetAssemblyPath { get; } internal string TargetAssemblyPdbPath => Path.ChangeExtension(TargetAssemblyPath, ".pdb"); /// - /// Optional path that the rewritten assembly should be written to, else TargetPath is overwritten. + /// Optional alternative path that the rewritten assembly should be written to, + /// else TargetAssemblyPath is overwritten by default. /// - public string? OutputPath { get; } + internal string? AlternativeOutputPath { get; } - internal string GetOutputPath() - => OutputPath ?? TargetAssemblyPath; + private string GetOutputPath() + => AlternativeOutputPath ?? TargetAssemblyPath; /// /// Writes invariants and postconditions into the assembly at TargetPath, /// or OutputPath if specified. /// - public void Rewrite() + internal void Rewrite() { string assemblyDir = Path.GetDirectoryName(Path.GetFullPath(TargetAssemblyPath))!; DefaultAssemblyResolver resolver = new(); @@ -87,7 +95,7 @@ public void Rewrite() { foreach (TypeDefinition type in module.GetTypes()) { - TypeRewriter currentType = new(type); + TypeRewriter currentType = new(type, _logger); rewritten += currentType.Rewrite(); } } diff --git a/DesignContracts/Rewriter/ConsoleLoggingAdaptor.cs b/DesignContracts/Rewriter/ConsoleLoggingAdaptor.cs new file mode 100644 index 0000000..28d6e9b --- /dev/null +++ b/DesignContracts/Rewriter/ConsoleLoggingAdaptor.cs @@ -0,0 +1,36 @@ +using Microsoft.Build.Framework; + +namespace Odin.DesignContracts.Rewriter; + +/// +public class ConsoleLoggingAdaptor : ILoggingAdaptor +{ + /// + public void LogMessage(LogImportance importance, string? message, params object[] messageArgs) + { + Console.WriteLine(message, messageArgs); + if (importance == LogImportance.High) + { + Console.Error.WriteLine(message, messageArgs); + } + else if (importance == LogImportance.Normal) + { + Console.WriteLine(message, messageArgs); + } + // Don't write Low diagnostic messages to the console + } + + /// + public void LogMessage(string? subcategory, string? code, string? helpKeyword, string? file, int lineNumber, int columnNumber, + int endLineNumber, int endColumnNumber, LogImportance importance, string? message, params object[] messageArgs) + { + LogMessage(importance, message, messageArgs); + } + + /// + public void LogErrorFromException(Exception exception, bool showStackTrace, bool showDetail, string file) + { + Console.WriteLine(exception.Message); + + } +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/ILoggingAdaptor.cs b/DesignContracts/Rewriter/ILoggingAdaptor.cs new file mode 100644 index 0000000..a5dd1ff --- /dev/null +++ b/DesignContracts/Rewriter/ILoggingAdaptor.cs @@ -0,0 +1,82 @@ +namespace Odin.DesignContracts.Rewriter; + +/// +/// This is to avoid needing to ship Microsoft.Build.Framework assemblies +/// with the tooling if we want to use MSBuild LogImportance directly. +/// +public enum LogImportance +{ + /// + /// High importance messages + /// + High, + /// + /// Normal importance messages + /// + Normal, + /// + /// Low importance messages + /// + Low +} + +/// +/// Created so that AssemblyRewriter can log to console or MSBuild output. +/// +public interface ILoggingAdaptor +{ + /// + /// Logs a message of the given importance using the specified string. + /// Thread safe. + /// + /// + /// Take care to order the parameters correctly or the other overload will be called inadvertently. + /// + /// Log verbosity level of the message. + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogMessage(LogImportance importance, string? message, params object[] messageArgs); + + + /// + /// Logs a message using the specified string and other message details. + /// Thread safe. + /// + /// Description of the warning type (can be null). + /// Message code (can be null) + /// The help keyword for the host IDE (can be null). + /// The path to the file causing the message (can be null). + /// The line in the file causing the message (set to zero if not available). + /// The column in the file causing the message (set to zero if not available). + /// The last line of a range of lines in the file causing the message (set to zero if not available). + /// The last column of a range of columns in the file causing the message (set to zero if not available). + /// Importance of the message. + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogMessage( + string? subcategory, + string? code, + string? helpKeyword, + string? file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + LogImportance importance, + string? message, + params object[] messageArgs); + + /// + /// Logs an error using the message, and optionally the stack-trace from the given exception, and + /// optionally inner exceptions too. + /// Thread safe. + /// + /// Exception to log. + /// If true, callstack will be appended to message. + /// Whether to log exception types and any inner exceptions. + /// File related to the exception, or null if the project file should be logged + /// Thrown when exception is null. + public void LogErrorFromException(Exception exception, bool showStackTrace, bool showDetail, string file); +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/MethodRewriter.cs b/DesignContracts/Rewriter/MethodRewriter.cs index ea314de..99de2a8 100644 --- a/DesignContracts/Rewriter/MethodRewriter.cs +++ b/DesignContracts/Rewriter/MethodRewriter.cs @@ -1,3 +1,4 @@ +using Microsoft.Build.Framework; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; @@ -36,13 +37,9 @@ public bool Rewrite() Method.Body.SimplifyMacros(); - InvariantWeavingRequirement invariantsToDo = IsInvariantToBeWeaved(); + MethodWeavingResult weaving = new MethodWeavingResult(IsInvariantToBeWeaved(), TryFindPostconditionInstructions()); - List postconditionsFound = - TryFindPostconditionInstructions(); - - if (!postconditionsFound.Any() && - !invariantsToDo.OnEntry && !invariantsToDo.OnExit) + if (weaving.NothingToWeave) { // Nothing to do. Method.Body.OptimizeMacros(); @@ -52,19 +49,19 @@ public bool Rewrite() ILProcessor il = Method.Body.GetILProcessor(); // Inject invariant call at entry (before any user code). - if (invariantsToDo.OnEntry) + if (weaving.Invariant.OnEntry) { - // An instructionless member body is possible don't understand how or why + // An instructionless member body is possible... I suppose the member could have been stubbed. Instruction first = Method.Body.Instructions.FirstOrDefault() ?? Instruction.Create(OpCodes.Nop); if (Method.Body.Instructions.Count == 0) il.Append(first); - + InsertInvariantCallBefore(il, first, _parentRewriter.InvariantMethod!); } // Remove the postconditions from the method when postconditions are present. // We could skip this as Contract.Ensures are all simply no-ops...? - foreach (Instruction instruction in postconditionsFound) + foreach (Instruction instruction in weaving.PostconditionsToWeave) { // If the instruction was already removed as part of a previous remove, skip. if (Method.Body.Instructions.Contains(instruction)) @@ -74,7 +71,7 @@ public bool Rewrite() // If we need to inject postconditions and/or invariant calls at exit, do it per-return. // Todo: If there are multiple returns, create a shadow method to execute the // invariant and\or postconditions, calling it from each return. - if (postconditionsFound.Any() || invariantsToDo.OnExit) + if (weaving.OnExitWeavingRequired) { VariableDefinition? resultVar = null; bool isVoid = IsVoidReturnType(); @@ -95,7 +92,7 @@ public bool Rewrite() } - foreach (Instruction inst in postconditionsFound) + foreach (Instruction inst in weaving.PostconditionsToWeave) { Instruction cloned = inst.CloneInstruction(); @@ -108,7 +105,7 @@ public bool Rewrite() il.InsertBefore(returnInst, cloned); } - if (invariantsToDo.OnExit) + if (weaving.Invariant.OnExit) { InsertInvariantCallBefore(il, returnInst, _parentRewriter.InvariantMethod!); } @@ -119,12 +116,29 @@ public bool Rewrite() } } } - Method.Body.OptimizeMacros(); + + string invariantResult; + if (weaving.Invariant.BothEntryAndExit) + { + invariantResult = " invariant calls (entry & exit)"; + } + else if (weaving.Invariant.NeitherEntryNorExit) + { + invariantResult = ""; + } + else + { + invariantResult = weaving.Invariant.OnEntry ? " invariant call (on entry)" : " invariant call (on exit)"; + } + string postconditionResult = weaving.PostconditionsToWeave.Any() ? " postcondition calls" : ""; + if (invariantResult!="" && postconditionResult!="") postconditionResult=" and " + postconditionResult; + string message = $"Weaved{invariantResult}{postconditionResult} into {Method.Name} of type {_parentRewriter.Type.FullName}."; + _parentRewriter.Logger.LogMessage(LogImportance.Low, message); return true; } - public InvariantWeavingRequirement IsInvariantToBeWeaved() + public InvariantWeavingResult IsInvariantToBeWeaved() { bool canWeaveInvariant = _parentRewriter.HasInvariant && IsPublicInstanceMethod(); bool isInvariantMethodItself = _parentRewriter.InvariantMethod is not null && Method == _parentRewriter.InvariantMethod; @@ -144,7 +158,7 @@ public InvariantWeavingRequirement IsInvariantToBeWeaved() weaveInvariantOnExit = false; } - return new InvariantWeavingRequirement() + return new InvariantWeavingResult() { OnEntry = weaveInvariantOnEntry, OnExit = weaveInvariantOnExit @@ -167,8 +181,7 @@ public List TryFindPostconditionInstructions() IList instructions = Method.Body.Instructions; if (instructions.Count == 0) return postconditionEnsuresCalls; - - int endIndex = -1; + for (int i = 0; i < instructions.Count; i++) { Instruction inst = instructions[i]; @@ -253,9 +266,25 @@ private void InsertInvariantCallBefore(ILProcessor il, Instruction before, Metho il.InsertBefore(before, Instruction.Create(OpCodes.Call, invariantMethod)); } - internal record InvariantWeavingRequirement + internal record InvariantWeavingResult { public required bool OnEntry { get; init; } public required bool OnExit { get; init; } + public bool BothEntryAndExit => OnEntry && OnExit; + public bool NeitherEntryNorExit => !OnEntry && !OnExit; + } + + internal record MethodWeavingResult + { + public MethodWeavingResult(InvariantWeavingResult invariant, List postconditionsFound) + { + Invariant = invariant; + PostconditionsToWeave = postconditionsFound; + } + public InvariantWeavingResult Invariant { get; } + public List PostconditionsToWeave { get; } + public bool NothingToWeave => !PostconditionsToWeave.Any() && !Invariant.OnEntry && !Invariant.OnExit; + public bool OnExitWeavingRequired => PostconditionsToWeave.Any() || Invariant.OnExit; } + } \ No newline at end of file diff --git a/DesignContracts/Rewriter/MsBuildLoggingAdaptor.cs b/DesignContracts/Rewriter/MsBuildLoggingAdaptor.cs new file mode 100644 index 0000000..4beee65 --- /dev/null +++ b/DesignContracts/Rewriter/MsBuildLoggingAdaptor.cs @@ -0,0 +1,54 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Odin.DesignContracts.Rewriter; + +/// +public class MsBuildLoggingAdaptor : ILoggingAdaptor +{ + private readonly TaskLoggingHelper _msBuildLogger; + + /// + /// Constructor + /// + /// + public MsBuildLoggingAdaptor(TaskLoggingHelper msBuildLogger) + { + _msBuildLogger = msBuildLogger; + } + + /// + public void LogMessage(LogImportance importance, string? message, params object[] messageArgs) + { + _msBuildLogger.LogMessage(MapImportance(importance), message, messageArgs); + } + + /// + public void LogMessage(string? subcategory, string? code, string? helpKeyword, string? file, int lineNumber, int columnNumber, + int endLineNumber, int endColumnNumber, LogImportance importance, string? message, params object[] messageArgs) + { + _msBuildLogger.LogMessage(subcategory, code, helpKeyword, file, lineNumber, columnNumber, endLineNumber, + endColumnNumber, MapImportance(importance), message, messageArgs); + } + + private MessageImportance MapImportance(LogImportance importance) + { + switch (importance) + { + case LogImportance.High: + return MessageImportance.High; + case LogImportance.Normal: + return MessageImportance.Normal; + case LogImportance.Low: + return MessageImportance.Low; + default: + throw new ArgumentOutOfRangeException(nameof(importance), importance, null); + } + } + + /// + public void LogErrorFromException(Exception exception, bool showStackTrace, bool showDetail, string file) + { + _msBuildLogger.LogErrorFromException(exception, showStackTrace, showDetail, file); + } +} \ No newline at end of file diff --git a/DesignContracts/Rewriter/Names.cs b/DesignContracts/Rewriter/Names.cs index 0f42d64..57e6618 100644 --- a/DesignContracts/Rewriter/Names.cs +++ b/DesignContracts/Rewriter/Names.cs @@ -8,6 +8,7 @@ internal static class Names internal const string BclContractNamespace = "System.Diagnostics.Contracts"; internal const string OdinDesignContractsNamespace = "Odin.DesignContracts"; internal const string OdinPostconditionEnsuresTypeFullName = OdinDesignContractsNamespace + ".Contract"; + internal const string OdinDesignContractsRewriter = OdinDesignContractsNamespace + ".Rewriter"; internal const string OdinInvariantAttributeFullName = OdinDesignContractsNamespace + ".ClassInvariantMethodAttribute"; internal const string BclInvariantAttributeFullName = BclContractNamespace + ".ContractInvariantMethodAttribute"; internal const string OdinPureAttributeFullName = OdinDesignContractsNamespace + ".PureAttribute"; diff --git a/DesignContracts/Rewriter/Program.cs b/DesignContracts/Rewriter/Program.cs index 13e93ed..e92a313 100644 --- a/DesignContracts/Rewriter/Program.cs +++ b/DesignContracts/Rewriter/Program.cs @@ -1,3 +1,5 @@ +using Microsoft.Build.Framework; + namespace Odin.DesignContracts.Rewriter; /// @@ -7,13 +9,11 @@ namespace Odin.DesignContracts.Rewriter; /// internal static class Program { - private const string Rewriter = "Odin.DesignContracts.Rewriter"; - private static int Main(string[] args) { if (args.Length < 1) { - Console.Error.WriteLine($"{Rewriter}: Usage 'dotnet {Rewriter}.dll '"); + Console.Error.WriteLine($"{Names.OdinDesignContractsRewriter}: Usage 'dotnet {Names.OdinDesignContractsRewriter}.dll '"); return 3; } @@ -26,20 +26,21 @@ private static int Main(string[] args) if (!File.Exists(assemblyPath)) { - Console.Error.WriteLine($"{Rewriter}: Input assembly not found: {assemblyPath}"); + Console.Error.WriteLine($"{Names.OdinDesignContractsRewriter}: Input assembly not found: {assemblyPath}"); return 2; } + var logger = new ConsoleLoggingAdaptor(); try { - AssemblyRewriter contractsRewriter = new AssemblyRewriter(assemblyPath, outputPath); + AssemblyRewriter contractsRewriter = new AssemblyRewriter(assemblyPath, logger,outputPath); contractsRewriter.Rewrite(); return 0; } catch (Exception ex) { - Console.Error.WriteLine($"{Rewriter}: Unexpected error while rewriting assembly..."); - Console.Error.WriteLine($"{Rewriter}: {ex.Message}"); + logger.LogMessage(LogImportance.High,$"{Names.OdinDesignContractsRewriter}: Unexpected error while rewriting assembly..." ); + logger.LogErrorFromException(ex, true, true,assemblyPath); return 1; } } diff --git a/DesignContracts/Rewriter/TypeRewriter.cs b/DesignContracts/Rewriter/TypeRewriter.cs index e3d0ec4..1921335 100644 --- a/DesignContracts/Rewriter/TypeRewriter.cs +++ b/DesignContracts/Rewriter/TypeRewriter.cs @@ -1,3 +1,4 @@ +using Microsoft.Build.Framework; using Mono.Cecil; namespace Odin.DesignContracts.Rewriter; @@ -10,28 +11,33 @@ internal class TypeRewriter private readonly TypeDefinition _target; private MethodDefinition? _invariant; private bool _invariantSearched = false; - + private ILoggingAdaptor _logger; + /// /// Constructor /// /// - public TypeRewriter(TypeDefinition target) + /// + internal TypeRewriter(TypeDefinition target, ILoggingAdaptor logger) { if (target == null!) throw new ArgumentNullException(nameof(target)); _target = target; + _logger = logger; } /// /// The enclosed Type being handled. /// - public TypeDefinition Type => _target; + internal TypeDefinition Type => _target; + + internal ILoggingAdaptor Logger => _logger; - public bool HasInvariant => InvariantMethod!=null; + internal bool HasInvariant => InvariantMethod!=null; /// /// Null if none was found. /// - public MethodDefinition? InvariantMethod + internal MethodDefinition? InvariantMethod { get { @@ -47,7 +53,7 @@ public MethodDefinition? InvariantMethod /// Rewrites the type returning the number of members rewritten. /// /// - public int Rewrite() + internal int Rewrite() { int rewritten = 0; foreach (var member in GetMembersToTryRewrite()) @@ -66,7 +72,6 @@ internal IReadOnlyList GetMembersToTryRewrite() internal void FindInvariantMethodOrThrow() { - var allAttributes = _target.Methods.SelectMany(m => m.CustomAttributes).ToList(); List candidates = _target.Methods .Where(m => m.HasAnyAttributeIn(Names.InvariantAttributeFullNames)) .ToList(); @@ -97,6 +102,7 @@ internal void FindInvariantMethodOrThrow() if (!invariant.HasBody) throw new InvalidOperationException($"Invariant method must have a body: {invariant.FullName}"); + _logger.LogMessage(LogImportance.Low, $"Found invariant method: {invariant.FullName} to weave for type {_target.FullName}."); _invariant = invariant; } diff --git a/DesignContracts/Rewriter/WeaveDesignContracts.cs b/DesignContracts/Rewriter/WeaveDesignContracts.cs index c870ac3..03ad4db 100644 --- a/DesignContracts/Rewriter/WeaveDesignContracts.cs +++ b/DesignContracts/Rewriter/WeaveDesignContracts.cs @@ -13,15 +13,16 @@ public class WeaveDesignContracts : Microsoft.Build.Utilities.Task /// public override bool Execute() { + var logger = new MsBuildLoggingAdaptor(Log); try { - AssemblyRewriter contractsRewriter = new AssemblyRewriter(AssemblyToRewritePath); + AssemblyRewriter contractsRewriter = new AssemblyRewriter(AssemblyToRewritePath, logger); contractsRewriter.Rewrite(); return true; } catch (Exception err) { - Log.LogError($"{Names.OdinDesignContractsNamespace}: Unexpected error while rewriting assembly {AssemblyToRewritePath}."); + Log.LogMessage(MessageImportance.High,$"{Names.OdinDesignContractsNamespace}: Unhandled error while rewriting assembly {AssemblyToRewritePath}."); Log.LogErrorFromException(err,true,true,AssemblyToRewritePath); return false; } diff --git a/DesignContracts/RewriterTests/MethodRewriterTests.cs b/DesignContracts/RewriterTests/MethodRewriterTests.cs index 23dac90..03f3d6d 100644 --- a/DesignContracts/RewriterTests/MethodRewriterTests.cs +++ b/DesignContracts/RewriterTests/MethodRewriterTests.cs @@ -1,4 +1,5 @@ using Mono.Cecil; +using Moq; using NUnit.Framework; using Odin.DesignContracts.Rewriter; using Targets; @@ -25,7 +26,7 @@ public void Pure_methods_are_recognised(Type type, string methodName, bool isPur private MethodRewriter? GetMethodHandlerFor(CecilAssemblyContext context, Type type, string methodName) { TypeDefinition? typeDef = context.FindType(type.FullName!); - TypeRewriter rewriter = new TypeRewriter(typeDef!); + TypeRewriter rewriter = new TypeRewriter(typeDef!, new Mock().Object); MethodDefinition? def = rewriter.Type.Methods.FirstOrDefault(n => n.Name == methodName); return new MethodRewriter(def!, rewriter); } diff --git a/DesignContracts/RewriterTests/RewriterTests.cs b/DesignContracts/RewriterTests/RewriterTests.cs index 4b8ec1f..e8684d6 100644 --- a/DesignContracts/RewriterTests/RewriterTests.cs +++ b/DesignContracts/RewriterTests/RewriterTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics.Contracts; using System.Reflection; using Mono.Cecil; +using Moq; using NUnit.Framework; using Odin.DesignContracts; using Odin.DesignContracts.Rewriter; @@ -173,7 +174,7 @@ public void Multiple_invariant_methods_causes_rewrite_to_fail([Values] Attribute // Act + Assert string outputPath = Path.Combine(temp.Path, "out.dll"); InvalidOperationException? expectedError = Assert.Throws(() => - new AssemblyRewriter(inputPath, outputPath).Rewrite()); + new AssemblyRewriter(inputPath, new Mock().Object, outputPath).Rewrite()); Assert.That(expectedError, Is.Not.Null); } diff --git a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs index 8ffc833..ebb26f6 100644 --- a/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs +++ b/DesignContracts/RewriterTests/RewrittenAssemblyContext.cs @@ -26,7 +26,7 @@ public RewrittenAssemblyContext(Assembly sourceAssembly) CopyIfExists(Path.ChangeExtension(sourceAssembly.Location, ".deps.json"), Path.Combine(_tempDir, Path.GetFileName(Path.ChangeExtension(sourceAssembly.Location, ".deps.json")))); string outputPath = Path.Combine(_tempDir, "rewritten.dll"); - var rewriter = new AssemblyRewriter(inputPath,outputPath); + var rewriter = new AssemblyRewriter(inputPath, new ConsoleLoggingAdaptor(), outputPath); rewriter.Rewrite(); _alc = new TestAssemblyLoadContext(outputPath); diff --git a/DesignContracts/RewriterTests/TypeRewriterTests.cs b/DesignContracts/RewriterTests/TypeRewriterTests.cs index 99a54b3..9cffee2 100644 --- a/DesignContracts/RewriterTests/TypeRewriterTests.cs +++ b/DesignContracts/RewriterTests/TypeRewriterTests.cs @@ -1,4 +1,5 @@ using Mono.Cecil; +using Moq; using NUnit.Framework; using Odin.DesignContracts.Rewriter; using Targets; @@ -16,7 +17,7 @@ public void Invariant_method_is_found(Type type, bool invariantExpected) { CecilAssemblyContext context = CecilAssemblyContext.GetTargetsUntooledAssemblyContext(); TypeDefinition? typeDef = context.FindType(type.FullName!); - TypeRewriter sut = new TypeRewriter(typeDef!); + TypeRewriter sut = new TypeRewriter(typeDef!, new Mock().Object); Assert.That(sut.HasInvariant, Is.EqualTo(invariantExpected)); if (invariantExpected) diff --git a/DesignContracts/TargetsRewritten/TargetsRewritten.csproj b/DesignContracts/TargetsRewritten/TargetsRewritten.csproj index 166e5c2..a0f5a14 100644 --- a/DesignContracts/TargetsRewritten/TargetsRewritten.csproj +++ b/DesignContracts/TargetsRewritten/TargetsRewritten.csproj @@ -21,7 +21,7 @@ - +