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