diff --git a/docs/design/method-query-source.md b/docs/design/method-query-source.md
index 1f200c46d4..eadda00a56 100644
--- a/docs/design/method-query-source.md
+++ b/docs/design/method-query-source.md
@@ -64,6 +64,11 @@ MethodDefs. The same source serves
`System.Text.Json.JsonDocument.Parse`, whose seven-body scope provides a
smaller real-package case.
+The pinned repository SDK's CoreLib is also the real-asset gate for semantic
+MethodDef identity: one exact `StringBuilder.AppendFormat` MethodDef must
+produce the same structural identity as the current `MethodIdentity`
+projection while examining one MethodDef and acquiring no body.
+
The current legacy body service spends most of a scoped request on
whole-assembly identity construction, generated-body discovery,
state-machine mapping, and method-map construction. One body can therefore
@@ -271,8 +276,9 @@ Depth is expressed as source capabilities rather than one broad `Body` bit:
| Layer | Source-owned acquisition | Does not define |
| --- | --- | --- |
-| Declaration | MethodDef, declaring TypeDef, attributes, flags, name comparison, and bounded signature-shape access | Display identity or body content |
-| Identity | Detached inert method identity and anchor fields | Consumer ordering or presentation |
+| Declaration | MethodDef, declaring TypeDef, attributes, flags, name comparison, and bounded signature-shape access | Semantic or display identity, or body content |
+| Semantic identity | Detached structural identity for an admitted MethodDef | Caller-unsafe policy, display spelling, or body content |
+| Identity text | Detached inert method identity and anchor fields | Semantic identity, consumer ordering, or presentation |
| Body | Managed body header, IL bytes, exception regions, and local-signature handle | Instruction meaning |
| Instructions | One bounded instruction decode | Analysis of those instructions |
| Control flow | The owner-issued CFG over the decoded instructions | Analysis-specific graph interpretation |
@@ -281,8 +287,50 @@ Depth is expressed as source capabilities rather than one broad `Body` bit:
Each layer includes only its prerequisites. For example, `Calls` requires
`Instructions` and the declared shared lookup support, but it does not require
-`Control flow` unless the request asks for both. `Identity` is projection work
-and remains absent from Count or Exists plans that do not need it.
+`Control flow` unless the request asks for both. Semantic identity and Identity
+text are independent projection layers. Either remains absent from Count or
+Exists plans that do not need it.
+
+### Semantic MethodDef identity
+
+`SemanticIdentity` is a distinct source-gate layer. For one admitted MethodDef
+it returns either a detached `MethodSemanticIdentity` or a typed unavailable
+result. The detached value contains:
+
+- assembly name and module MVID;
+- declaring `TypeRef`, method name, MethodDef token, and static and extension
+ facts;
+- parameter and return `TypeRef` values;
+- method generic arity and parameter names;
+- raw signature header and vararg required-parameter count; and
+- invalid-generic-declaration and virtual-dispatch-open facts.
+
+`MethodSemanticIdentity.ToMethodIdentity` requires the separately produced
+`CallerUnsafeMode`. Semantic identity neither computes nor defaults that
+contract. A producer publishing the current `MethodIdentity` therefore
+declares both prerequisites, and both remain visible in its plan and receipts.
+
+This focused layer is legal only with direct exact-MethodDef breadth and no
+expansion. All-definition, exact-TypeDef, predicate, and expanded plans are
+rejected rather than becoming whole-assembly identity materialization.
+
+The gate validates an exact MethodDef row number against the admitted MethodDef
+table before recording or reading a row. Invalid exact coordinates fail source
+admission with zero definitions examined. A valid bodiless MethodDef is an
+ordinary semantic-identity unit and never causes body acquisition.
+
+Signature bytes, type-decoder work, generic-parameter rows, string bytes, and
+attribute/relationship associations have independent execution-scoped bounds.
+The source receipt and `WorkReceipt` publish every counter and the first bound
+reached. Bound exhaustion aborts the execution through the source gate.
+Unsafe-to-decode shapes return typed unsupported identity; malformed metadata
+returns typed malformed identity. Both are ordinary producer facts and are
+counted in the semantic-identity receipt.
+
+Identity text remains the existing independently declared, independently
+budgeted projection to inert `MethodRowIdentity`. Semantic identity does not
+arm or charge that budget and never uses display text or an anchor as a join
+key.
Instruction demand has two independent source-owned facets:
@@ -340,7 +388,8 @@ The current `MethodDefinitionLayers` maps into this vocabulary during
migration:
- its declaration facets remain Declaration demand;
-- `IdentityText` becomes Identity;
+- `SemanticIdentity` becomes Semantic identity;
+- `IdentityText` becomes Identity text;
- `Body` becomes Body or Instructions according to the producer's actual read;
and
- `ModuleLookup` becomes declared shared lookup support.
diff --git a/src/ILInspector.Analysis/MemberIdentity.cs b/src/ILInspector.Analysis/MemberIdentity.cs
index 2bd1aefc75..226ff03526 100644
--- a/src/ILInspector.Analysis/MemberIdentity.cs
+++ b/src/ILInspector.Analysis/MemberIdentity.cs
@@ -163,6 +163,156 @@ public override int GetHashCode()
}
}
+///
+/// Detached structural identity for one MethodDef. Caller-unsafe policy is a
+/// separately owned fact and is supplied only when constructing the current
+/// .
+///
+public sealed record MethodSemanticIdentity(
+ string AssemblyName,
+ Guid ModuleVersionId,
+ TypeRef DeclaringType,
+ string Name,
+ ImmutableArray ParameterTypes,
+ TypeRef ReturnType,
+ int MetadataToken,
+ bool IsStatic,
+ bool IsExtension,
+ int GenericArity,
+ ImmutableArray GenericParameterNames,
+ byte SignatureHeader,
+ int RequiredParameterCount,
+ bool HasInvalidGenericParameterDeclaration,
+ bool IsVirtualDispatchOpen)
+{
+ ImmutableArray _parameterTypes =
+ ImmutableArrayValueEquality.RequireInitialized(
+ ParameterTypes,
+ nameof(ParameterTypes));
+ ImmutableArray _genericParameterNames =
+ ImmutableArrayValueEquality.EmptyIfDefault(
+ GenericParameterNames,
+ nameof(GenericParameterNames));
+
+ public ImmutableArray ParameterTypes
+ {
+ get => _parameterTypes;
+ init => _parameterTypes =
+ ImmutableArrayValueEquality.RequireInitialized(
+ value,
+ nameof(ParameterTypes));
+ }
+
+ public ImmutableArray GenericParameterNames
+ {
+ get => _genericParameterNames;
+ init => _genericParameterNames =
+ ImmutableArrayValueEquality.EmptyIfDefault(
+ value,
+ nameof(GenericParameterNames));
+ }
+
+ /// Composes this structural identity with its separately owned caller-safety fact.
+ public MethodIdentity ToMethodIdentity(CallerUnsafeMode callerUnsafeMode) =>
+ new(
+ AssemblyName,
+ ModuleVersionId,
+ DeclaringType,
+ Name,
+ ParameterTypes,
+ ReturnType,
+ MetadataToken,
+ IsStatic,
+ IsExtension,
+ callerUnsafeMode,
+ GenericArity,
+ GenericParameterNames)
+ {
+ SignatureHeader = SignatureHeader,
+ RequiredParameterCount = RequiredParameterCount,
+ HasInvalidGenericParameterDeclaration =
+ HasInvalidGenericParameterDeclaration,
+ IsVirtualDispatchOpen = IsVirtualDispatchOpen,
+ };
+
+ public bool Equals(MethodSemanticIdentity? other) =>
+ other is not null
+ && AssemblyName == other.AssemblyName
+ && ModuleVersionId == other.ModuleVersionId
+ && Equals(DeclaringType, other.DeclaringType)
+ && Name == other.Name
+ && ImmutableArrayValueEquality.SequenceEqual(
+ ParameterTypes,
+ other.ParameterTypes)
+ && Equals(ReturnType, other.ReturnType)
+ && MetadataToken == other.MetadataToken
+ && IsStatic == other.IsStatic
+ && IsExtension == other.IsExtension
+ && GenericArity == other.GenericArity
+ && ImmutableArrayValueEquality.SequenceEqual(
+ GenericParameterNames,
+ other.GenericParameterNames)
+ && SignatureHeader == other.SignatureHeader
+ && RequiredParameterCount
+ == other.RequiredParameterCount
+ && HasInvalidGenericParameterDeclaration
+ == other.HasInvalidGenericParameterDeclaration
+ && IsVirtualDispatchOpen
+ == other.IsVirtualDispatchOpen;
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(AssemblyName);
+ hash.Add(ModuleVersionId);
+ hash.Add(DeclaringType);
+ hash.Add(Name);
+ ImmutableArrayValueEquality.AddToHash(
+ ref hash,
+ ParameterTypes);
+ hash.Add(ReturnType);
+ hash.Add(MetadataToken);
+ hash.Add(IsStatic);
+ hash.Add(IsExtension);
+ hash.Add(GenericArity);
+ ImmutableArrayValueEquality.AddToHash(
+ ref hash,
+ GenericParameterNames);
+ hash.Add(SignatureHeader);
+ hash.Add(RequiredParameterCount);
+ hash.Add(HasInvalidGenericParameterDeclaration);
+ hash.Add(IsVirtualDispatchOpen);
+ return hash.ToHashCode();
+ }
+}
+
+/// Why semantic MethodDef identity could not be decoded.
+public enum MethodSemanticIdentityUnavailableKind
+{
+ /// A guarded metadata shape is outside the supported decoding contract.
+ Unsupported,
+
+ /// The admitted row references malformed metadata.
+ Malformed,
+}
+
+/// Typed semantic-identity result for one admitted MethodDef.
+public abstract record MethodSemanticIdentityResult
+{
+ private MethodSemanticIdentityResult()
+ {
+ }
+
+ /// The semantic identity was decoded.
+ public sealed record Available(MethodSemanticIdentity Identity)
+ : MethodSemanticIdentityResult;
+
+ /// The row was admitted but its semantic identity was unavailable.
+ public sealed record Unavailable(
+ MethodSemanticIdentityUnavailableKind Kind)
+ : MethodSemanticIdentityResult;
+}
+
public sealed record MemberRef(
TypeRef DeclaringType,
string Name,
diff --git a/src/ILInspector.Analysis/Planning/MethodDefinitionExecution.cs b/src/ILInspector.Analysis/Planning/MethodDefinitionExecution.cs
index 96be4e11d9..2be7815262 100644
--- a/src/ILInspector.Analysis/Planning/MethodDefinitionExecution.cs
+++ b/src/ILInspector.Analysis/Planning/MethodDefinitionExecution.cs
@@ -153,9 +153,12 @@ static MethodDefinitionExecution Execute(
}
execution._gate = new MethodRowGate(
+ sourceName,
peReader,
reader,
- (FieldsRead(description) & MethodDefinitionLayers.IdentityText) != 0);
+ (FieldsRead(description) & MethodDefinitionLayers.IdentityText) != 0,
+ (FieldsRead(description)
+ & MethodDefinitionLayers.SemanticIdentity) != 0);
bool lookupDeclared = false;
bool referenceBindingDeclared = false;
@@ -332,6 +335,18 @@ static MethodDefinitionExecution CreateExecution(
bool tracksSourceCoverage,
MethodBodyAnalyzerPlan? instructionPlan)
{
+ if ((FieldsRead(description)
+ & MethodDefinitionLayers.SemanticIdentity) != 0
+ && (breadth.Kind
+ != MethodDefinitionSourceBreadthKind.ExactMethods
+ || breadth.Expansion
+ != MethodDefinitionSourceExpansion.None))
+ {
+ throw new ProducerContractException(
+ "Semantic MethodDef identity requires direct exact-method "
+ + "breadth with no expansion.");
+ }
+
var execution = new MethodDefinitionExecution(
description,
breadth,
@@ -446,10 +461,13 @@ internal static MethodDefinitionSharedExecution ExecuteShared(
MethodDefinitionExecution lane = lanes[laneIndex];
WorkDescription description = descriptions[laneIndex];
lane._gate = new MethodRowGate(
+ sourceName,
peReader,
reader,
(FieldsRead(description)
- & MethodDefinitionLayers.IdentityText) != 0);
+ & MethodDefinitionLayers.IdentityText) != 0,
+ (FieldsRead(description)
+ & MethodDefinitionLayers.SemanticIdentity) != 0);
bool lookupDeclared = false;
foreach (ProducerState state in lane._states)
@@ -1117,6 +1135,18 @@ void VisitExactMethods(
if (!AnyActive(visiting))
return;
+ int methodRow = MetadataTokens.GetRowNumber(methodHandle);
+ if (methodRow > reader.GetTableRowCount(TableIndex.MethodDef))
+ {
+ FailActiveSource(
+ MetadataTokens.GetToken(methodHandle),
+ "(method source)",
+ new BadImageFormatException(
+ "The exact MethodDef handle is outside the admitted "
+ + "MethodDef table."));
+ continue;
+ }
+
MethodDefinition methodDefinition;
try
{
@@ -1711,7 +1741,12 @@ void CompleteWithoutUnits()
void PublishSourceCoverage()
{
- SourceCoverage = _sourceCoverage.Build();
+ SourceCoverage = _sourceCoverage.Build() with
+ {
+ SemanticIdentityWork =
+ _gate?.SemanticIdentityWorkReceipt
+ ?? MethodSemanticIdentityWorkReceipt.Empty,
+ };
}
/// How many times the execution's gate looked up a classifier's cache.
@@ -1724,7 +1759,7 @@ WorkReceipt CreateReceipt(int unitsVisited, MethodRowGate? gate)
_states.Length);
foreach (ProducerState state in _states)
{
- var layers = ImmutableArray.CreateBuilder(2);
+ var layers = ImmutableArray.CreateBuilder(3);
if (state.HasBodyLayer)
{
layers.Add(new ProducerLayerParticipation(
@@ -1741,6 +1776,13 @@ WorkReceipt CreateReceipt(int unitsVisited, MethodRowGate? gate)
state.LookupUses));
}
+ if (state.HasSemanticIdentityLayer)
+ {
+ layers.Add(new ProducerLayerParticipation(
+ nameof(MethodDefinitionLayers.SemanticIdentity),
+ state.SemanticIdentityAcquisitions));
+ }
+
if (state.HasMemorySafetyLayer)
{
layers.Add(new ProducerLayerParticipation(
@@ -1765,6 +1807,9 @@ WorkReceipt CreateReceipt(int unitsVisited, MethodRowGate? gate)
IdentityBudgetArmed = gate?.IdentityBudgetArmed ?? false,
IdentityWorkCharged = gate?.IdentityWorkCharged ?? 0,
SignatureShapeNodesWalked = gate?.SignatureShapeNodesWalked ?? 0,
+ SemanticIdentityWork =
+ gate?.SemanticIdentityWorkReceipt
+ ?? MethodSemanticIdentityWorkReceipt.Empty,
};
}
@@ -1930,6 +1975,9 @@ public virtual bool CompleteInstructionShapes(
public bool HasReferenceBindingLayer { get; } =
(layers & MethodDefinitionLayers.ReferenceBinding) != 0;
+ public bool HasSemanticIdentityLayer { get; } =
+ (layers & MethodDefinitionLayers.SemanticIdentity) != 0;
+
public bool HasMemorySafetyLayer { get; } =
(layers & MethodDefinitionLayers.MemorySafetyContracts) != 0;
@@ -1955,10 +2003,15 @@ public virtual bool CompleteInstructionShapes(
/// Units in which the module lookup was used.
public int LookupUses;
+ /// Units in which semantic MethodDef identity was requested.
+ public int SemanticIdentityAcquisitions;
+
public int LastBodyUnit;
public int LastLookupUnit;
+ public int LastSemanticIdentityUnit;
+
/// Units in which memory-safety contract support was queried.
public int MemorySafetyUses;
diff --git a/src/ILInspector.Analysis/Planning/MethodDefinitionProducers.cs b/src/ILInspector.Analysis/Planning/MethodDefinitionProducers.cs
index 9a1a932c55..a2420334b6 100644
--- a/src/ILInspector.Analysis/Planning/MethodDefinitionProducers.cs
+++ b/src/ILInspector.Analysis/Planning/MethodDefinitionProducers.cs
@@ -76,6 +76,13 @@ public enum MethodDefinitionLayers
/// metadata work. It does not imply .
///
MemorySafetyContracts = 1024,
+
+ ///
+ /// Tier 2: bounded detached structural identity for one admitted MethodDef.
+ /// This is independent from and excludes the
+ /// separately owned caller-unsafe contract.
+ ///
+ SemanticIdentity = 2048,
}
///
@@ -521,6 +528,21 @@ public MethodRowIdentity Identity
}
}
+ /// Tier 2: detached structural identity for the admitted MethodDef.
+ public MethodSemanticIdentityResult SemanticIdentity
+ {
+ get
+ {
+ Require(MethodDefinitionLayers.SemanticIdentity);
+ MethodDefinitionExecution.ProducerState producer = Producer;
+ producer.CountUnit(
+ ref producer.LastSemanticIdentityUnit,
+ Token,
+ ref producer.SemanticIdentityAcquisitions);
+ return _unit.Gate.SemanticIdentity();
+ }
+ }
+
/// Tier 2: the row's P/Invoke import module, or null.
public InertString? PInvokeModuleName
{
diff --git a/src/ILInspector.Analysis/Planning/MethodQuerySource.cs b/src/ILInspector.Analysis/Planning/MethodQuerySource.cs
index 6aa5e37b3d..82c4f36a64 100644
--- a/src/ILInspector.Analysis/Planning/MethodQuerySource.cs
+++ b/src/ILInspector.Analysis/Planning/MethodQuerySource.cs
@@ -907,6 +907,9 @@ public MethodDefinitionTerminalWorkCoverage TerminalWork
public MethodDefinitionInstructionWorkCoverage InstructionWork
{ get; init; } = MethodDefinitionInstructionWorkCoverage.Empty;
+ public MethodSemanticIdentityWorkReceipt SemanticIdentityWork
+ { get; init; } = MethodSemanticIdentityWorkReceipt.Empty;
+
public MemorySafetyContractCoverage MemorySafetyContracts
{ get; init; } = MemorySafetyContractCoverage.Empty;
}
diff --git a/src/ILInspector.Analysis/Planning/MethodRowGate.cs b/src/ILInspector.Analysis/Planning/MethodRowGate.cs
index e9343e53ed..6f9dee5f16 100644
--- a/src/ILInspector.Analysis/Planning/MethodRowGate.cs
+++ b/src/ILInspector.Analysis/Planning/MethodRowGate.cs
@@ -194,9 +194,9 @@ internal sealed class ProducerAbortException(CriticalFailure failure)
/// The method-row gate: part of the method-definition source. It is the only
/// path by which a producer reads a row's fields. Tier 1 accessors charge no
/// budget and memoize every structural walk per handle and per blob for the
-/// execution, with a per-row cap that aborts. Tier 2 identity text goes
-/// through one budget per execution, armed only when the plan declares
-/// .
+/// execution, with a per-row cap that aborts. Tier 2 identity text and
+/// semantic identity use independent execution-scoped budgets, armed only
+/// when the plan declares their respective layers.
///
internal sealed class MethodRowGate
{
@@ -205,6 +205,7 @@ internal sealed class MethodRowGate
internal const string TypeSpecificationGuard = "TypeSpecificationGuard";
internal const string IdentityWork = "IdentityWork";
internal const string IdentityDecodeFailures = "IdentityDecodeFailures";
+ internal const string SemanticIdentityWork = "SemanticIdentityWork";
internal const string AttributeTypeChain = "AttributeTypeChain";
readonly SignatureShapeWalker _signatures;
@@ -212,6 +213,7 @@ internal sealed class MethodRowGate
readonly Dictionary<(EntityHandle Constructor, MetadataTypeNameTarget Target), bool> _attributeConstructors = [];
readonly Dictionary<(EntityHandle Type, MetadataTypeNameTarget Target), bool> _attributeTypes = [];
readonly Dictionary<(BlobHandle Signature, MetadataTypeNameTarget Target), bool> _attributeTypeSpecs = [];
+ readonly Dictionary _extensionTypes = [];
// The identity budget: legacy's per-scan budgets, per execution.
int _identityWorkRemaining = MetadataSafetyPolicy.MaxClassificationScanWorkChars;
@@ -231,11 +233,42 @@ internal sealed class MethodRowGate
BudgetedStringDecoder? _identityDecoder;
bool _identityExhausted;
- internal MethodRowGate(PEReader peReader, MetadataReader reader, bool identityBudgetArmed)
+ readonly string _sourceName;
+ readonly TypeRefDecoder? _semanticTypeDecoder;
+ bool _semanticIdentityRead;
+ MethodSemanticIdentityResult? _semanticIdentity;
+ string? _semanticAssemblyName;
+ Guid _semanticModuleVersionId;
+ bool _semanticModuleIdentityRead;
+ int _semanticIdentitiesDecoded;
+ int _semanticUnsupportedResults;
+ int _semanticMalformedResults;
+ long _semanticSignatureBytes;
+ long _semanticTypeWork;
+ long _semanticGenericParameterRows;
+ long _semanticStringBytes;
+ long _semanticAssociationRows;
+ MethodSemanticIdentityWorkLimitKind? _semanticReachedLimit;
+
+ internal MethodRowGate(
+ string sourceName,
+ PEReader peReader,
+ MetadataReader reader,
+ bool identityBudgetArmed,
+ bool semanticIdentityBudgetArmed)
{
+ ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
+ _sourceName = sourceName;
_peReader = peReader;
Reader = reader;
IdentityBudgetArmed = identityBudgetArmed;
+ SemanticIdentityBudgetArmed = semanticIdentityBudgetArmed;
+ _semanticTypeDecoder = semanticIdentityBudgetArmed
+ ? new TypeRefDecoder(
+ amount => ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.TypeWork,
+ amount))
+ : null;
_signatures = new SignatureShapeWalker(this);
}
@@ -243,11 +276,26 @@ internal MethodRowGate(PEReader peReader, MetadataReader reader, bool identityBu
public bool IdentityBudgetArmed { get; }
+ public bool SemanticIdentityBudgetArmed { get; }
+
public long IdentityWorkCharged =>
MetadataSafetyPolicy.MaxClassificationScanWorkChars - (long)_identityWorkRemaining;
public long SignatureShapeNodesWalked => _signatures.TotalNodes;
+ public MethodSemanticIdentityWorkReceipt SemanticIdentityWorkReceipt =>
+ new(
+ SemanticIdentityBudgetArmed,
+ _semanticIdentitiesDecoded,
+ _semanticUnsupportedResults,
+ _semanticMalformedResults,
+ _semanticSignatureBytes,
+ _semanticTypeWork,
+ _semanticGenericParameterRows,
+ _semanticStringBytes,
+ _semanticAssociationRows,
+ _semanticReachedLimit);
+
internal void MoveTo(
TypeDefinitionHandle typeHandle,
TypeDefinition typeDefinition,
@@ -265,6 +313,8 @@ internal void MoveTo(
_rowToken = MetadataTokens.GetToken(methodHandle);
_identityRead = false;
_identity = null;
+ _semanticIdentityRead = false;
+ _semanticIdentity = null;
}
internal static ProducerContractException Undeclared(
@@ -803,6 +853,322 @@ anchor is null
return _identity;
}
+ ///
+ /// Decodes one admitted MethodDef's detached structural identity through
+ /// independent semantic-identity bounds. It never acquires a body or arms
+ /// the identity-text budget.
+ ///
+ internal MethodSemanticIdentityResult SemanticIdentity()
+ {
+ if (_semanticIdentityRead)
+ return _semanticIdentity!;
+
+ try
+ {
+ MethodDefinition method = _methodDefinition;
+ TypeDefinition type = _typeDefinition;
+ BlobReader signatureBlob =
+ Reader.GetBlobReader(method.Signature);
+ ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.SignatureBytes,
+ signatureBlob.Length);
+ if (!SignatureBlobGuard.IsSafeToDecode(
+ Reader,
+ method.Signature,
+ SignatureBlobGuard.Kind.Method))
+ {
+ return CacheSemanticIdentity(
+ new MethodSemanticIdentityResult.Unavailable(
+ MethodSemanticIdentityUnavailableKind.Unsupported));
+ }
+
+ GenericParameterHandleCollection typeParameters =
+ type.GetGenericParameters();
+ GenericParameterHandleCollection methodParameters =
+ method.GetGenericParameters();
+ ImmutableArray typeParameterNames =
+ ReadGenericParameterNames(typeParameters);
+ ImmutableArray methodParameterNames =
+ ReadGenericParameterNames(methodParameters);
+ var scope = new GenericScope(
+ typeParameterNames,
+ methodParameterNames);
+ TypeRefDecoder decoder =
+ _semanticTypeDecoder
+ ?? throw new ProducerContractException(
+ "Semantic identity was read without arming its budget.");
+ MethodSignature signature =
+ method.DecodeSignature(decoder, scope);
+
+ ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.AssociationRows,
+ 1);
+ TypeRef declaringType =
+ decoder.GetTypeFromDefinition(
+ Reader,
+ _typeHandle,
+ 0);
+ if (ContainsUnsupportedType(declaringType)
+ || ContainsUnsupportedType(
+ signature.ReturnType)
+ || signature.ParameterTypes.Any(
+ ContainsUnsupportedType))
+ {
+ return CacheSemanticIdentity(
+ new MethodSemanticIdentityResult.Unavailable(
+ MethodSemanticIdentityUnavailableKind.Unsupported));
+ }
+
+ (string assemblyName, Guid moduleVersionId) =
+ SemanticModuleIdentity();
+ string methodName = ReadSemanticString(method.Name);
+ bool invalidGenericDeclaration =
+ !MemberResolver.HasExactGenericParameters(
+ Reader,
+ methodParameters,
+ signature.GenericParameterCount,
+ () => ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind
+ .GenericParameterRows,
+ 1));
+ bool isExtension =
+ IsSemanticExtensionMethod(type, method);
+
+ var identity = new MethodSemanticIdentity(
+ assemblyName,
+ moduleVersionId,
+ declaringType,
+ methodName,
+ signature.ParameterTypes,
+ signature.ReturnType,
+ _rowToken,
+ (method.Attributes & MethodAttributes.Static) != 0,
+ isExtension,
+ methodParameters.Count,
+ methodParameterNames,
+ signature.Header.RawValue,
+ signature.RequiredParameterCount,
+ invalidGenericDeclaration,
+ LibraryBodyPrimaryMetadataResolver
+ .DispatchCanTargetOverride(type, method));
+ _semanticIdentitiesDecoded++;
+ return CacheSemanticIdentity(
+ new MethodSemanticIdentityResult.Available(identity));
+ }
+ catch (ProducerAbortException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ when (LibraryMethodAnalysisRunner
+ .IsRecoverableMethodFailure(ex))
+ {
+ _semanticMalformedResults++;
+ return CacheSemanticIdentity(
+ new MethodSemanticIdentityResult.Unavailable(
+ MethodSemanticIdentityUnavailableKind.Malformed));
+ }
+ }
+
+ MethodSemanticIdentityResult CacheSemanticIdentity(
+ MethodSemanticIdentityResult result)
+ {
+ if (result is MethodSemanticIdentityResult.Unavailable
+ {
+ Kind: MethodSemanticIdentityUnavailableKind.Unsupported,
+ })
+ {
+ _semanticUnsupportedResults++;
+ }
+
+ _semanticIdentity = result;
+ _semanticIdentityRead = true;
+ return result;
+ }
+
+ ImmutableArray ReadGenericParameterNames(
+ GenericParameterHandleCollection handles)
+ {
+ if (handles.Count == 0)
+ return [];
+
+ var names =
+ ImmutableArray.CreateBuilder(handles.Count);
+ foreach (GenericParameterHandle handle in handles)
+ {
+ ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.GenericParameterRows,
+ 1);
+ GenericParameter parameter =
+ Reader.GetGenericParameter(handle);
+ names.Add(ReadSemanticString(parameter.Name));
+ }
+
+ return names.MoveToImmutable();
+ }
+
+ (string AssemblyName, Guid ModuleVersionId)
+ SemanticModuleIdentity()
+ {
+ if (!_semanticModuleIdentityRead)
+ {
+ ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.AssociationRows,
+ Reader.IsAssembly ? 2 : 1);
+ _semanticAssemblyName =
+ Reader.IsAssembly
+ ? ReadSemanticString(
+ Reader.GetAssemblyDefinition().Name)
+ : Path.GetFileNameWithoutExtension(_sourceName);
+ _semanticModuleVersionId =
+ Reader.GetGuid(
+ Reader.GetModuleDefinition().Mvid);
+ _semanticModuleIdentityRead = true;
+ }
+
+ return (
+ _semanticAssemblyName!,
+ _semanticModuleVersionId);
+ }
+
+ bool IsSemanticExtensionMethod(
+ TypeDefinition type,
+ MethodDefinition method)
+ {
+ if ((type.Attributes & TypeAttributes.Abstract) == 0
+ || (type.Attributes & TypeAttributes.Sealed) == 0
+ || (method.Attributes & MethodAttributes.Static) == 0)
+ {
+ return false;
+ }
+
+ if (!_extensionTypes.TryGetValue(
+ _typeHandle,
+ out bool extensionType))
+ {
+ extensionType =
+ HasSemanticExtensionAttribute(
+ type.GetCustomAttributes());
+ _extensionTypes.Add(_typeHandle, extensionType);
+ }
+
+ return extensionType
+ && HasSemanticExtensionAttribute(
+ method.GetCustomAttributes());
+ }
+
+ bool HasSemanticExtensionAttribute(
+ CustomAttributeHandleCollection attributes)
+ {
+ foreach (CustomAttributeHandle handle in attributes)
+ {
+ ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.AssociationRows,
+ 1);
+ CustomAttribute attribute =
+ Reader.GetCustomAttribute(handle);
+ string? name =
+ AttributeReader.GetAttributeTypeName(
+ Reader,
+ attribute.Constructor,
+ amount => ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.StringBytes,
+ amount));
+ AbortIfSemanticIdentityWorkExhausted();
+ if (name == KnownAttributeNames.ExtensionAttribute)
+ return true;
+ }
+
+ return false;
+ }
+
+ string ReadSemanticString(StringHandle handle)
+ {
+ if (handle.IsNil)
+ return string.Empty;
+
+ ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind.StringBytes,
+ Reader.GetBlobReader(handle).Length);
+ return MetadataSafetyPolicy.ReadStructuralString(
+ Reader,
+ handle);
+ }
+
+ static bool ContainsUnsupportedType(TypeRef type)
+ {
+ if (type.Kind == TypeRefKind.Unsupported)
+ return true;
+ if (type.ElementType is { } element
+ && ContainsUnsupportedType(element))
+ {
+ return true;
+ }
+ foreach (TypeRef argument in type.TypeArguments)
+ {
+ if (ContainsUnsupportedType(argument))
+ return true;
+ }
+
+ return false;
+ }
+
+ void ChargeSemanticIdentityWork(
+ MethodSemanticIdentityWorkLimitKind kind,
+ int amount)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(amount);
+ ref long charged =
+ ref SemanticIdentityCounter(kind);
+ charged += amount;
+ long limit =
+ kind is MethodSemanticIdentityWorkLimitKind
+ .GenericParameterRows
+ or MethodSemanticIdentityWorkLimitKind
+ .AssociationRows
+ ? MetadataSafetyPolicy
+ .MaxMemorySafetyProjectionIntegrityRows
+ : MetadataSafetyPolicy
+ .MaxClassificationScanWorkChars;
+ if (charged <= limit)
+ return;
+
+ _semanticReachedLimit ??= kind;
+ Abort(
+ SemanticIdentityWork,
+ $"The semantic-identity {kind} bound is exhausted.");
+ }
+
+ void AbortIfSemanticIdentityWorkExhausted()
+ {
+ if (_semanticReachedLimit is { } reached)
+ {
+ Abort(
+ SemanticIdentityWork,
+ $"The semantic-identity {reached} bound is exhausted.");
+ }
+ }
+
+ ref long SemanticIdentityCounter(
+ MethodSemanticIdentityWorkLimitKind kind)
+ {
+ switch (kind)
+ {
+ case MethodSemanticIdentityWorkLimitKind.SignatureBytes:
+ return ref _semanticSignatureBytes;
+ case MethodSemanticIdentityWorkLimitKind.TypeWork:
+ return ref _semanticTypeWork;
+ case MethodSemanticIdentityWorkLimitKind.GenericParameterRows:
+ return ref _semanticGenericParameterRows;
+ case MethodSemanticIdentityWorkLimitKind.StringBytes:
+ return ref _semanticStringBytes;
+ case MethodSemanticIdentityWorkLimitKind.AssociationRows:
+ return ref _semanticAssociationRows;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(kind));
+ }
+ }
+
readonly Dictionary _moduleNames = [];
///
diff --git a/src/ILInspector.Analysis/Planning/ProducerResults.cs b/src/ILInspector.Analysis/Planning/ProducerResults.cs
index f041f696ba..bd6726b4e6 100644
--- a/src/ILInspector.Analysis/Planning/ProducerResults.cs
+++ b/src/ILInspector.Analysis/Planning/ProducerResults.cs
@@ -77,6 +77,33 @@ public sealed record ProducerLayerParticipation(
string Layer,
int Acquired);
+/// Semantic MethodDef identity work performed by one source-gate execution.
+public sealed record MethodSemanticIdentityWorkReceipt(
+ bool BudgetArmed,
+ int IdentitiesDecoded,
+ int UnsupportedResults,
+ int MalformedResults,
+ long SignatureBytes,
+ long TypeWork,
+ long GenericParameterRows,
+ long StringBytes,
+ long AssociationRows,
+ MethodSemanticIdentityWorkLimitKind? ReachedLimit)
+{
+ public static MethodSemanticIdentityWorkReceipt Empty { get; } =
+ new(false, 0, 0, 0, 0, 0, 0, 0, 0, null);
+}
+
+/// A semantic-identity work dimension that reached its execution bound.
+public enum MethodSemanticIdentityWorkLimitKind
+{
+ SignatureBytes,
+ TypeWork,
+ GenericParameterRows,
+ StringBytes,
+ AssociationRows,
+}
+
///
/// What actually happened for one producer, recorded by the executor where
/// work started and ended.
@@ -106,6 +133,10 @@ public sealed record WorkReceipt(
/// Signature-shape nodes the gate walked across the execution, memoized walks counted once.
public long SignatureShapeNodesWalked { get; init; }
+ /// Bounded structural MethodDef identity work performed by the source gate.
+ public MethodSemanticIdentityWorkReceipt SemanticIdentityWork { get; init; } =
+ MethodSemanticIdentityWorkReceipt.Empty;
+
public ProducerParticipation For(ProducerDeclaration producer)
{
ArgumentNullException.ThrowIfNull(producer);
diff --git a/tests/ILInspector.Analysis.Tests/GateFixtureImage.cs b/tests/ILInspector.Analysis.Tests/GateFixtureImage.cs
index 07ffee8ed6..5639b19c10 100644
--- a/tests/ILInspector.Analysis.Tests/GateFixtureImage.cs
+++ b/tests/ILInspector.Analysis.Tests/GateFixtureImage.cs
@@ -118,6 +118,17 @@ public ImmutableArray Build(Machine machine = Machine.Unknown)
_metadata.AddCustomAttribute(handle, constructor, default);
foreach ((EntityHandle constructor, BlobBuilder value) in type.ValuedAttributes)
_metadata.AddCustomAttribute(handle, constructor, _metadata.GetOrAddBlob(value));
+ for (int genericIndex = 0;
+ genericIndex < type.GenericParameterNames.Count;
+ genericIndex++)
+ {
+ _metadata.AddGenericParameter(
+ handle,
+ GenericParameterAttributes.None,
+ _metadata.GetOrAddString(
+ type.GenericParameterNames[genericIndex]),
+ genericIndex);
+ }
if (type.Enclosing is { } enclosing)
_metadata.AddNestedType(handle, typeHandles[enclosing]);
@@ -131,6 +142,17 @@ public ImmutableArray Build(Machine machine = Machine.Unknown)
-1,
MetadataTokens.ParameterHandle(nextParameter));
nextMethod++;
+ for (int genericIndex = 0;
+ genericIndex < method.GenericParameterNames.Length;
+ genericIndex++)
+ {
+ _metadata.AddGenericParameter(
+ methodHandle,
+ GenericParameterAttributes.None,
+ _metadata.GetOrAddString(
+ method.GenericParameterNames[genericIndex]),
+ genericIndex);
+ }
for (int p = 0; p < method.ParameterNames.Length; p++)
{
_metadata.AddParameter(
@@ -360,6 +382,13 @@ public FixtureType MethodAttribute(EntityHandle constructor, BlobBuilder value)
public StringHandle? NamespaceOverride { get; init; }
public List Methods { get; } = [];
public List AttributeConstructors { get; } = [];
+ public List GenericParameterNames { get; } = [];
+
+ public FixtureType GenericParameters(params string[] names)
+ {
+ GenericParameterNames.AddRange(names);
+ return this;
+ }
/// Attaches custom attributes to the type itself.
public FixtureType Attributes(params EntityHandle[] attributeConstructors)
@@ -387,6 +416,16 @@ public FixtureType Method(
parameterNames ?? []));
return this;
}
+
+ /// Adds generic parameters to the most recently added method.
+ public FixtureType MethodGenericParameters(params string[] names)
+ {
+ Methods[^1] = Methods[^1] with
+ {
+ GenericParameterNames = names,
+ };
+ return this;
+ }
}
internal sealed record FixtureMethod(
@@ -399,6 +438,7 @@ internal sealed record FixtureMethod(
string[]? ParameterNames = null)
{
public string[] ParameterNames { get; init; } = ParameterNames ?? [];
+ public string[] GenericParameterNames { get; init; } = [];
public List<(EntityHandle Constructor, BlobBuilder Value)> ValuedAttributes { get; } = [];
}
}
diff --git a/tests/ILInspector.Analysis.Tests/MethodRowGateTests.cs b/tests/ILInspector.Analysis.Tests/MethodRowGateTests.cs
index c3d5fba4f5..04c383ffd4 100644
--- a/tests/ILInspector.Analysis.Tests/MethodRowGateTests.cs
+++ b/tests/ILInspector.Analysis.Tests/MethodRowGateTests.cs
@@ -100,6 +100,324 @@ public void IdentityBudget_IdentityTextArmsAndCharges()
Assert.Equal(ProducerOutcome.Complete, execution.ResultOf(rows).Outcome);
}
+ [Fact]
+ public void SemanticIdentity_MatchesCurrentProjectionWithoutIdentityTextOrBody()
+ {
+ ImmutableArray image = SemanticIdentities();
+ var rows = new GateProducer(
+ "Semantic",
+ MethodDefinitionLayers.SemanticIdentity);
+ SemanticIdentityPredicate.Seen.Clear();
+
+ MethodDefinitionExecution execution =
+ Run(
+ image,
+ Plan(new ProducerRequest(rows)),
+ MethodDefinitionSourceBreadth.ExactMethods(
+ MethodHandles(image)));
+
+ ImmutableArray results =
+ [.. SemanticIdentityPredicate.Seen];
+ Assert.Equal(5, results.Length);
+ Assert.All(
+ results,
+ result =>
+ {
+ MethodSemanticIdentity semantic =
+ Assert.IsType<
+ MethodSemanticIdentityResult.Available>(
+ result).Identity;
+ MethodIdentity current =
+ CurrentMethodIdentity(
+ image,
+ semantic.MetadataToken);
+ Assert.Equal(
+ current,
+ semantic.ToMethodIdentity(
+ current.CallerUnsafeMode));
+ Assert.Equal(
+ current.IsVirtualDispatchOpen,
+ semantic.IsVirtualDispatchOpen);
+ });
+
+ MethodSemanticIdentity generic =
+ AvailableNamed(results, "Generic");
+ Assert.Equal(1, generic.GenericArity);
+ Assert.Equal(["TMethod"], generic.GenericParameterNames);
+ Assert.False(generic.HasInvalidGenericParameterDeclaration);
+
+ MethodSemanticIdentity invalidGeneric =
+ AvailableNamed(results, "InvalidGeneric");
+ Assert.True(
+ invalidGeneric.HasInvalidGenericParameterDeclaration);
+
+ MethodSemanticIdentity vararg =
+ AvailableNamed(results, "Vararg");
+ Assert.Equal(0x05, vararg.SignatureHeader & 0x0F);
+ Assert.Equal(1, vararg.RequiredParameterCount);
+
+ MethodSemanticIdentity extension =
+ AvailableNamed(results, "Extension");
+ Assert.True(extension.IsExtension);
+
+ MethodSemanticIdentity nested =
+ AvailableNamed(results, "NestedVirtual");
+ Assert.Equal(TypeRefKind.Definition, nested.DeclaringType.Kind);
+ Assert.Contains("Outer`1+Inner`1", nested.DeclaringType.Name);
+ Assert.True(nested.IsVirtualDispatchOpen);
+
+ Assert.False(execution.Receipt.IdentityBudgetArmed);
+ Assert.Equal(0, execution.Receipt.IdentityWorkCharged);
+ Assert.True(
+ execution.Receipt.SemanticIdentityWork.BudgetArmed);
+ Assert.Equal(
+ 5,
+ execution.Receipt.SemanticIdentityWork
+ .IdentitiesDecoded);
+ Assert.True(
+ execution.Receipt.SemanticIdentityWork.SignatureBytes > 0);
+ Assert.True(
+ execution.Receipt.SemanticIdentityWork.TypeWork > 0);
+ Assert.True(
+ execution.Receipt.SemanticIdentityWork
+ .GenericParameterRows > 0);
+ Assert.True(
+ execution.Receipt.SemanticIdentityWork.StringBytes > 0);
+ Assert.True(
+ execution.Receipt.SemanticIdentityWork.AssociationRows > 0);
+ Assert.Equal(
+ execution.Receipt.SemanticIdentityWork,
+ execution.SourceCoverage.SemanticIdentityWork);
+ Assert.Equal(0, execution.SourceCoverage.BodiesAttempted.Count);
+ Assert.Equal(0, execution.SourceCoverage.BodiesAcquired.Count);
+ ProducerLayerParticipation semanticLayer =
+ Assert.Single(
+ execution.Receipt.For(rows).Layers,
+ layer => layer.Layer
+ == nameof(
+ MethodDefinitionLayers.SemanticIdentity));
+ Assert.Equal(5, semanticLayer.Acquired);
+ }
+
+ [Fact]
+ public void SemanticIdentity_CoreLibExactMethodStaysSparse()
+ {
+ ImmutableArray image =
+ [.. File.ReadAllBytes(
+ typeof(System.Text.StringBuilder)
+ .Assembly.Location)];
+ MethodDefinitionHandle target;
+ using (var peReader = new PEReader(image))
+ {
+ MetadataReader reader =
+ peReader.GetMetadataReader();
+ target = reader.MethodDefinitions
+ .First(
+ handle =>
+ {
+ MethodDefinition method =
+ reader.GetMethodDefinition(handle);
+ if (reader.GetString(method.Name)
+ != "AppendFormat")
+ {
+ return false;
+ }
+
+ TypeDefinition type =
+ reader.GetTypeDefinition(
+ method.GetDeclaringType());
+ return reader.GetString(type.Namespace)
+ == "System.Text"
+ && reader.GetString(type.Name)
+ == "StringBuilder";
+ });
+ }
+
+ var rows = new GateProducer(
+ "Semantic",
+ MethodDefinitionLayers.SemanticIdentity);
+ SemanticIdentityPredicate.Seen.Clear();
+ MethodDefinitionExecution execution =
+ Run(
+ image,
+ Plan(new ProducerRequest(rows)),
+ MethodDefinitionSourceBreadth
+ .ExactMethods(target));
+
+ MethodSemanticIdentity semantic =
+ Assert.IsType<
+ MethodSemanticIdentityResult.Available>(
+ Assert.Single(
+ SemanticIdentityPredicate.Seen))
+ .Identity;
+ Assert.Equal("AppendFormat", semantic.Name);
+ Assert.Equal(
+ "System.Text",
+ semantic.DeclaringType.Namespace);
+ Assert.Equal(
+ "StringBuilder",
+ semantic.DeclaringType.Name);
+ Assert.Equal(
+ 1,
+ execution.SourceCoverage
+ .DefinitionsExamined.Count);
+ Assert.Equal(
+ 1,
+ execution.SourceCoverage.MethodsSelected.Count);
+ Assert.Equal(
+ 0,
+ execution.SourceCoverage.BodiesAttempted.Count);
+ Assert.False(execution.Receipt.IdentityBudgetArmed);
+ }
+
+ [Fact]
+ public void SemanticIdentity_MalformedAndUnsupportedAreTyped()
+ {
+ var malformedBuilder = new GateFixtureImage();
+ malformedBuilder.Type(
+ "N",
+ "Malformed",
+ namespaceOverride:
+ MetadataTokens.StringHandle(1))
+ .Method("M");
+ (
+ ImmutableArray
+ malformedResults,
+ MethodSemanticIdentityWorkReceipt malformedWork) =
+ ReadSemanticIdentities(
+ malformedBuilder.Build());
+ MethodSemanticIdentityResult malformed =
+ Assert.Single(malformedResults);
+ Assert.Equal(
+ MethodSemanticIdentityUnavailableKind.Malformed,
+ Assert.IsType<
+ MethodSemanticIdentityResult.Unavailable>(
+ malformed).Kind);
+ Assert.Equal(1, malformedWork.MalformedResults);
+
+ var unsupportedBuilder = new GateFixtureImage();
+ var signature = new BlobBuilder();
+ signature.WriteByte(0x00);
+ int parameters =
+ MetadataSafetyPolicy.MaxSignatureTypeNodes + 1;
+ signature.WriteCompressedInteger(parameters);
+ signature.WriteByte(0x01);
+ for (int i = 0; i < parameters; i++)
+ signature.WriteByte(0x08);
+ unsupportedBuilder.Type("N", "Unsupported")
+ .Method("M", signature);
+ (
+ ImmutableArray
+ unsupportedResults,
+ MethodSemanticIdentityWorkReceipt unsupportedWork) =
+ ReadSemanticIdentities(
+ unsupportedBuilder.Build());
+ MethodSemanticIdentityResult unsupported =
+ Assert.Single(unsupportedResults);
+ Assert.Equal(
+ MethodSemanticIdentityUnavailableKind.Unsupported,
+ Assert.IsType<
+ MethodSemanticIdentityResult.Unavailable>(
+ unsupported).Kind);
+ Assert.Equal(
+ 1,
+ unsupportedWork.UnsupportedResults);
+ }
+
+ [Fact]
+ public void SemanticIdentity_InvalidExactTargetExaminesNoMethodRow()
+ {
+ ImmutableArray image = Ordinary().Build();
+ var rows = new GateProducer(
+ "Semantic",
+ MethodDefinitionLayers.SemanticIdentity);
+ WorkDescription work =
+ Plan(new ProducerRequest(rows));
+ var breadth =
+ MethodDefinitionSourceBreadth.ExactMethods(
+ MetadataTokens.MethodDefinitionHandle(10_000));
+ SemanticIdentityPredicate.Seen.Clear();
+
+ MethodDefinitionExecution execution =
+ Run(image, work, breadth);
+
+ Assert.Empty(SemanticIdentityPredicate.Seen);
+ Assert.Equal(
+ 0,
+ execution.SourceCoverage.DefinitionsExamined.Count);
+ Assert.Equal(
+ ProducerOutcome.Failed,
+ execution.ResultOf(rows).Outcome);
+ Assert.Equal(
+ 0,
+ execution.Receipt.SemanticIdentityWork
+ .IdentitiesDecoded);
+ }
+
+ [Fact]
+ public void SemanticIdentity_RejectsBroadOrExpandedBreadth()
+ {
+ ImmutableArray image = Ordinary().Build();
+ var rows = new GateProducer(
+ "Semantic",
+ MethodDefinitionLayers.SemanticIdentity);
+ WorkDescription work =
+ Plan(new ProducerRequest(rows));
+
+ Assert.Throws(
+ () => Run(
+ image,
+ work,
+ MethodDefinitionSourceBreadth
+ .AllDefinitions));
+ Assert.Throws(
+ () => Run(
+ image,
+ work,
+ MethodDefinitionSourceBreadth
+ .ExactMethods(
+ MethodHandles(image))
+ .IncludeGeneratedExecutionBodies()));
+ }
+
+ [Fact]
+ public void SemanticIdentity_ExhaustedBoundIsTypedAndReceipted()
+ {
+ var builder = new GateFixtureImage();
+ var signature = new BlobBuilder();
+ signature.WriteByte(0x00);
+ signature.WriteBytes(
+ new byte[
+ MetadataSafetyPolicy
+ .MaxClassificationScanWorkChars]);
+ builder.Type("N", "OverBudget")
+ .Method("M", signature);
+ ImmutableArray image = builder.Build();
+ var rows = new GateProducer(
+ "Semantic",
+ MethodDefinitionLayers.SemanticIdentity);
+
+ MethodDefinitionExecution execution =
+ Run(
+ image,
+ Plan(new ProducerRequest(rows)),
+ MethodDefinitionSourceBreadth.ExactMethods(
+ MethodHandles(image)));
+
+ ProducerResult result =
+ execution.ResultOf(rows);
+ Assert.Equal(ProducerOutcome.Aborted, result.Outcome);
+ Assert.Equal(
+ MethodRowGate.SemanticIdentityWork,
+ result.Critical!.Budget);
+ Assert.Equal(
+ MethodSemanticIdentityWorkLimitKind.SignatureBytes,
+ execution.Receipt.SemanticIdentityWork.ReachedLimit);
+ Assert.Equal(
+ execution.Receipt.SemanticIdentityWork,
+ execution.SourceCoverage.SemanticIdentityWork);
+ }
+
[Fact]
public void IdentityBudget_HostileIdentitiesAbortRowsButNotCount()
{
@@ -1106,6 +1424,156 @@ static GateFixtureImage Ordinary()
return builder;
}
+ static ImmutableArray SemanticIdentities()
+ {
+ var builder = new GateFixtureImage();
+ TypeReferenceHandle extensionAttribute =
+ builder.TypeRef(
+ "System.Runtime.CompilerServices",
+ "ExtensionAttribute");
+ MemberReferenceHandle extensionConstructor =
+ builder.AttributeConstructor(
+ extensionAttribute);
+ GateFixtureImage.FixtureType extensions =
+ builder.Type("N", "Extensions")
+ .Attributes(extensionConstructor)
+ .Method(
+ "Generic",
+ GenericIdentitySignature())
+ .MethodGenericParameters("TMethod")
+ .Method(
+ "InvalidGeneric",
+ InvalidGenericIdentitySignature())
+ .MethodGenericParameters("TOnly")
+ .Method(
+ "Vararg",
+ VarargIdentitySignature())
+ .Method(
+ "Extension",
+ GateFixtureImage.VoidSignature(
+ parameter => parameter.Int32()),
+ attributeConstructors:
+ extensionConstructor);
+ _ = extensions;
+
+ GateFixtureImage.FixtureType outer =
+ builder.Type("N", "Outer`1")
+ .GenericParameters("TOuter");
+ builder.Type("N", "Inner`1", outer)
+ .GenericParameters("TOuter")
+ .WithShape(TypeAttributes.NestedPublic)
+ .Method(
+ "NestedVirtual",
+ attributes:
+ MethodAttributes.Public
+ | MethodAttributes.Virtual);
+ return builder.Build();
+ }
+
+ static BlobBuilder GenericIdentitySignature()
+ {
+ var signature = new BlobBuilder();
+ signature.WriteByte(0x10);
+ signature.WriteCompressedInteger(1);
+ signature.WriteCompressedInteger(1);
+ signature.WriteByte(0x1E);
+ signature.WriteCompressedInteger(0);
+ signature.WriteByte(0x1E);
+ signature.WriteCompressedInteger(0);
+ return signature;
+ }
+
+ static BlobBuilder InvalidGenericIdentitySignature()
+ {
+ var signature = GenericIdentitySignature();
+ byte[] bytes = signature.ToArray();
+ bytes[1] = 2;
+ return Bytes(bytes);
+ }
+
+ static BlobBuilder VarargIdentitySignature()
+ {
+ var signature = new BlobBuilder();
+ signature.WriteByte(0x05);
+ signature.WriteCompressedInteger(2);
+ signature.WriteByte(0x01);
+ signature.WriteByte(0x08);
+ signature.WriteByte(0x41);
+ signature.WriteByte(0x0E);
+ return signature;
+ }
+
+ static MethodSemanticIdentity AvailableNamed(
+ ImmutableArray results,
+ string name) =>
+ Assert.Single(
+ results
+ .OfType<
+ MethodSemanticIdentityResult.Available>()
+ .Select(result => result.Identity),
+ identity => identity.Name == name);
+
+ static (
+ ImmutableArray Results,
+ MethodSemanticIdentityWorkReceipt Work)
+ ReadSemanticIdentities(
+ ImmutableArray image)
+ {
+ var rows = new GateProducer(
+ "Semantic",
+ MethodDefinitionLayers.SemanticIdentity);
+ SemanticIdentityPredicate.Seen.Clear();
+ MethodDefinitionExecution execution =
+ Run(
+ image,
+ Plan(new ProducerRequest(rows)),
+ MethodDefinitionSourceBreadth.ExactMethods(
+ MethodHandles(image)));
+ return (
+ [.. SemanticIdentityPredicate.Seen],
+ execution.Receipt.SemanticIdentityWork);
+ }
+
+ static ImmutableArray MethodHandles(
+ ImmutableArray image)
+ {
+ using var peReader = new PEReader(image);
+ return [
+ .. peReader.GetMetadataReader()
+ .MethodDefinitions,
+ ];
+ }
+
+ static MethodIdentity CurrentMethodIdentity(
+ ImmutableArray image,
+ int token)
+ {
+ using var peReader = new PEReader(image);
+ MetadataReader reader =
+ peReader.GetMetadataReader();
+ var methodHandle =
+ (MethodDefinitionHandle)
+ MetadataTokens.EntityHandle(token);
+ MethodDefinition method =
+ reader.GetMethodDefinition(methodHandle);
+ TypeDefinitionHandle typeHandle =
+ method.GetDeclaringType();
+ TypeDefinition type =
+ reader.GetTypeDefinition(typeHandle);
+ using var builder =
+ new LibraryBodyAnalysisBuilder(
+ "GateFixture.dll",
+ reader,
+ peReader);
+ var infrastructure =
+ (ILibraryMethodAnalysisInfrastructure)builder;
+ return infrastructure.CreateMethodIdentity(
+ typeHandle,
+ methodHandle,
+ method,
+ infrastructure.CreateScope(type, method));
+ }
+
static ImmutableArray HostileIdentities(int methodCount, int parameterCount, int genericArity)
{
var builder = new GateFixtureImage();
@@ -1186,6 +1654,20 @@ static MethodDefinitionExecution Run(ImmutableArray image, WorkDescription
return MethodDefinitionExecution.Execute(description, "GateFixture.dll", peReader);
}
+ static MethodDefinitionExecution Run(
+ ImmutableArray image,
+ WorkDescription description,
+ MethodDefinitionSourceBreadth breadth)
+ {
+ using var peReader = new PEReader(image);
+ return MethodDefinitionExecution.Execute(
+ description,
+ "GateFixture.dll",
+ peReader,
+ breadth,
+ MethodDefinitionTerminalWorkLimits.Unbounded);
+ }
+
// ---- Producers ----
sealed class GateProducer(
@@ -1287,6 +1769,19 @@ struct IdentityPredicate : IMethodDefinitionPredicate
public readonly bool Test(scoped MethodDefinitionView view) => view.Identity.Signature.ToString().Length > 0;
}
+ struct SemanticIdentityPredicate : IMethodDefinitionPredicate
+ {
+ public static readonly List<
+ MethodSemanticIdentityResult> Seen = [];
+
+ public readonly bool Test(
+ scoped MethodDefinitionView view)
+ {
+ Seen.Add(view.SemanticIdentity);
+ return true;
+ }
+ }
+
struct LoggingPredicate : IMethodDefinitionPredicate
{
public static readonly List Tokens = [];