diff --git a/ROADMAP.md b/ROADMAP.md
index f1f1577..5276234 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -429,7 +429,7 @@ publish warnings and no cleanup diagnostic. Both CoreCLR and Native AOT print
| P1-02 | ✅ Complete | Parse modules, items, statements, expressions, patterns, types, generics, and attributes in the safe-core profile. | P1-01 | `dotnet run --project tools/RustSharp.Conformance -c Release --no-restore -- --profile safe-core-syntax`
`pwsh -NoProfile -File eng/Test-SyntaxEvidence.ps1` | Manifest v3 passes 49/49 cases, 34/34 exact AST snapshots and 18/18 required categories. Rejections match diagnostic codes and source text; cancellation/deadlines, recovery and invalid evidence are tested in the 141/141 regression harness. The declared syntax profile is complete; unsupported semantic/HIR extensions explicitly report RSN1007. See the syntax contract and local evidence above. |
| P1-03 | ✅ Complete | Lower AST to HIR and implement the declared safe-core modules, namespaces, visibility, imports, name resolution, and Cargo package entry point. | P1-02 | `dotnet run --project tools/RustSharp.Conformance -c Release --no-restore -- --profile safe-core-name-resolution`
`dotnet run --project tests/RustSharp.Tests/RustSharp.Tests.csproj -c Release --no-restore`
`rsc check tests/workspaces/basic/Cargo.toml --profile safe-core-primitives-v1` | The acceptance manifest and executable harness pass 25/25 and 190/190. HIR preserves const-function qualifiers and deterministic declaration/reference bindings. Grouped/glob/self/anonymous imports, restricted visibility, source documentation, bounded file modules, original-file diagnostics and PDB mappings are integrated. `Cargo.toml` is accepted by `check`, `build`/`compile`, `run` and `publish`; package metadata, deterministic local `path` dependencies, source discovery, cycle/limit checks and explicit registry-dependency diagnostics are implemented. Leading `::`, unevaluated attributes, registry packages, Cargo features/lockfiles and macro expansion remain explicit profile boundaries for later milestones. |
| P1-04 | ✅ Complete | Implement primitive, tuple, array, slice, reference, function, ADT, and never types with inference/coercion rules. | P1-03 | `dotnet run --project tests/RustSharp.Tests/RustSharp.Tests.csproj -c Release --no-restore`
`dotnet run --project tools/RustSharp.Conformance -c Release --no-build --no-restore -- --profile safe-core-types-v1 --oracle rustc-1.98` | The monomorphic check-only contract includes primitive numeric types, aggregates, references, function pointers, nongeneric ADTs, aliases, patterns/match, closures, bounded const evaluation, inference and directional coercions. All 265/265 regressions and 96/96 version 2 differential cases across sixteen required categories pass, with zero failures/skips and no cleanup diagnostic. File/Cargo checking is integrated; executable commands reject with RSC0009 before output. Generic/trait, MIR, ownership and executable-lowering gates remain separate. |
-| P1-05 | ⏳ Planned | Implement generic substitution, monomorphization, impl coherence, and the versioned trait-solver subset. | P0-14, P1-04 | `dotnet test RustSharp.slnx -c Release --filter GenericsAndTraits` | Generic functions/types emit closed AOT-reachable bodies; overlap, ambiguity, and missing bounds fail predictably. |
+| P1-05 | 🚧 In progress | Implement generic substitution, monomorphization, impl coherence, and the versioned trait-solver subset. | P0-14, P1-04 | `dotnet run --project tests/RustSharp.Tests -c Release --no-build --no-restore` | The first PR adds bounded structural substitution, trait obligations/coherence and deterministic closed-instance planning; see the [generic foundation contract](docs/generic-profile.md). Generic source/HIR integration, body specialization and AOT-reachable emission remain required for the full gate. |
| P1-06 | ⏳ Planned | Define typed MIR, CFG validation, desugaring, and source mapping. | P1-04 | `dotnet test RustSharp.slnx -c Release --filter Mir` | MIR snapshots are deterministic; invalid edges/types are rejected; diagnostics map back to `.rs` spans. |
| P1-07 | ⏳ Planned | Implement move paths, borrow checking, non-lexical lifetimes, reborrowing, and escape analysis for the profile. | P0-13, P1-06 | `dotnet run --project tools/RustSharp.Conformance -- --profile safe-core-borrow` | All declared borrow compile-pass/fail cases match rustc outcome and no rejected construct is silently accepted under CLR rules. |
| P1-08 | ⏳ Planned | Implement scope cleanup, deterministic `Drop`, unwind/abort profile behavior, and panic boundaries. | P1-06, P1-07 | `dotnet test RustSharp.slnx -c Release --filter DropAndPanic` | Normal/early-return/branch/panic paths run destructors once in specified order on CoreCLR and AOT. |
@@ -440,6 +440,20 @@ P1 exits when the versioned safe-core profile passes on CoreCLR and Windows/
Linux x64 Native AOT, and when borrow/Drop behavior has no unresolved semantic
difference inside that profile.
+### PR execution order after P1-04
+
+The next two implementation tracks may run in parallel: P1-05 depends on
+P0-14/P1-04, and P1-06 depends on P1-04. Review and merge the generic foundation
+PR first, followed by the typed-MIR foundation PR. Each PR states its exact
+implemented subset, regression evidence and remaining milestone criteria.
+
+Continue P1-05 with source/HIR generic binding and body specialization, and
+P1-06 with aggregate, pattern and closure lowering. P1-07 starts when its typed
+MIR prerequisites are usable; P1-08 follows the move/borrow gate. P1-09 combines
+generic specialization and ownership-aware lowering before P1-10 closes the
+full differential denominator. Completing a foundation PR does not mark its
+entire milestone complete.
+
## P2: Deliver the core library and usable toolchain
| ID | Status | Work item | Hard dependency | Acceptance command | Observable result |
diff --git a/ROADMAP_zh.md b/ROADMAP_zh.md
index d2005bb..75dbdae 100644
--- a/ROADMAP_zh.md
+++ b/ROADMAP_zh.md
@@ -354,7 +354,7 @@ AOT 探测器。这些工作流修改需要新的 CI 运行。此配置的 Linux
| P1-02 | ✅ 已完成 | 解析安全核心配置档中的模块、项、语句、表达式、模式、类型、泛型和属性。 | P1-01 | `dotnet run --project tools/RustSharp.Conformance -c Release --no-restore -- --profile safe-core-syntax`
`pwsh -NoProfile -File eng/Test-SyntaxEvidence.ps1` | 第 3 版清单通过 49/49 个用例、34/34 份精确 AST 快照及 18/18 个必需类别。拒绝用例匹配诊断代码和源码文本;141/141 项回归工具覆盖取消/超时、错误恢复及非法证据。声明的语法配置已完成;尚不支持的语义/HIR 扩展显式返回 RSN1007。详见语法契约及上方本地证据。 |
| P1-03 | ✅ 已完成 | 将 AST 降低为 HIR,并实现声明的安全核心模块、命名空间、可见性、导入、名称解析和 Cargo 包入口。 | P1-02 | `dotnet run --project tools/RustSharp.Conformance -c Release --no-restore -- --profile safe-core-name-resolution`
`dotnet run --project tests/RustSharp.Tests/RustSharp.Tests.csproj -c Release --no-restore`
`rsc check tests/workspaces/basic/Cargo.toml --profile safe-core-primitives-v1` | 名称解析清单和可执行测试分别通过 25/25、190/190。HIR 保留 const 函数限定符,并确定性绑定声明和引用。分组/glob/self/匿名导入、受限可见性、源码文档、有界文件模块、原文件诊断和 PDB 映射已接入。`Cargo.toml` 已接入 `check`、`build`/`compile`、`run` 和 `publish`;已实现包元数据、确定性的本地 `path` 依赖、源码发现、循环/限制检查及对注册表依赖的明确诊断。前导 `::`、未求值属性、注册表包、Cargo feature/锁文件和宏展开仍作为后续里程碑的明确配置档边界。 |
| P1-04 | ✅ 已完成 | 实现原始类型、元组、数组、切片、引用、函数、ADT 和 never 类型,以及推断/强制转换规则。 | P1-03 | `dotnet run --project tests/RustSharp.Tests/RustSharp.Tests.csproj -c Release --no-restore`
`dotnet run --project tools/RustSharp.Conformance -c Release --no-build --no-restore -- --profile safe-core-types-v1 --oracle rustc-1.98` | 单态的仅检查类型契约覆盖基础数值类型、聚合、引用、函数指针、非泛型 ADT、别名、模式/match、闭包、有界 const 求值、推断和有方向的强制转换。265/265 项回归及十六个必需类别中的 96/96 项第 2 版差分用例全部通过,失败和跳过均为零,无清理诊断。已接入文件/Cargo 检查;可执行命令在输出前以 RSC0009 拒绝。泛型/trait、MIR、所有权及可执行降低仍属于独立门槛。 |
-| P1-05 | ⏳ 计划中 | 实现泛型替换、单态化、impl 一致性和版本化 trait 求解器子集。 | P0-14, P1-04 | `dotnet test RustSharp.slnx -c Release --filter GenericsAndTraits` | 泛型函数/类型发出封闭且 AOT 可达的主体;重叠、歧义和缺失约束会以可预测方式失败。 |
+| P1-05 | 🚧 进行中 | 实现泛型替换、单态化、impl 一致性和版本化 trait 求解器子集。 | P0-14, P1-04 | `dotnet run --project tests/RustSharp.Tests -c Release --no-build --no-restore` | 首个 PR 增加有界结构替换、trait 约束/一致性和确定性的封闭实例规划;见[泛型基础契约](docs/generic-profile.md)。完整门槛仍要求泛型源码/HIR 接入、主体特化以及 AOT 可达的代码生成。 |
| P1-06 | ⏳ 计划中 | 定义类型化 MIR、CFG 验证、脱糖和源码映射。 | P1-04 | `dotnet test RustSharp.slnx -c Release --filter Mir` | MIR 快照具有确定性;无效边/类型被拒绝;诊断映射回 `.rs` 范围。 |
| P1-07 | ⏳ 计划中 | 为该配置档实现移动路径、借用检查、非词法生命周期、再借用和逃逸分析。 | P0-13, P1-06 | `dotnet run --project tools/RustSharp.Conformance -- --profile safe-core-borrow` | 所有已声明的借用编译通过/失败用例都与 rustc 结果匹配,且不会在 CLR 规则下静默接受被拒绝的构造。 |
| P1-08 | ⏳ 计划中 | 实现作用域清理、确定性 `Drop`、展开/中止配置档行为和 panic 边界。 | P1-06, P1-07 | `dotnet test RustSharp.slnx -c Release --filter DropAndPanic` | 正常/提前返回/分支/panic 路径在 CoreCLR 和 AOT 上按指定顺序恰好运行一次析构函数。 |
@@ -364,6 +364,17 @@ AOT 探测器。这些工作流修改需要新的 CI 运行。此配置的 Linux
当版本化安全核心配置档在 CoreCLR 以及 Windows/Linux x64 Native AOT 上通过,且该
配置档内的借用/Drop 行为不存在未解决的语义差异时,P1 才能退出。
+### P1-04 之后的 PR 执行顺序
+
+接下来的两条实现线可以并行:P1-05 依赖 P0-14/P1-04,P1-06 依赖 P1-04。
+评审和合并时先处理泛型基础 PR,再处理类型化 MIR 基础 PR。每个 PR 都列明
+准确的已实现子集、回归证据及里程碑剩余验收条件。
+
+随后 P1-05 继续接入源码/HIR 泛型绑定和主体特化,P1-06 继续实现聚合、模式和
+闭包降低。P1-07 在类型化 MIR 前置条件可用后开始,P1-08 在移动/借用门槛之后
+推进。P1-09 汇合泛型特化和所有权感知降低,之后由 P1-10 闭合完整差分分母。
+基础 PR 完成不等于整个里程碑完成。
+
## P2:交付核心库和可用工具链
| ID | 状态 | 工作项 | 硬依赖 | 验收命令 | 可观察结果 |
diff --git a/docs/generic-profile.md b/docs/generic-profile.md
new file mode 100644
index 0000000..25757e2
--- /dev/null
+++ b/docs/generic-profile.md
@@ -0,0 +1,127 @@
+# Generic and trait foundation contracts
+
+P1-05 is 🚧 In progress. This first PR adds reusable semantic foundations on the
+existing `RustType` model. The P0 `TraitSolver`, its public signatures and default
+behavior, and the `safe-core-types-v1` check-only profile remain unchanged.
+
+The two version identifiers below describe library contracts. They are **not CLI
+profile names**. This PR neither accepts generic Rust source through the compiler
+pipeline nor emits generic executable bodies.
+
+## Structural substitution and matching
+
+`GenericSubstitution.Apply` recursively replaces type parameters inside nominal
+type arguments. Substitution is simultaneous: `{T -> U, U -> i32}` applied to
+`Pair` produces `Pair`. A missing replacement leaves its parameter
+open. Input dictionaries are copied with ordinal name comparison, regardless of
+the caller's dictionary comparer. Replacement types and the resulting composed
+type must satisfy the operation's nesting and identity-size limits.
+
+`GenericSubstitution.Match` matches a template against a **closed** actual type.
+Every occurrence of the same parameter must match the same structural type:
+`Pair` matches `Pair` and rejects `Pair`. The returned
+binding map is immutable and ordinal. A failed match returns no partial bindings.
+
+The type vocabulary is the P0 model: unit, bool, i32, str, parameters and nominal
+types with type arguments. The richer P1-04 semantic type model is not implicitly
+converted or erased. Lifetime and const parameters are outside this contract.
+
+## `bounded-traits-v1`
+
+`GenericTraitSolver` consumes immutable `GenericTraitImplementation` records.
+Each record declares an implementation identity, trait identity, target template,
+type parameters, and zero or more positive `GenericTraitObligation` bounds.
+Identities supplied by the integration layer must be fully resolved and unique;
+this layer does not perform source name resolution or crate ownership checks.
+
+The solver validates its complete implementation set before solving a closed
+goal. Every parameter must be declared and occur in its implementation target;
+every bound may reference only those parameters. Duplicate implementation IDs and
+malformed/default collections report `InvalidInput`.
+
+Coherence uses structural unification of implementation heads. Variables are
+shared within one head and renamed apart between implementations. An occurs
+check rules out overlap that would require an infinite type. An exact head and a
+matching generic head overlap; no most-specific winner is silently selected.
+Overlapping heads fail with `OverlappingImplementations` even if their bounds
+currently lack evidence. This conservative rule does not implement specialization,
+negative reasoning, Rust's orphan rules, associated types, supertraits, auto
+traits, higher-ranked bounds or trait objects.
+
+A unique matching implementation substitutes and recursively proves its bounds.
+Repeated successful goals are memoized within one operation. Missing evidence
+reports `MissingImplementation`; a repeated active goal reports
+`CyclicObligation`. Cycles are not accepted as coinductive proofs. Implementations
+and selected evidence are ordered by ordinal identity, independent of insertion
+order. Failures expose no partial successful evidence.
+
+## `generic-plan-v1`
+
+`GenericMonomorphization.Plan` consumes immutable function definitions, call
+templates and explicit root instances. A definition contains its declared type
+parameters, parameter/return type templates, call edges and trait bounds. All
+definitions and call references are validated, including unreachable definitions.
+Only instances reachable from the explicit roots appear in the output.
+
+The planner substitutes each reachable function's signature and calls, rejects
+open roots, undeclared parameters, wrong generic arity and missing definitions,
+and proves each instantiated function's bounds with `bounded-traits-v1`. The
+substitution, coherence checks, obligation proofs and reachability traversal share
+one resource budget.
+
+An instance is identified by its resolved function identity and structural type
+arguments, using length-delimited canonical keys rather than display strings.
+Repeated roots, diamond-shaped calls, and recursive calls to the same closed
+instance deduplicate. Calls and instances have deterministic canonical-key order;
+the order is stable, not a promise of source order or human alphabetical order.
+Type-growing recursion must terminate within the instance, depth, work and time
+budgets or report `LimitExceeded` with an empty plan. A failed plan never exposes
+a partial graph as an AOT-ready result.
+
+The output contains closed signatures and call edges. It is a reachability plan,
+not a typed generic body or generated IL, and does not itself prove executable
+AOT compatibility or Rust ownership semantics.
+
+## Resource and failure contract
+
+`GenericAnalysisLimits` applies to one public operation:
+
+| Limit | Default | Allowed range |
+| --- | --- | --- |
+| Recursive depth | 64 | 1–128 |
+| Work units | 100,000 | 1–1,000,000 |
+| Collection, cache or instance items | 4,096 | 1–4,096 |
+| Wall-clock timeout | 2 seconds | Greater than zero, at most 1 minute |
+
+Existing nominal types allow at most 16 arguments; generic declarations likewise
+allow at most 16 type parameters. Individual names are limited to 1,024
+characters and canonical type identities to 65,536 characters. Recursive and
+iterative work checks the shared wall-clock deadline and cancellation token.
+No background tasks, processes, timers or temporary files are created.
+
+Invalid limit configuration and null top-level arguments throw standard argument
+exceptions. Invalid model data returns `InvalidInput`. Cancellation throws
+`OperationCanceledException`. Exhaustion returns `LimitExceeded`; it never becomes
+an assumed proof or a successful partial plan. Public results include a status,
+an optional diagnostic and an `IsSuccess` property.
+
+## Validation and remaining P1-05 work
+
+`GenericFoundationTests` exercises nested/simultaneous substitution, ordinal
+identities, repeated-parameter consistency, composed depth limits, nested and
+missing bounds, conservative overlap, alpha-renaming and occurs checks, recursive
+obligation cycles, type-growing obligations, deterministic reachability,
+same-instance recursion, display-key collisions, bound validation, malformed
+inputs, cancellation and resource limits. The tests run in the existing executable
+regression harness:
+
+```powershell
+dotnet run --project tests/RustSharp.Tests/RustSharp.Tests.csproj -c Release --no-restore
+```
+
+The next P1-05 PRs must connect generic declarations and body checking to typed
+HIR, resolve trait/impl identities and crate ownership, define source diagnostics
+and a fixed differential corpus, and produce closed executable bodies through
+the later CLR LIR/AOT integration. The full P1-05 acceptance criterion remains
+open until generic functions/types emit closed AOT-reachable bodies and overlap,
+ambiguity and missing bounds fail predictably through that compiler pipeline.
diff --git a/src/RustSharp.Semantics/GenericMonomorphization.cs b/src/RustSharp.Semantics/GenericMonomorphization.cs
new file mode 100644
index 0000000..64208fa
--- /dev/null
+++ b/src/RustSharp.Semantics/GenericMonomorphization.cs
@@ -0,0 +1,210 @@
+using System.Collections.Immutable;
+using System.Globalization;
+
+namespace RustSharp.Semantics;
+
+public sealed record GenericFunctionInstance(string FunctionId, ImmutableArray Arguments);
+
+/// A structural call/signature template, supplied by a future typed HIR integration.
+public sealed record GenericFunctionDefinition(
+ string Id,
+ ImmutableArray Parameters,
+ ImmutableArray ParameterTypes,
+ RustType ReturnType,
+ ImmutableArray Calls,
+ ImmutableArray Bounds);
+
+public sealed record GenericMonomorphizedFunction(
+ GenericFunctionInstance Instance,
+ ImmutableArray ParameterTypes,
+ RustType ReturnType,
+ ImmutableArray Calls);
+
+public sealed record GenericMonomorphizationResult(
+ GenericAnalysisStatus Status,
+ ImmutableArray Instances,
+ ImmutableArray SelectedImplementations,
+ string? Diagnostic)
+{
+ public bool IsSuccess => Status == GenericAnalysisStatus.Complete;
+}
+
+/// Computes a deterministic finite set of closed reachable signatures and calls, without emitting bodies.
+public static class GenericMonomorphization
+{
+ public const string ProfileId = "generic-plan-v1";
+
+ public static GenericMonomorphizationResult Plan(
+ ImmutableArray definitions,
+ ImmutableArray roots,
+ GenericTraitSolver? traitSolver = null,
+ GenericAnalysisLimits? limits = null,
+ CancellationToken cancellationToken = default)
+ {
+ var budget = new GenericBudget(limits, cancellationToken);
+ try
+ {
+ var planner = new Planner(definitions, traitSolver ?? new GenericTraitSolver([]), budget);
+ return planner.Plan(roots);
+ }
+ catch (GenericFailure failure)
+ {
+ // A partial reachability graph must never be mistaken for a complete AOT plan.
+ return new(failure.Status, [], [], failure.Message);
+ }
+ }
+
+ private sealed class Planner
+ {
+ private readonly GenericBudget budget;
+ private readonly GenericTraitSession traits;
+ private readonly Dictionary definitions = new(StringComparer.Ordinal);
+ private readonly SortedDictionary pending = new(StringComparer.Ordinal);
+ private readonly SortedDictionary instances = new(StringComparer.Ordinal);
+
+ public Planner(ImmutableArray definitions, GenericTraitSolver solver, GenericBudget budget)
+ {
+ this.budget = budget;
+ budget.Count(definitions.IsDefault ? -1 : definitions.Length);
+ foreach (GenericFunctionDefinition definition in definitions)
+ {
+ budget.Step();
+ if (definition is null) throw Invalid("A function definition must not be null.");
+ budget.Name(definition.Id);
+ HashSet parameters = budget.Parameters(definition.Parameters);
+ ValidateTypes(definition.ParameterTypes, parameters);
+ GenericTypes.Key(definition.ReturnType, budget, parameters: parameters);
+ budget.Count(definition.Calls.IsDefault ? -1 : definition.Calls.Length);
+ foreach (GenericFunctionInstance call in definition.Calls)
+ {
+ if (call is null) throw Invalid("A call template must not be null.");
+ budget.Name(call.FunctionId);
+ ValidateTypes(call.Arguments, parameters);
+ }
+
+ budget.Count(definition.Bounds.IsDefault ? -1 : definition.Bounds.Length);
+ foreach (GenericTraitObligation bound in definition.Bounds)
+ {
+ budget.Step();
+ if (bound is null) throw Invalid("A function bound must not be null.");
+ budget.Name(bound.Trait);
+ GenericTypes.Key(bound.Target, budget, parameters: parameters);
+ }
+
+ if (!this.definitions.TryAdd(definition.Id, definition)) throw Invalid("Function identifiers must be unique.");
+ }
+
+ foreach (GenericFunctionDefinition definition in definitions)
+ {
+ budget.Step();
+ foreach (GenericFunctionInstance call in definition.Calls)
+ {
+ ValidateCall(call);
+ }
+ }
+
+ traits = solver.CreateSession(budget);
+ }
+
+ public GenericMonomorphizationResult Plan(ImmutableArray roots)
+ {
+ budget.Count(roots.IsDefault ? -1 : roots.Length);
+ foreach (GenericFunctionInstance root in roots) Enqueue(root);
+ // Each iteration consumes one unique instance; the count and shared time/work
+ // budget bound type-growing recursion while same-instance recursion deduplicates.
+ for (int iteration = 0; iteration < budget.Limits.MaximumItems && pending.Count > 0; iteration++)
+ {
+ budget.Step();
+ KeyValuePair entry = pending.First();
+ pending.Remove(entry.Key);
+ GenericFunctionInstance instance = entry.Value;
+ GenericFunctionDefinition definition = definitions[instance.FunctionId];
+ var bindings = new Dictionary(StringComparer.Ordinal);
+ for (int index = 0; index < definition.Parameters.Length; index++)
+ {
+ budget.Step();
+ bindings.Add(definition.Parameters[index], instance.Arguments[index]);
+ }
+
+ ImmutableArray parameterTypes = SubstituteTypes(definition.ParameterTypes, bindings);
+ RustType returnType = GenericTypes.Substitute(definition.ReturnType, bindings, budget, 0);
+ GenericTypes.Key(returnType, budget, requireClosed: true);
+ foreach (GenericTraitObligation bound in definition.Bounds)
+ {
+ RustType target = GenericTypes.Substitute(bound.Target, bindings, budget, 0);
+ traits.Resolve(new(bound.Trait, target), 0);
+ }
+
+ var calls = new SortedDictionary(StringComparer.Ordinal);
+ foreach (GenericFunctionInstance template in definition.Calls)
+ {
+ budget.Step();
+ var call = new GenericFunctionInstance(template.FunctionId, SubstituteTypes(template.Arguments, bindings));
+ calls.TryAdd(InstanceKey(call), call);
+ }
+
+ instances.Add(entry.Key, new(instance, parameterTypes, returnType, [.. calls.Values]));
+ foreach (GenericFunctionInstance call in calls.Values) Enqueue(call);
+ }
+
+ if (pending.Count > 0) throw new GenericFailure(GenericAnalysisStatus.LimitExceeded, "Monomorphization exceeded its closed-instance budget.");
+ return new(GenericAnalysisStatus.Complete, [.. instances.Values], traits.Selected, null);
+ }
+
+ private void Enqueue(GenericFunctionInstance instance)
+ {
+ string key = InstanceKey(instance);
+ if (instances.ContainsKey(key) || pending.ContainsKey(key)) return;
+ budget.Count(instances.Count + pending.Count + 1);
+ pending.Add(key, instance);
+ }
+
+ private string InstanceKey(GenericFunctionInstance instance)
+ {
+ budget.Step();
+ ValidateCall(instance);
+ string key = instance.FunctionId.Length.ToString(CultureInfo.InvariantCulture) + ":" + instance.FunctionId + "[";
+ foreach (RustType argument in instance.Arguments)
+ {
+ key += GenericTypes.Key(argument, budget, requireClosed: true);
+ }
+
+ return key + "]";
+ }
+
+ private void ValidateCall(GenericFunctionInstance instance)
+ {
+ budget.Step();
+ if (instance is null) throw Invalid("A function instance must not be null.");
+ budget.Name(instance.FunctionId);
+ budget.Count(instance.Arguments.IsDefault ? -1 : instance.Arguments.Length);
+ if (!definitions.TryGetValue(instance.FunctionId, out GenericFunctionDefinition? definition))
+ {
+ throw Invalid("A call references an undefined function.");
+ }
+
+ if (definition.Parameters.Length != instance.Arguments.Length) throw Invalid("A function instance has incorrect generic arity.");
+ }
+
+ private void ValidateTypes(ImmutableArray types, HashSet parameters)
+ {
+ budget.Count(types.IsDefault ? -1 : types.Length);
+ foreach (RustType type in types) GenericTypes.Key(type, budget, parameters: parameters);
+ }
+
+ private ImmutableArray SubstituteTypes(ImmutableArray types, Dictionary bindings)
+ {
+ var result = ImmutableArray.CreateBuilder(types.Length);
+ foreach (RustType type in types)
+ {
+ RustType closed = GenericTypes.Substitute(type, bindings, budget, 0);
+ GenericTypes.Key(closed, budget, requireClosed: true);
+ result.Add(closed);
+ }
+
+ return result.ToImmutable();
+ }
+
+ private static GenericFailure Invalid(string message) => new(GenericAnalysisStatus.InvalidInput, message);
+ }
+}
diff --git a/src/RustSharp.Semantics/GenericSubstitution.cs b/src/RustSharp.Semantics/GenericSubstitution.cs
new file mode 100644
index 0000000..7be0c49
--- /dev/null
+++ b/src/RustSharp.Semantics/GenericSubstitution.cs
@@ -0,0 +1,291 @@
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+
+namespace RustSharp.Semantics;
+
+/// Limits shared by every phase of one generic analysis operation.
+public sealed record GenericAnalysisLimits
+{
+ public int MaximumDepth { get; init; } = 64;
+ public int MaximumWork { get; init; } = 100_000;
+ public int MaximumItems { get; init; } = 4096;
+ public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
+}
+
+public enum GenericAnalysisStatus
+{
+ Complete,
+ NoMatch,
+ InvalidInput,
+ MissingImplementation,
+ OverlappingImplementations,
+ CyclicObligation,
+ LimitExceeded,
+}
+
+public sealed record GenericSubstitutionResult(
+ GenericAnalysisStatus Status,
+ RustType? Type,
+ ImmutableDictionary Bindings,
+ string? Diagnostic)
+{
+ public bool IsSuccess => Status == GenericAnalysisStatus.Complete;
+}
+
+/// Opt-in, bounded operations on the P0 structural generic type model.
+public static class GenericSubstitution
+{
+ /// Simultaneously substitutes parameters; replacements are not recursively substituted.
+ public static GenericSubstitutionResult Apply(
+ RustType type,
+ ImmutableDictionary bindings,
+ GenericAnalysisLimits? limits = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+ ArgumentNullException.ThrowIfNull(bindings);
+ var budget = new GenericBudget(limits, cancellationToken);
+ try
+ {
+ budget.Count(bindings.Count);
+ var ordinalBindings = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal);
+ foreach ((string name, RustType value) in bindings)
+ {
+ budget.Name(name);
+ GenericTypes.Key(value, budget);
+ if (!ordinalBindings.TryAdd(name, value))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput,
+ "Substitution parameters must be unique under ordinal name comparison.");
+ }
+ }
+
+ ImmutableDictionary normalizedBindings = ordinalBindings.ToImmutable();
+ RustType substituted = GenericTypes.Substitute(type, normalizedBindings, budget, 0);
+ // A replacement can increase total nesting beyond either input's depth.
+ GenericTypes.Key(substituted, budget);
+ return new(GenericAnalysisStatus.Complete, substituted, normalizedBindings, null);
+ }
+ catch (GenericFailure failure)
+ {
+ return new(failure.Status, null, ImmutableDictionary.Empty, failure.Message);
+ }
+ }
+
+ /// Matches a template to a closed type, preserving repeated-parameter equality.
+ public static GenericSubstitutionResult Match(
+ RustType template,
+ RustType closedType,
+ GenericAnalysisLimits? limits = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(template);
+ ArgumentNullException.ThrowIfNull(closedType);
+ var budget = new GenericBudget(limits, cancellationToken);
+ try
+ {
+ GenericTypes.Key(template, budget);
+ GenericTypes.Key(closedType, budget, requireClosed: true);
+ var bindings = new Dictionary(StringComparer.Ordinal);
+ if (!GenericTypes.Match(template, closedType, bindings, budget, 0))
+ {
+ return new(GenericAnalysisStatus.NoMatch, null, ImmutableDictionary.Empty,
+ "The closed type does not match every occurrence of the template parameters.");
+ }
+
+ return new(GenericAnalysisStatus.Complete, closedType, bindings.ToImmutableDictionary(StringComparer.Ordinal), null);
+ }
+ catch (GenericFailure failure)
+ {
+ return new(failure.Status, null, ImmutableDictionary.Empty, failure.Message);
+ }
+ }
+}
+
+internal sealed class GenericFailure(GenericAnalysisStatus status, string message) : Exception(message)
+{
+ public GenericAnalysisStatus Status { get; } = status;
+}
+
+internal sealed class GenericBudget
+{
+ private readonly long started = Stopwatch.GetTimestamp();
+ private readonly CancellationToken cancellationToken;
+ private int work;
+
+ public GenericBudget(GenericAnalysisLimits? limits, CancellationToken cancellationToken)
+ {
+ Limits = limits ?? new GenericAnalysisLimits();
+ if (Limits.MaximumDepth is < 1 or > 128 || Limits.MaximumWork is < 1 or > 1_000_000 ||
+ Limits.MaximumItems is < 1 or > 4096 || Limits.Timeout <= TimeSpan.Zero || Limits.Timeout > TimeSpan.FromMinutes(1))
+ {
+ throw new ArgumentOutOfRangeException(nameof(limits), "Generic analysis requires finite positive depth, work, item and time limits.");
+ }
+
+ this.cancellationToken = cancellationToken;
+ }
+
+ public GenericAnalysisLimits Limits { get; }
+
+ public void Step(int depth = 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (++work > Limits.MaximumWork || depth > Limits.MaximumDepth || Stopwatch.GetElapsedTime(started) >= Limits.Timeout)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.LimitExceeded, "Generic analysis exceeded its depth, work or elapsed-time budget.");
+ }
+ }
+
+ public void Count(int count)
+ {
+ Step();
+ if (count < 0)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "Generic collections must be initialized.");
+ }
+
+ if (count > Limits.MaximumItems)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.LimitExceeded, "Generic analysis exceeded its item budget.");
+ }
+ }
+
+ public void Name(string? name)
+ {
+ Step();
+ if (name is null || name.Length is < 1 or > 1024 || string.IsNullOrWhiteSpace(name))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "Generic identities must contain 1 to 1024 nonblank characters.");
+ }
+ }
+
+ public HashSet Parameters(ImmutableArray parameters)
+ {
+ Count(parameters.IsDefault ? -1 : parameters.Length);
+ if (parameters.Length > TraitSolver.MaximumTypeArguments)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "A generic declaration supports at most 16 type parameters.");
+ }
+
+ var names = new HashSet(StringComparer.Ordinal);
+ foreach (string parameter in parameters)
+ {
+ Name(parameter);
+ if (!names.Add(parameter))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "Generic parameter names must be distinct.");
+ }
+ }
+
+ return names;
+ }
+}
+
+internal static class GenericTypes
+{
+ public static string Key(RustType type, GenericBudget budget, bool requireClosed = false, HashSet? parameters = null)
+ {
+ var builder = new StringBuilder();
+ Append(type, 0);
+ if (builder.Length > 65_536)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.LimitExceeded, "A canonical type identity exceeds 65536 characters.");
+ }
+
+ return builder.ToString();
+
+ void Append(RustType current, int depth)
+ {
+ budget.Step(depth);
+ if (current is null)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "A type argument must not be null.");
+ }
+
+ budget.Name(current.Name);
+ if (builder.Length + current.Name.Length + 32 > 65_536)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.LimitExceeded, "A canonical type identity exceeds 65536 characters.");
+ }
+ if (current.Kind == RustTypeKind.Parameter && (requireClosed || parameters is not null && !parameters.Contains(current.Name)))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "A type contains an unbound or undeclared parameter.");
+ }
+
+ builder.Append((int)current.Kind).Append(':').Append(current.Name.Length.ToString(CultureInfo.InvariantCulture))
+ .Append(':').Append(current.Name).Append('[');
+ budget.Count(current.Arguments.Length);
+ foreach (RustType argument in current.Arguments)
+ {
+ Append(argument, depth + 1);
+ }
+
+ builder.Append(']');
+ }
+ }
+
+ public static RustType Substitute(RustType type, IReadOnlyDictionary bindings, GenericBudget budget, int depth)
+ {
+ budget.Step(depth);
+ if (type is null)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "A type argument must not be null.");
+ }
+
+ budget.Name(type.Name);
+ if (type.Kind == RustTypeKind.Parameter)
+ {
+ return bindings.TryGetValue(type.Name, out RustType? replacement) ? replacement : type;
+ }
+
+ budget.Count(type.Arguments.Length);
+ if (type.Arguments.IsEmpty) return type;
+ var arguments = new RustType[type.Arguments.Length];
+ for (int index = 0; index < arguments.Length; index++)
+ {
+ arguments[index] = Substitute(type.Arguments[index], bindings, budget, depth + 1);
+ }
+
+ return RustType.Named(type.Name, arguments);
+ }
+
+ public static bool Match(RustType template, RustType actual, Dictionary bindings, GenericBudget budget, int depth)
+ {
+ budget.Step(depth);
+ if (template.Kind == RustTypeKind.Parameter)
+ {
+ if (bindings.TryGetValue(template.Name, out RustType? previous))
+ {
+ return Equal(previous, actual, budget, depth + 1);
+ }
+
+ budget.Count(bindings.Count + 1);
+ bindings.Add(template.Name, actual);
+ return true;
+ }
+
+ if (template.Kind != actual.Kind || !string.Equals(template.Name, actual.Name, StringComparison.Ordinal) ||
+ template.Arguments.Length != actual.Arguments.Length) return false;
+ for (int index = 0; index < template.Arguments.Length; index++)
+ {
+ if (!Match(template.Arguments[index], actual.Arguments[index], bindings, budget, depth + 1)) return false;
+ }
+
+ return true;
+ }
+
+ private static bool Equal(RustType left, RustType right, GenericBudget budget, int depth)
+ {
+ budget.Step(depth);
+ if (left.Kind != right.Kind || !string.Equals(left.Name, right.Name, StringComparison.Ordinal) ||
+ left.Arguments.Length != right.Arguments.Length) return false;
+ for (int index = 0; index < left.Arguments.Length; index++)
+ {
+ if (!Equal(left.Arguments[index], right.Arguments[index], budget, depth + 1)) return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/RustSharp.Semantics/GenericTraitSolver.cs b/src/RustSharp.Semantics/GenericTraitSolver.cs
new file mode 100644
index 0000000..3b167ec
--- /dev/null
+++ b/src/RustSharp.Semantics/GenericTraitSolver.cs
@@ -0,0 +1,253 @@
+using System.Collections.Immutable;
+
+namespace RustSharp.Semantics;
+
+public sealed record GenericTraitObligation(string Trait, RustType Target);
+
+public sealed record GenericTraitImplementation(
+ string Id,
+ string Trait,
+ RustType Target,
+ ImmutableArray Parameters,
+ ImmutableArray Bounds);
+
+public sealed record GenericTraitResolutionResult(
+ GenericAnalysisStatus Status,
+ string? ImplementationId,
+ ImmutableArray SelectedImplementations,
+ string? Diagnostic)
+{
+ public bool IsSuccess => Status == GenericAnalysisStatus.Complete;
+}
+
+///
+/// A conservative, closed-world trait subset. Overlapping heads are rejected before
+/// solving; bounds never imply specialization or permit otherwise overlapping impls.
+///
+public sealed class GenericTraitSolver(ImmutableArray implementations)
+{
+ public const string ProfileId = "bounded-traits-v1";
+
+ public GenericTraitResolutionResult CheckCoherence(
+ GenericAnalysisLimits? limits = null,
+ CancellationToken cancellationToken = default)
+ {
+ var budget = new GenericBudget(limits, cancellationToken);
+ try
+ {
+ CreateSession(budget);
+ return new(GenericAnalysisStatus.Complete, null, [], null);
+ }
+ catch (GenericFailure failure)
+ {
+ return Failure(failure);
+ }
+ }
+
+ public GenericTraitResolutionResult Resolve(
+ GenericTraitObligation obligation,
+ GenericAnalysisLimits? limits = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(obligation);
+ var budget = new GenericBudget(limits, cancellationToken);
+ try
+ {
+ GenericTraitSession session = CreateSession(budget);
+ string implementation = session.Resolve(obligation, 0);
+ return new(GenericAnalysisStatus.Complete, implementation, session.Selected, null);
+ }
+ catch (GenericFailure failure)
+ {
+ return Failure(failure);
+ }
+ }
+
+ internal GenericTraitSession CreateSession(GenericBudget budget) => new(implementations, budget);
+
+ private static GenericTraitResolutionResult Failure(GenericFailure failure) =>
+ new(failure.Status, null, [], failure.Message);
+}
+
+internal sealed class GenericTraitSession
+{
+ private readonly GenericBudget budget;
+ private readonly ImmutableArray implementations;
+ private readonly Dictionary resolved = new(StringComparer.Ordinal);
+ private readonly HashSet active = new(StringComparer.Ordinal);
+ private readonly SortedSet selected = new(StringComparer.Ordinal);
+
+ public GenericTraitSession(ImmutableArray implementations, GenericBudget budget)
+ {
+ this.budget = budget;
+ budget.Count(implementations.IsDefault ? -1 : implementations.Length);
+ var sorted = new SortedDictionary(StringComparer.Ordinal);
+ foreach (GenericTraitImplementation implementation in implementations)
+ {
+ budget.Step();
+ if (implementation is null)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "A trait implementation must not be null.");
+ }
+
+ budget.Name(implementation.Id);
+ budget.Name(implementation.Trait);
+ HashSet parameters = budget.Parameters(implementation.Parameters);
+ GenericTypes.Key(implementation.Target, budget, parameters: parameters);
+ budget.Count(implementation.Bounds.IsDefault ? -1 : implementation.Bounds.Length);
+ foreach (GenericTraitObligation bound in implementation.Bounds)
+ {
+ ValidateObligation(bound, parameters);
+ }
+
+ // Every implementation parameter must be constrained by its head.
+ var headParameters = new HashSet(StringComparer.Ordinal);
+ CollectParameters(implementation.Target, headParameters, 0);
+ if (!parameters.SetEquals(headParameters))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "Implementation parameters must all occur in the target type.");
+ }
+
+ if (!sorted.TryAdd(implementation.Id, implementation))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "Implementation identifiers must be unique.");
+ }
+ }
+
+ this.implementations = [.. sorted.Values];
+ for (int left = 0; left < this.implementations.Length; left++)
+ {
+ for (int right = left + 1; right < this.implementations.Length; right++)
+ {
+ budget.Step();
+ GenericTraitImplementation a = this.implementations[left];
+ GenericTraitImplementation b = this.implementations[right];
+ if (string.Equals(a.Trait, b.Trait, StringComparison.Ordinal) && Overlap(a.Target, b.Target))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.OverlappingImplementations,
+ $"Trait '{a.Trait}' implementations '{a.Id}' and '{b.Id}' have overlapping heads; specialization is outside {GenericTraitSolver.ProfileId}.");
+ }
+ }
+ }
+ }
+
+ public ImmutableArray Selected => [.. selected];
+
+ public string Resolve(GenericTraitObligation obligation, int depth)
+ {
+ budget.Step(depth);
+ ValidateObligation(obligation, null);
+ string key = obligation.Trait.Length + ":" + obligation.Trait + GenericTypes.Key(obligation.Target, budget, requireClosed: true);
+ if (resolved.TryGetValue(key, out string? cached)) return cached;
+ budget.Count(active.Count + 1);
+ if (!active.Add(key))
+ {
+ throw new GenericFailure(GenericAnalysisStatus.CyclicObligation,
+ $"A recursive '{obligation.Trait}' obligation has no finite proof; coinductive solving is outside {GenericTraitSolver.ProfileId}.");
+ }
+
+ try
+ {
+ foreach (GenericTraitImplementation implementation in implementations)
+ {
+ budget.Step(depth);
+ if (!string.Equals(implementation.Trait, obligation.Trait, StringComparison.Ordinal)) continue;
+ var bindings = new Dictionary(StringComparer.Ordinal);
+ if (!GenericTypes.Match(implementation.Target, obligation.Target, bindings, budget, depth)) continue;
+ foreach (GenericTraitObligation bound in implementation.Bounds)
+ {
+ RustType target = GenericTypes.Substitute(bound.Target, bindings, budget, depth + 1);
+ Resolve(new(bound.Trait, target), depth + 1);
+ }
+
+ budget.Count(resolved.Count + 1);
+ resolved.Add(key, implementation.Id);
+ selected.Add(implementation.Id);
+ return implementation.Id;
+ }
+
+ throw new GenericFailure(GenericAnalysisStatus.MissingImplementation,
+ $"No implementation satisfies the closed '{obligation.Trait}' obligation.");
+ }
+ finally
+ {
+ active.Remove(key);
+ }
+ }
+
+ private void ValidateObligation(GenericTraitObligation obligation, HashSet? parameters)
+ {
+ budget.Step();
+ if (obligation is null)
+ {
+ throw new GenericFailure(GenericAnalysisStatus.InvalidInput, "A trait obligation must not be null.");
+ }
+
+ budget.Name(obligation.Trait);
+ GenericTypes.Key(obligation.Target, budget, parameters: parameters);
+ }
+
+ private void CollectParameters(RustType type, HashSet parameters, int depth)
+ {
+ budget.Step(depth);
+ if (type.Kind == RustTypeKind.Parameter) parameters.Add(type.Name);
+ foreach (RustType argument in type.Arguments) CollectParameters(argument, parameters, depth + 1);
+ }
+
+ private readonly record struct Term(RustType Type, int Side);
+
+ private bool Overlap(RustType left, RustType right)
+ {
+ var substitutions = new Dictionary<(int Side, string Name), Term>();
+ return Unify(new(left, 0), new(right, 1), 0);
+
+ Term Walk(Term term, int depth)
+ {
+ budget.Step(depth);
+ return term.Type.Kind == RustTypeKind.Parameter && substitutions.TryGetValue((term.Side, term.Type.Name), out Term replacement)
+ ? Walk(replacement, depth + 1) : term;
+ }
+
+ bool Occurs(Term variable, Term target, int depth)
+ {
+ budget.Step(depth);
+ target = Walk(target, depth);
+ if (target.Type.Kind == RustTypeKind.Parameter)
+ {
+ return variable.Side == target.Side && string.Equals(variable.Type.Name, target.Type.Name, StringComparison.Ordinal);
+ }
+
+ foreach (RustType argument in target.Type.Arguments)
+ {
+ if (Occurs(variable, new(argument, target.Side), depth + 1)) return true;
+ }
+
+ return false;
+ }
+
+ bool Unify(Term a, Term b, int depth)
+ {
+ budget.Step(depth);
+ a = Walk(a, depth);
+ b = Walk(b, depth);
+ if (a.Type.Kind == RustTypeKind.Parameter)
+ {
+ if (b.Type.Kind == RustTypeKind.Parameter && a.Side == b.Side && string.Equals(a.Type.Name, b.Type.Name, StringComparison.Ordinal)) return true;
+ if (Occurs(a, b, depth + 1)) return false;
+ budget.Count(substitutions.Count + 1);
+ substitutions.Add((a.Side, a.Type.Name), b);
+ return true;
+ }
+
+ if (b.Type.Kind == RustTypeKind.Parameter) return Unify(b, a, depth + 1);
+ if (a.Type.Kind != b.Type.Kind || !string.Equals(a.Type.Name, b.Type.Name, StringComparison.Ordinal) ||
+ a.Type.Arguments.Length != b.Type.Arguments.Length) return false;
+ for (int index = 0; index < a.Type.Arguments.Length; index++)
+ {
+ if (!Unify(new(a.Type.Arguments[index], a.Side), new(b.Type.Arguments[index], b.Side), depth + 1)) return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/tests/RustSharp.Tests/GenericFoundationTests.cs b/tests/RustSharp.Tests/GenericFoundationTests.cs
new file mode 100644
index 0000000..bc7144b
--- /dev/null
+++ b/tests/RustSharp.Tests/GenericFoundationTests.cs
@@ -0,0 +1,285 @@
+using System.Collections.Immutable;
+using System.Diagnostics;
+using RustSharp.Semantics;
+
+namespace RustSharp.Tests;
+
+internal static class GenericFoundationTests
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new("generic substitution preserves nested structure and simultaneous replacements", SubstitutionAsync),
+ new("generic substitution uses ordinal parameter identities", OrdinalBindingsAsync),
+ new("generic matching preserves repeated parameter equality without partial bindings", MatchingAsync),
+ new("generic substitution enforces composed result depth", SubstitutionDepthAsync),
+ new("generic traits resolve nested bounds and report missing evidence", TraitBoundsAsync),
+ new("generic traits preserve repeated parameters in implementation heads", RepeatedHeadAsync),
+ new("generic coherence rejects exact and generic overlap deterministically", CoherenceAsync),
+ new("generic coherence alpha renames implementations and checks infinite types", AlphaRenameAsync),
+ new("generic trait cycles require a finite proof", TraitCyclesAsync),
+ new("generic trait growth shares the recursive operation budget", TraitGrowthAsync),
+ new("generic plan closes signatures and deduplicates recursive reachability", ReachabilityAsync),
+ new("generic plan ordering is independent of roots definitions and calls", StablePlanAsync),
+ new("generic plan validates reachable trait obligations", PlanBoundsAsync),
+ new("generic plan bounds growing instance recursion without partial success", GrowingPlanAsync),
+ new("generic plan canonical identities cannot collide through display text", CanonicalIdentityAsync),
+ new("generic APIs diagnose malformed open and undeclared input", InvalidInputAsync),
+ new("generic analysis consistently enforces work time item and cancellation bounds", LimitsAsync),
+ ];
+
+ private static RustType T => RustType.Parameter("T");
+ private static RustType U => RustType.Parameter("U");
+ private static RustType Box(RustType type) => RustType.Named("Box", type);
+ private static RustType Pair(RustType left, RustType right) => RustType.Named("Pair", left, right);
+
+ private static Task SubstitutionAsync()
+ {
+ var bindings = ImmutableDictionary.Empty.Add("T", RustType.Bool).Add("U", RustType.I32);
+ GenericSubstitutionResult result = GenericSubstitution.Apply(Pair(Box(T), U), bindings);
+ AssertEx.True(result.IsSuccess, result.Diagnostic ?? "Substitution should succeed.");
+ AssertEx.Equal(Pair(Box(RustType.Bool), RustType.I32), result.Type!);
+ AssertEx.Equal(Pair(U, RustType.I32), GenericSubstitution.Apply(Pair(T, U),
+ ImmutableDictionary.Empty.Add("T", U).Add("U", RustType.I32)).Type!);
+ AssertEx.Equal(T, GenericSubstitution.Apply(T, ImmutableDictionary.Empty).Type!);
+ return Task.CompletedTask;
+ }
+
+ private static Task OrdinalBindingsAsync()
+ {
+ var bindings = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase).Add("T", RustType.I32);
+ GenericSubstitutionResult result = GenericSubstitution.Apply(RustType.Parameter("t"), bindings);
+ AssertEx.True(result.IsSuccess, "A foreign comparer must not change Rust identifiers.");
+ AssertEx.Equal(RustType.Parameter("t"), result.Type!);
+ AssertEx.False(result.Bindings.ContainsKey("t"), "Returned bindings must also use ordinal comparison.");
+ var duplicates = ImmutableDictionary.Create(ReferenceEqualityComparer.Instance)
+ .Add(new string('T', 1), RustType.I32)
+ .Add(new string('T', 1), RustType.Bool);
+ GenericSubstitutionResult duplicateResult = GenericSubstitution.Apply(T, duplicates);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, duplicateResult.Status);
+ AssertEx.Equal(0, duplicateResult.Bindings.Count);
+ AssertEx.True(duplicateResult.Type is null, "Ordinal duplicate input must return a diagnostic without partial substitution.");
+ return Task.CompletedTask;
+ }
+
+ private static Task MatchingAsync()
+ {
+ GenericSubstitutionResult yes = GenericSubstitution.Match(Pair(T, Box(T)), Pair(RustType.I32, Box(RustType.I32)));
+ AssertEx.True(yes.IsSuccess, "Repeated parameters with equal actual types must match.");
+ AssertEx.Equal(RustType.I32, yes.Bindings["T"]);
+ GenericSubstitutionResult no = GenericSubstitution.Match(Pair(T, T), Pair(RustType.I32, RustType.Bool));
+ AssertEx.Equal(GenericAnalysisStatus.NoMatch, no.Status);
+ AssertEx.Equal(0, no.Bindings.Count);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericSubstitution.Match(T, U).Status);
+ return Task.CompletedTask;
+ }
+
+ private static Task SubstitutionDepthAsync()
+ {
+ GenericSubstitutionResult result = GenericSubstitution.Apply(Box(Box(T)),
+ ImmutableDictionary.Empty.Add("T", Box(RustType.I32)), new() { MaximumDepth = 2 });
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, result.Status);
+ AssertEx.True(result.Type is null, "A rejected composed type must not escape as a usable result.");
+ using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+ var clock = Stopwatch.StartNew();
+ RustType longIdentity = RustType.Named(new string('L', 352));
+ string wrapperName = new('N', 1010);
+ for (int depth = 0; depth < 64; depth++)
+ {
+ cancellation.Token.ThrowIfCancellationRequested();
+ AssertEx.True(clock.Elapsed < TimeSpan.FromSeconds(5), "Canonical-identity fixture construction exceeded its deadline.");
+ longIdentity = RustType.Named(wrapperName, longIdentity);
+ }
+
+ // All node prefixes fit the per-node reserve, but pending closing brackets
+ // take the final identity beyond 65536 characters.
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded,
+ GenericSubstitution.Match(longIdentity, longIdentity, cancellationToken: cancellation.Token).Status);
+ return Task.CompletedTask;
+ }
+
+ private static GenericTraitSolver PrintableSolver() => new([
+ new("print_bool", "Print", RustType.Bool, [], []),
+ new("print_box", "Print", Box(T), ["T"], [new("Print", T)]),
+ ]);
+
+ private static Task TraitBoundsAsync()
+ {
+ GenericTraitSolver solver = PrintableSolver();
+ GenericTraitResolutionResult result = solver.Resolve(new("Print", Box(Box(RustType.Bool))));
+ AssertEx.True(result.IsSuccess, result.Diagnostic ?? "Nested bounds must resolve.");
+ AssertEx.Equal("print_box", result.ImplementationId!);
+ AssertEx.Equal("print_bool,print_box", string.Join(',', result.SelectedImplementations));
+ GenericTraitResolutionResult missing = solver.Resolve(new("Print", Box(RustType.I32)));
+ AssertEx.Equal(GenericAnalysisStatus.MissingImplementation, missing.Status);
+ AssertEx.Equal(0, missing.SelectedImplementations.Length);
+ return Task.CompletedTask;
+ }
+
+ private static Task RepeatedHeadAsync()
+ {
+ var solver = new GenericTraitSolver([new("same", "Same", Pair(T, T), ["T"], [])]);
+ AssertEx.True(solver.Resolve(new("Same", Pair(RustType.Bool, RustType.Bool))).IsSuccess, "Equal repeated arguments should resolve.");
+ AssertEx.Equal(GenericAnalysisStatus.MissingImplementation, solver.Resolve(new("Same", Pair(RustType.Bool, RustType.I32))).Status);
+ return Task.CompletedTask;
+ }
+
+ private static Task CoherenceAsync()
+ {
+ GenericTraitImplementation exact = new("a_exact", "Print", RustType.Bool, [], []);
+ GenericTraitImplementation blanket = new("z_blanket", "Print", T, ["T"], [new("Other", T)]);
+ var first = new GenericTraitSolver([exact, blanket]);
+ var second = new GenericTraitSolver([blanket, exact]);
+ AssertEx.Equal(GenericAnalysisStatus.OverlappingImplementations, first.CheckCoherence().Status);
+ AssertEx.Equal(first.CheckCoherence().Diagnostic!, second.CheckCoherence().Diagnostic!);
+ AssertEx.Equal(GenericAnalysisStatus.OverlappingImplementations, first.Resolve(new("Print", RustType.Bool)).Status);
+ return Task.CompletedTask;
+ }
+
+ private static Task AlphaRenameAsync()
+ {
+ var overlaps = new GenericTraitSolver([
+ new("left", "Trait", Pair(T, RustType.I32), ["T"], []),
+ new("right", "Trait", Pair(RustType.Bool, T), ["T"], []),
+ ]);
+ AssertEx.Equal(GenericAnalysisStatus.OverlappingImplementations, overlaps.CheckCoherence().Status);
+ var disjoint = new GenericTraitSolver([
+ new("same", "Trait", Pair(T, T), ["T"], []),
+ new("different", "Trait", Pair(RustType.I32, RustType.Bool), [], []),
+ ]);
+ AssertEx.True(disjoint.CheckCoherence().IsSuccess, "Repeated variables rule out the mismatched concrete pair.");
+ var occurs = new GenericTraitSolver([
+ new("same", "Trait", Pair(T, T), ["T"], []),
+ new("grows", "Trait", Pair(U, Box(U)), ["U"], []),
+ ]);
+ AssertEx.True(occurs.CheckCoherence().IsSuccess, "Only an infinite type could overlap these two heads.");
+ return Task.CompletedTask;
+ }
+
+ private static Task TraitCyclesAsync()
+ {
+ var solver = new GenericTraitSolver([
+ new("a", "A", T, ["T"], [new("B", T)]),
+ new("b", "B", T, ["T"], [new("A", T)]),
+ ]);
+ GenericTraitResolutionResult result = solver.Resolve(new("A", RustType.I32));
+ AssertEx.Equal(GenericAnalysisStatus.CyclicObligation, result.Status);
+ AssertEx.Equal(0, result.SelectedImplementations.Length);
+ return Task.CompletedTask;
+ }
+
+ private static Task TraitGrowthAsync()
+ {
+ var solver = new GenericTraitSolver([new("grow", "Grow", T, ["T"], [new("Grow", Box(T))])]);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded,
+ solver.Resolve(new("Grow", RustType.Bool), new() { MaximumDepth = 8 }).Status);
+ return Task.CompletedTask;
+ }
+
+ private static GenericFunctionDefinition Function(string name, ImmutableArray calls,
+ ImmutableArray bounds = default) =>
+ new(name, ["T"], [Box(T)], T, calls, bounds.IsDefault ? [] : bounds);
+
+ private static Task ReachabilityAsync()
+ {
+ GenericFunctionDefinition entry = Function("entry", [new("worker", [T]), new("worker", [T])]);
+ GenericFunctionDefinition worker = Function("worker", [new("worker", [T])]);
+ GenericFunctionDefinition dead = Function("dead", []);
+ GenericMonomorphizationResult result = GenericMonomorphization.Plan([entry, worker, dead], [new("entry", [RustType.Bool])]);
+ AssertEx.True(result.IsSuccess, result.Diagnostic ?? "Closed recursive instances must be finite.");
+ AssertEx.Equal(2, result.Instances.Length);
+ GenericMonomorphizedFunction closedEntry = result.Instances.Single(value => value.Instance.FunctionId == "entry");
+ AssertEx.Equal(Box(RustType.Bool), closedEntry.ParameterTypes[0]);
+ AssertEx.Equal(RustType.Bool, closedEntry.ReturnType);
+ AssertEx.Equal(1, closedEntry.Calls.Length);
+ AssertEx.Equal(RustType.Bool, closedEntry.Calls[0].Arguments[0]);
+ return Task.CompletedTask;
+ }
+
+ private static Task StablePlanAsync()
+ {
+ GenericFunctionDefinition a = Function("a", [new("b", [T]), new("b", [RustType.Bool])]);
+ GenericFunctionDefinition b = Function("b", []);
+ GenericMonomorphizationResult first = GenericMonomorphization.Plan([a, b], [new("a", [RustType.I32]), new("a", [RustType.Bool])]);
+ GenericMonomorphizationResult second = GenericMonomorphization.Plan([b, a with { Calls = [new("b", [RustType.Bool]), new("b", [T])] }],
+ [new("a", [RustType.Bool]), new("a", [RustType.I32]), new("a", [RustType.Bool])]);
+ AssertEx.True(first.IsSuccess && second.IsSuccess, "Reordered inputs must both yield plans.");
+ AssertEx.Equal(4, first.Instances.Length);
+ AssertEx.Equal(Describe(first), Describe(second));
+ return Task.CompletedTask;
+ }
+
+ private static string Describe(GenericMonomorphizationResult result) => string.Join(';', result.Instances.Select(value =>
+ value.Instance.FunctionId + "<" + string.Join(',', value.Instance.Arguments) + ">:" + value.ReturnType + ":" +
+ string.Join(',', value.Calls.Select(call => call.FunctionId + "<" + string.Join(',', call.Arguments) + ">"))));
+
+ private static Task PlanBoundsAsync()
+ {
+ GenericFunctionDefinition function = Function("display", [], [new("Print", T)]);
+ AssertEx.True(GenericMonomorphization.Plan([function], [new("display", [Box(RustType.Bool)])], PrintableSolver()).IsSuccess,
+ "A closed instance must check substituted bounds.");
+ GenericMonomorphizationResult failure = GenericMonomorphization.Plan([function], [new("display", [Box(RustType.I32)])], PrintableSolver());
+ AssertEx.Equal(GenericAnalysisStatus.MissingImplementation, failure.Status);
+ AssertEx.Equal(0, failure.Instances.Length);
+ return Task.CompletedTask;
+ }
+
+ private static Task GrowingPlanAsync()
+ {
+ GenericFunctionDefinition function = Function("grow", [new("grow", [Box(T)])]);
+ GenericMonomorphizationResult result = GenericMonomorphization.Plan([function], [new("grow", [RustType.I32])],
+ limits: new() { MaximumItems = 4 });
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, result.Status);
+ AssertEx.Equal(0, result.Instances.Length);
+ return Task.CompletedTask;
+ }
+
+ private static Task CanonicalIdentityAsync()
+ {
+ GenericFunctionDefinition function = Function("f", []);
+ GenericMonomorphizationResult result = GenericMonomorphization.Plan([function],
+ [new("f", [RustType.Named("X")]), new("f", [RustType.Named("X", RustType.Named("Y"))])]);
+ AssertEx.True(result.IsSuccess, "Structural keys must not use ambiguous display text.");
+ AssertEx.Equal(2, result.Instances.Length);
+ return Task.CompletedTask;
+ }
+
+ private static Task InvalidInputAsync()
+ {
+ GenericFunctionDefinition valid = Function("f", []);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan([valid], [new("f", [T])]).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan([valid], [new("f", [])]).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan([valid], [new("missing", [RustType.I32])]).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan([valid with { ReturnType = U }], []).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan([valid with { Calls = default }], []).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan([valid with { ReturnType = null! }], []).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericMonomorphization.Plan(default, []).Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, new GenericTraitSolver(default).CheckCoherence().Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, new GenericTraitSolver([null!]).CheckCoherence().Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, new GenericTraitSolver([new("bad", "Trait", T, ["T", "U"], [])]).CheckCoherence().Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, new GenericTraitSolver([new("bad", "Trait", T, ["T"], [new("Trait", U)])]).CheckCoherence().Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, new GenericTraitSolver([new("bad", "Trait", T, ["T", "T"], [])]).CheckCoherence().Status);
+ AssertEx.Equal(GenericAnalysisStatus.InvalidInput, GenericSubstitution.Match(RustType.Named("Null", [null!]), RustType.I32).Status);
+ return Task.CompletedTask;
+ }
+
+ private static Task LimitsAsync()
+ {
+ var work = new GenericAnalysisLimits { MaximumWork = 1 };
+ var time = new GenericAnalysisLimits { Timeout = TimeSpan.FromTicks(1) };
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, GenericSubstitution.Match(T, RustType.I32, work).Status);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, GenericSubstitution.Match(T, RustType.I32, time).Status);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, PrintableSolver().CheckCoherence(work).Status);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, PrintableSolver().Resolve(new("Print", RustType.Bool), time).Status);
+ GenericFunctionDefinition f = Function("f", []);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, GenericMonomorphization.Plan([f], [new("f", [RustType.I32])], limits: work).Status);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, GenericMonomorphization.Plan([f], [new("f", [RustType.I32])], limits: time).Status);
+ AssertEx.Equal(GenericAnalysisStatus.LimitExceeded, GenericMonomorphization.Plan([f, f with { Id = "g" }], [], limits: new() { MaximumItems = 1 }).Status);
+ using var cancellation = new CancellationTokenSource();
+ cancellation.Cancel();
+ AssertEx.Throws(() => GenericSubstitution.Match(T, RustType.I32, cancellationToken: cancellation.Token));
+ AssertEx.Throws(() => PrintableSolver().Resolve(new("Print", RustType.Bool), cancellationToken: cancellation.Token));
+ AssertEx.Throws(() => GenericMonomorphization.Plan([f], [], cancellationToken: cancellation.Token));
+ AssertEx.Throws(() => GenericSubstitution.Match(T, RustType.I32, new() { MaximumDepth = 0 }));
+ return Task.CompletedTask;
+ }
+}
diff --git a/tests/RustSharp.Tests/Program.cs b/tests/RustSharp.Tests/Program.cs
index e0e2772..f23906e 100644
--- a/tests/RustSharp.Tests/Program.cs
+++ b/tests/RustSharp.Tests/Program.cs
@@ -14,6 +14,7 @@ public static async Task Main(string[] args)
IReadOnlyList tests =
[.. SyntaxTests.All, .. LexerTests.All, .. LexerClosureTests.All, .. LexingManifestTests.All, .. SafeCoreSyntaxTests.All, .. SafeCoreTypeHirTests.All, .. SafeCoreTypeInferenceTests.All, .. SafeCoreTypeProfileTests.All, .. SafeCoreTypeAnalysisTests.All, .. SafeCoreTypeConformanceTests.All, .. SyntaxGrammarTests.All, .. SyntaxModuleExpansionTests.All, .. SyntaxItemExpansionTests.All, .. SyntaxExpressionExpansionTests.All, .. SyntaxProfileBoundaryTests.All, .. SemanticAstBoundaryTests.All, .. SyntaxManifestTests.All, .. NameResolutionManifestTests.All, .. SafeCoreNameResolutionTests.All, .. SafeCoreModuleResolutionTests.All, .. SafeCoreHirTests.All, .. SafeCoreCompilationTests.All, .. SafeCoreWorkspaceTests.All, .. CargoWorkspaceTests.All, .. SafeCoreModuleCompilationTests.All, .. WorkspaceSourceMapTests.All, .. EmissionTests.All, .. NativeAotTests.All, .. BoundedProcessTests.All, .. ClrLirTests.All, .. VerticalProofTests.All, .. OwnershipTests.All];
tests = [.. tests, .. SafeCoreAdvancedTypeHirTests.All, .. SafeCorePatternClosureTests.All, .. SafeCoreConstantTests.All];
+ tests = [.. tests, .. GenericFoundationTests.All];
if (tests.Count > MaximumTestCount)
{
Console.Error.WriteLine($"Test count {tests.Count} exceeds the safety limit {MaximumTestCount}.");
diff --git a/tools/RustSharp.Conformance/fixtures/syntax-expanded-closures.ast.txt b/tools/RustSharp.Conformance/fixtures/syntax-expanded-closures.ast.txt
index 56b56eb..8e9b421 100644
--- a/tools/RustSharp.Conformance/fixtures/syntax-expanded-closures.ast.txt
+++ b/tools/RustSharp.Conformance/fixtures/syntax-expanded-closures.ast.txt
@@ -1,4 +1,4 @@
-unit@0:62
+unit@0:61
function@0:60 name=Zg== const=0
visibility@0:0 kind=Private path=-
block@7:53
diff --git a/tools/RustSharp.Conformance/fixtures/syntax-expanded-expressions.ast.txt b/tools/RustSharp.Conformance/fixtures/syntax-expanded-expressions.ast.txt
index 4483906..4810d7d 100644
--- a/tools/RustSharp.Conformance/fixtures/syntax-expanded-expressions.ast.txt
+++ b/tools/RustSharp.Conformance/fixtures/syntax-expanded-expressions.ast.txt
@@ -1,4 +1,4 @@
-unit@0:112
+unit@0:111
function@0:110 name=Zg== const=0
visibility@0:0 kind=Private path=-
block@7:103
diff --git a/tools/RustSharp.Conformance/fixtures/syntax-expanded-modules.ast.txt b/tools/RustSharp.Conformance/fixtures/syntax-expanded-modules.ast.txt
index a5f8677..2b03650 100644
--- a/tools/RustSharp.Conformance/fixtures/syntax-expanded-modules.ast.txt
+++ b/tools/RustSharp.Conformance/fixtures/syntax-expanded-modules.ast.txt
@@ -1,4 +1,4 @@
-unit@0:149
+unit@0:148
use@0:75 path=OjpjcmF0ZV9uYW1l alias=-
visibility@0:0 kind=Private path=-
use-tree@4:70 kind=Group absolute=1 prefix=Y3JhdGVfbmFtZQ== alias=-
diff --git a/tools/RustSharp.Conformance/fixtures/syntax-expanded-qualified.ast.txt b/tools/RustSharp.Conformance/fixtures/syntax-expanded-qualified.ast.txt
index e14f6af..34861fb 100644
--- a/tools/RustSharp.Conformance/fixtures/syntax-expanded-qualified.ast.txt
+++ b/tools/RustSharp.Conformance/fixtures/syntax-expanded-qualified.ast.txt
@@ -1,4 +1,4 @@
-unit@0:68
+unit@0:67
function@0:66 name=Zg== const=0
visibility@0:0 kind=Private path=-
block@7:59