diff --git a/ROADMAP.md b/ROADMAP.md index 5276234..86d6258 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -430,7 +430,7 @@ publish warnings and no cleanup diagnostic. Both CoreCLR and Native AOT print | 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 | 🚧 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-06 | 🚧 In progress | Define typed MIR, CFG validation, desugaring, and source mapping. | P1-04 | `dotnet run --project tests/RustSharp.Tests -c Release --no-build --no-restore` | The first PR adds immutable typed MIR, bounded CFG/type validation, deterministic snapshots and scalar HIR lowering with source spans; see the [typed MIR contract](docs/typed-mir-profile.md). Aggregate/pattern/closure lowering, ownership integration and backend consumption remain subsequent batches. | | 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. | | P1-09 | 🚧 In progress | Emit safe-core programs through CLR LIR with Rust# cross-package metadata. | P0-07, P1-05, P1-08 | `rsc build tests/programs/safe-core/Cargo.toml` (future full gate; current primitive commands above) | Primitive multi-function IL/PDB emission is integrated. Generic/ownership lowering, cross-package metadata and separate consumer compilation remain open. | diff --git a/ROADMAP_zh.md b/ROADMAP_zh.md index 75dbdae..00c417e 100644 --- a/ROADMAP_zh.md +++ b/ROADMAP_zh.md @@ -355,7 +355,7 @@ AOT 探测器。这些工作流修改需要新的 CI 运行。此配置的 Linux | 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 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-06 | 🚧 进行中 | 定义类型化 MIR、CFG 验证、脱糖和源码映射。 | P1-04 | `dotnet run --project tests/RustSharp.Tests -c Release --no-build --no-restore` | 首个 PR 增加不可变类型化 MIR、有界 CFG/类型验证、确定性快照以及带源码范围的标量 HIR 降低;见[类型化 MIR 契约](docs/typed-mir-profile.md)。聚合/模式/闭包降低、所有权接入和后端消费保留为后续批次。 | | 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 上按指定顺序恰好运行一次析构函数。 | | P1-09 | 🚧 进行中 | 通过 CLR LIR 发出带有 Rust# 跨包元数据的安全核心程序。 | P0-07, P1-05, P1-08 | `rsc build tests/programs/safe-core/Cargo.toml`(未来完整门槛;当前基础类型命令见上文) | 基础类型多函数 IL/PDB 发射已接入。泛型/所有权降低、跨包元数据和独立消费者编译仍未完成。 | diff --git a/docs/typed-mir-profile.md b/docs/typed-mir-profile.md new file mode 100644 index 0000000..a93cb7e --- /dev/null +++ b/docs/typed-mir-profile.md @@ -0,0 +1,116 @@ +# Typed MIR: first P1-06 pull request + +Status: 🚧 In progress. The opt-in `safe-core-mir-v1` API establishes the scalar +HIR-to-MIR boundary. P1-06 remains open for aggregate, reference, closure, match, +const, move, and drop lowering and for later ownership and backend integration. + +This profile consumes successful `SafeCoreTypeAnalysisProgram` evidence from +P1-04. It does not change the existing primitive executable profile, the CLI, +or the IL emitter, and it does not make type-only programs executable. + +## Supported source subset + +Functions have unit, bool, char, integer, or floating-point parameters and return +values. A function may also return never (`!`). Scalar aliases, modules, and +resolved imports keep their existing HIR meaning. Calls resolve directly to a +function item in the same HIR document, including forward and recursive calls. +Function IDs follow declaration order; the function name is the type checker's +canonical item identity, including the `#value` namespace discriminator. + +Bodies support initialized identifier/wildcard bindings, shadowing, scalar local +assignment and compound assignment, unary numeric/boolean operators, binary +arithmetic/bitwise/comparison operators, scalar casts, blocks, return, `if` and +`else`, `loop`, `while`, unlabeled `break` and `continue`, loop values, +and short-circuit `&&`/`||`. Every local read is captured before evaluation of +subsequent operands, so `x + { x = 2; x }` and multi-argument calls preserve source +evaluation order. Compound assignment evaluates its right operand before reading +the destination scalar place. + +Aggregates, references, function-pointer values/indirect calls, closures, +`match`, destructuring, `let-else`, const items, and inline const blocks produce +`RSM2002` at the unsupported construct. Unreachable source tails after an +unconditional transfer are omitted after P1-04 has checked their types; they are +not represented as executable MIR or independently checked against this subset. +No unsupported construct is translated into a dummy value. + +Loop/control-flow labels remain outside the upstream P1-04 HIR gate and receive +`RSN1007` before MIR lowering. Internal loop contexts retain label information, +but this first source profile makes no labeled-control-flow support claim. + +## Representation and invariants + +`SafeCoreMirProgram` owns functions. A function owns typed locals and basic +blocks. Function, local, and block IDs are their stable collection indices. +Parameters precede other locals; user names and mutability are preserved and +temporary names are deterministic `tmpN` values. Constants store invariant +decimal integers, round-trip float text, bool words, decimal Unicode scalars for +char, or `()` for unit. Signed minimum literals, including grouped operands, are +represented as one negative constant instead of an out-of-range positive value. + +Statements assign an explicit typed rvalue to a local slot. Every block has +exactly one terminator: return, goto, boolean branch, direct call, or unreachable. +Calls have explicit result destinations and continuations. A diverging call has +no result and an unreachable continuation. Branch and loop results are assigned +on their incoming paths to a shared temporary. Short-circuit operators branch +before entering the right operand's block. Empty unreachable join blocks have +an explicit unreachable terminator. + +Each element carries `SafeCoreMirSource`: original source path, span, HIR node +ID, and HIR document extent (`Hir.Root.Span.End`). The extent can exclude trailing +trivia; it is not claimed to be the original text buffer length. Introduced +temporaries and control-flow blocks refer to the HIR expression that caused +their creation. + +`SafeCoreMirValidation.Validate` checks IDs, targets, source bounds, operand and +rvalue types, supported operations, function/call signatures, and return types. +It checks unreachable blocks too and reports reachable block IDs separately. +Its structural validation does not prove ownership, definite initialization, +termination, panic behavior, or executable backend correctness. Those properties +must not be inferred from a successful result. + +## Opt-in API + +```csharp +SafeCoreTypeAnalysisResult types = SafeCoreTypeAnalysis.Check(hir, + cancellationToken: cancellationToken); +if (!types.IsSuccessful) + return; + +SafeCoreMirLoweringResult lowered = SafeCoreMirLowering.Lower(types.Program!, + new SafeCoreMirLoweringOptions { Timeout = TimeSpan.FromSeconds(10) }, + cancellationToken); +if (lowered.IsSuccessful) +{ + // Every published program includes successful structural validation evidence. + string text = SafeCoreMirFormatting.Format(lowered.Program!); +} +``` + +`Lower` returns no partial program when lowering or validation fails. Stable +lowering diagnostics distinguish incomplete input evidence (`RSM2001`), +unsupported syntax (`RSM2002`), and exceeded bounds (`RSM2003`). Validation errors +retain their `RSM000x`/`RSM100x` codes and are also available in `Validation`. +Caller cancellation throws `OperationCanceledException`; invalid option values +throw `ArgumentOutOfRangeException`. + +The default lowering budget is ten seconds and one million operations, shared +with final validation. Independent size caps cover functions, per-function +blocks and locals, and nesting. MIR collection construction, validation, and +formatting also impose bounded size/time limits and accept cancellation. A +small configured limit rejects deterministically instead of publishing an +incomplete graph. + +## Verification + +`SafeCoreMirLoweringTests` parses real sources through HIR and P1-04 type analysis. +It checks deterministic typed snapshots, constants and HIR provenance, exact +unsupported spans, and work/depth/size/time/cancellation failures. A small, +independent test interpreter runs scalar control-flow graphs with a maximum of +4,096 total steps, 32 call levels, a five-second deadline, and cancellation. It +checks evaluation order, shadowing, direct calls, nested loops, branch joins, +early returns, and short-circuit behavior. + +`SafeCoreMirValidationTests` exercises constructed malformed MIR independently +of source lowering. Both test classes run with the repository test harness. +This first pull request provides no runtime or rustc differential conformance +claim for the new MIR representation. diff --git a/src/RustSharp.Semantics/SafeCoreMirFormatting.cs b/src/RustSharp.Semantics/SafeCoreMirFormatting.cs new file mode 100644 index 0000000..dce6338 --- /dev/null +++ b/src/RustSharp.Semantics/SafeCoreMirFormatting.cs @@ -0,0 +1,155 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text; + +namespace RustSharp.Semantics; + +public sealed record SafeCoreMirFormattingOptions +{ + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(10); + public CancellationToken CancellationToken { get; init; } + public int MaximumOperations { get; init; } = 1_000_000; + public int MaximumCharacters { get; init; } = 4_000_000; +} + +/// Versioned deterministic text with explicit IDs, types and original source evidence. +/// Inputs must first pass ; formatting does not +/// repeat structural validation or certify validity of manually constructed programs. +public static class SafeCoreMirFormatting +{ + public static string Format(SafeCoreMirProgram program, SafeCoreMirFormattingOptions? options = null) + { + ArgumentNullException.ThrowIfNull(program); + options ??= new(); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(options.Timeout, TimeSpan.Zero); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumOperations); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumCharacters); + return new Formatter(options).Format(program); + } + + private sealed class Formatter(SafeCoreMirFormattingOptions options) + { + private readonly StringBuilder _text = new(); + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private int _operations; + + public string Format(SafeCoreMirProgram program) + { + Add("safe-core-mir-v1\n"); + for (int index = 0; index < program.Functions.Count; index++) + { + Step(); + SafeCoreMirFunction function = program.Functions[index]; + Add(FormattableString.Invariant($"fn @{function.Id} {Escape(function.Name)} -> {function.ReturnType} entry bb{function.EntryBlockId} ")); + Source(function.Source); + Add(" {\n"); + for (int localIndex = 0; localIndex < function.Locals.Count; localIndex++) + { + Step(); + SafeCoreMirLocal local = function.Locals[localIndex]; + Add(FormattableString.Invariant($" let %{local.Id} {local.Kind.ToString().ToLowerInvariant()}{(local.IsMutable ? " mut" : "")} {Escape(local.Name)}: {local.Type} ")); + Source(local.Source); + Add("\n"); + } + for (int blockIndex = 0; blockIndex < function.Blocks.Count; blockIndex++) + { + Step(); + SafeCoreMirBlock block = function.Blocks[blockIndex]; + Add(FormattableString.Invariant($" bb{block.Id} ")); + Source(block.Source); + Add(":\n"); + for (int statementIndex = 0; statementIndex < block.Statements.Count; statementIndex++) + { + Step(); + SafeCoreMirStatement statement = block.Statements[statementIndex]; + SafeCoreMirRvalue value = statement.Value; + Add(FormattableString.Invariant($" %{statement.DestinationLocalId} = {value.Kind.ToString().ToLowerInvariant()}")); + if (value.Operator is not null) Add($" {Escape(value.Operator)}"); + Add("("); + Operands(value.Operands); + Add($"): {value.Type} "); + Source(statement.Source); + Add("\n"); + } + Terminator(block.Terminator); + } + Add("}\n"); + } + return _text.ToString(); + } + + private void Terminator(SafeCoreMirTerminator terminator) + { + Step(); + Add($" {terminator.Kind.ToString().ToLowerInvariant()}"); + if (terminator.Operand is not null) + { + Add(" "); + Operand(terminator.Operand); + } + if (terminator.Kind == SafeCoreMirTerminatorKind.Call) + { + Add("("); + Operands(terminator.Arguments); + Add(")"); + if (terminator.DestinationLocalId is int destination) Add(FormattableString.Invariant($" -> %{destination}")); + } + if (terminator.TargetBlockId >= 0) Add(FormattableString.Invariant($" bb{terminator.TargetBlockId}")); + if (terminator.FalseTargetBlockId >= 0) Add(FormattableString.Invariant($" else bb{terminator.FalseTargetBlockId}")); + Add(" "); + Source(terminator.Source); + Add("\n"); + } + + private void Operands(IReadOnlyList operands) + { + for (int index = 0; index < operands.Count; index++) + { + Step(); + if (index != 0) Add(", "); + Operand(operands[index]); + } + } + + private void Operand(SafeCoreMirOperand operand) + { + Step(); + Add(operand.Kind switch + { + SafeCoreMirOperandKind.Local => "%" + operand.Id.ToString(CultureInfo.InvariantCulture), + SafeCoreMirOperandKind.Function => "@" + operand.Id.ToString(CultureInfo.InvariantCulture), + _ => "const " + Escape(operand.Value ?? ""), + }); + Add($":{operand.Type}"); + } + + private void Source(SafeCoreMirSource source) => Add(FormattableString.Invariant( + $"[{Escape(source.SourcePath)}:{source.Span.Start}+{source.Span.Length}/{source.SourceLength} hir#{source.HirNodeId}]")); + + private string Escape(string value) + { + Step(); + if (value.Length > 262_144) throw new SafeCoreMirLimitException("MIR formatting input text limit reached."); + return value.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\r", "\\r", StringComparison.Ordinal) + .Replace("\n", "\\n", StringComparison.Ordinal).Replace("\t", "\\t", StringComparison.Ordinal) + .Replace("[", "\\[", StringComparison.Ordinal).Replace("]", "\\]", StringComparison.Ordinal); + } + + private void Add(string value) + { + Step(); + if ((long)_text.Length + value.Length > Math.Clamp(options.MaximumCharacters, 1, 4_000_000)) + throw new SafeCoreMirLimitException("MIR formatting character limit reached."); + _text.Append(value); + } + + private void Step() + { + options.CancellationToken.ThrowIfCancellationRequested(); + if (++_operations > Math.Clamp(options.MaximumOperations, 1, 1_000_000) + || _clock.Elapsed >= (options.Timeout > TimeSpan.Zero && options.Timeout <= TimeSpan.FromMinutes(1) + ? options.Timeout : TimeSpan.FromSeconds(10))) + throw new SafeCoreMirLimitException("MIR formatting work or time limit reached."); + } + } +} diff --git a/src/RustSharp.Semantics/SafeCoreMirLowering.cs b/src/RustSharp.Semantics/SafeCoreMirLowering.cs new file mode 100644 index 0000000..ab80a28 --- /dev/null +++ b/src/RustSharp.Semantics/SafeCoreMirLowering.cs @@ -0,0 +1,618 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Text; +using RustSharp.Syntax; +using K = RustSharp.Semantics.SafeCoreSemanticTypeKind; +using N = RustSharp.Semantics.SafeCoreHirNodeKind; + +namespace RustSharp.Semantics; + +/// Independent bounds for the opt-in scalar MIR lowering pass. +public sealed record SafeCoreMirLoweringOptions +{ + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(10); + public int MaximumOperations { get; init; } = 1_000_000; + public int MaximumNestingDepth { get; init; } = 128; + public int MaximumFunctions { get; init; } = 1_024; + public int MaximumBlocksPerFunction { get; init; } = 16_384; + public int MaximumLocalsPerFunction { get; init; } = 65_536; +} + +/// No partial program is published when lowering or validation fails. +public sealed record SafeCoreMirLoweringResult( + SafeCoreMirProgram? Program, + IReadOnlyList Diagnostics, + SafeCoreMirValidationResult? Validation, + bool IsTruncated) +{ + public bool IsSuccessful => Program is not null && Diagnostics.Count == 0 && + !IsTruncated && Validation is { IsSuccessful: true }; +} + +/// +/// Lowers resolved P1-04 evidence to the experimental scalar MIR profile. +/// This API does not change the existing compiler driver or IL emission path. +/// +public static class SafeCoreMirLowering +{ + public const string Profile = "safe-core-mir-v1"; + public const string InvalidEvidence = "RSM2001"; + public const string UnsupportedSyntax = "RSM2002"; + public const string LimitReached = "RSM2003"; + + public static SafeCoreMirLoweringResult Lower(SafeCoreTypeAnalysisProgram program, + SafeCoreMirLoweringOptions? options = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(program); + ArgumentNullException.ThrowIfNull(program.Hir); + ArgumentNullException.ThrowIfNull(program.Types); + ArgumentNullException.ThrowIfNull(program.Coercions); + cancellationToken.ThrowIfCancellationRequested(); + options ??= new(); + if (options.Timeout <= TimeSpan.Zero || options.Timeout > TimeSpan.FromMinutes(1) || + options.MaximumOperations is < 1 or > 4_000_000 || + options.MaximumNestingDepth is < 1 or > 128 || + options.MaximumFunctions is < 1 or > 4_096 || + options.MaximumBlocksPerFunction is < 1 or > 65_536 || + options.MaximumLocalsPerFunction is < 1 or > 262_144) + throw new ArgumentOutOfRangeException(nameof(options)); + if (!program.Hir.IsSuccessful) + return new(null, [new Diagnostic(InvalidEvidence, "MIR requires successful, resolved HIR type evidence.", + program.Hir.Root?.Span ?? new TextSpan(0, 0)) { SourcePath = program.Hir.SourcePath }], null, false); + try + { + var lowerer = new Lowerer(program, options, cancellationToken); + SafeCoreMirProgram mir = lowerer.Run(); + SafeCoreMirValidationResult validation = SafeCoreMirValidation.Validate(mir, + new() { CancellationToken = cancellationToken, Timeout = lowerer.Remaining, + MaximumOperations = lowerer.RemainingOperations }); + return validation.IsSuccessful + ? new(mir, [], validation, false) + : new(null, validation.Diagnostics.Select(d => new Diagnostic(d.Code, d.Message, + d.Source?.Span ?? program.Hir.Root!.Span) { SourcePath = d.Source?.SourcePath ?? program.Hir.SourcePath }).ToArray(), + validation, validation.IsTruncated); + } + catch (LoweringException exception) + { + return new(null, [exception.Diagnostic with { SourcePath = program.Hir.SourcePath }], null, + exception.Diagnostic.Code == LimitReached); + } + catch (SafeCoreMirLimitException) + { + return new(null, [new Diagnostic(LimitReached, "MIR construction exceeded its bounded collection limits.", + program.Hir.Root!.Span) { SourcePath = program.Hir.SourcePath }], null, true); + } + } + + private sealed class LoweringException(Diagnostic diagnostic) : Exception(diagnostic.Message) + { + public Diagnostic Diagnostic { get; } = diagnostic; + } + + private sealed class Lowerer(SafeCoreTypeAnalysisProgram input, SafeCoreMirLoweringOptions options, + CancellationToken cancellation) + { + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private readonly List _functionNodes = []; + private readonly Dictionary _functions = new(StringComparer.Ordinal); + private readonly List _locals = []; + private readonly Dictionary _bindings = []; + private readonly List _blocks = []; + private readonly List _loops = []; + private BlockBuilder? _current; + private int _operations; + + private sealed class BlockBuilder(int id, SafeCoreMirSource source) + { + public int Id { get; } = id; + public SafeCoreMirSource Source { get; } = source; + public List Statements { get; } = []; + public SafeCoreMirTerminator? Terminator { get; set; } + } + + private sealed class LoopContext(string? label, int header, int exit, int? result) + { + public string? Label { get; } = label; + public int Header { get; } = header; + public int Exit { get; } = exit; + public int? Result { get; } = result; + public bool HasBreak { get; set; } + } + + public TimeSpan Remaining + { + get + { + Step(input.Hir.Root!, 0); + TimeSpan remaining = options.Timeout - _clock.Elapsed; + if (remaining <= TimeSpan.Zero) Limit(input.Hir.Root!); + return remaining; + } + } + + public int RemainingOperations + { + get + { + if (_operations >= options.MaximumOperations) Limit(input.Hir.Root!); + return options.MaximumOperations - _operations; + } + } + + public SafeCoreMirProgram Run() + { + Collect(input.Hir.Root!, 0); + var functions = new List(_functionNodes.Count); + for (int index = 0; index < _functionNodes.Count; index++) + { + Step(_functionNodes[index], 0); + functions.Add(Function(_functionNodes[index], index)); + } + return new(functions, cancellation); + } + + private void Collect(SafeCoreHirNode node, int depth) + { + Step(node, depth); + if (node.Kind is N.CompilationUnit or N.Module) + { + for (int index = 0; index < node.ChildIds.Count; index++) Collect(Child(node, index), depth + 1); + return; + } + if (node.Kind is N.Attribute or N.Import or N.ImportGroup or N.TypeAlias) return; + if (node.Kind != N.Function) Unsupported(node); + if (_functionNodes.Count >= options.MaximumFunctions) Limit(node); + SafeCoreType signature = Type(node); + if (signature.Kind != K.Function || node.DeclaredSymbol is null) Invalid(node); + Scalar(signature.ReturnType, node, allowNever: true); + for (int index = 0; index < signature.ParameterTypes.Count; index++) + { + Step(node, depth); + Scalar(signature.ParameterTypes[index], node); + } + if (!_functions.TryAdd(SymbolKey(node.DeclaredSymbol!), _functionNodes.Count)) Invalid(node); + _functionNodes.Add(node); + } + + private SafeCoreMirFunction Function(SafeCoreHirNode node, int id) + { + _locals.Clear(); _bindings.Clear(); _blocks.Clear(); _loops.Clear(); + SafeCoreType signature = Type(node); + int parameterIndex = 0; + for (int index = 0; index < node.ChildIds.Count; index++) + { + SafeCoreHirNode child = Child(node, index); + Step(child, 0); + if (child.Kind != N.Parameter) continue; + SafeCoreHirNode pattern = UnwrapPattern(Child(child, 0)); + if (pattern.Kind is not (N.IdentifierPattern or N.WildcardPattern) || + pattern.Modifiers.HasFlag(SafeCoreHirNodeModifiers.ByReference)) Unsupported(pattern); + int local = Local(pattern.Name ?? $"arg{parameterIndex.ToString(CultureInfo.InvariantCulture)}", + signature.ParameterTypes[parameterIndex++], SafeCoreMirLocalKind.Parameter, + pattern.Modifiers.HasFlag(SafeCoreHirNodeModifiers.Mutable), pattern); + if (pattern.DeclaredSymbol is not null) _bindings.Add(pattern.DeclaredSymbol, local); + } + if (parameterIndex != signature.ParameterTypes.Count) Invalid(node); + _current = Block(node); + SafeCoreMirOperand? body = Expr(Child(node, node.ChildIds.Count - 1), 0); + if (_current is not null) + End(SafeCoreMirTerminator.Return(signature.ReturnType.Kind == K.Unit ? null : body, Source(node))); + var blocks = new List(_blocks.Count); + for (int index = 0; index < _blocks.Count; index++) + { + Step(node, 0); + BlockBuilder block = _blocks[index]; + blocks.Add(new(block.Id, block.Statements, + block.Terminator ?? SafeCoreMirTerminator.Unreachable(block.Source), block.Source, cancellation)); + } + return new(id, signature.Name!, signature.ReturnType, _locals.ToArray(), blocks, 0, Source(node), cancellation); + } + + private SafeCoreMirOperand? Expr(SafeCoreHirNode node, int depth) + { + Step(node, depth); + if (_current is null) return null; + SafeCoreMirOperand? value = ExprCore(node, depth); + if (value is not null && _current is not null && input.Coercions.TryGetValue(node.Id, out SafeCoreType? target) && + !value.Type.Equals(target)) + { + Scalar(target, node); + value = Emit(SafeCoreMirRvalue.Coerce(value, target, Source(node)), target, node); + } + return value; + } + + private SafeCoreMirOperand? ExprCore(SafeCoreHirNode node, int depth) + { + switch (node.Kind) + { + case N.Attribute: return Unit(node); + case N.Block: + SafeCoreMirOperand? result = Unit(node); + for (int index = 0; index < node.ChildIds.Count && _current is not null; index++) + result = Expr(Child(node, index), depth + 1); + return result; + case N.BlockExpression: return Expr(Child(node, 0), depth + 1); + case N.TupleExpression when node.ChildIds.Count == 0: return Unit(node); + case N.TupleExpression when node.ChildIds.Count == 1 && + !node.Modifiers.HasFlag(SafeCoreHirNodeModifiers.HasTrailingComma): + return Expr(Child(node, 0), depth + 1); + case N.ExpressionStatement: + _ = Expr(Child(node, 0), depth + 1); + return _current is null ? null : Unit(node); + case N.LetStatement: return Let(node, depth); + case N.LiteralExpression: return Literal(node, Type(node)); + case N.NameExpression: + int binding = -1; + if (node.ReferencedSymbol is null || !_bindings.TryGetValue(node.ReferencedSymbol, out binding)) Unsupported(node); + SafeCoreMirLocal local = _locals[binding]; + // Snapshot the read before a later operand can mutate the user local. + return Emit(SafeCoreMirRvalue.Use(SafeCoreMirOperand.Local(binding, local.Type, Source(node)), Source(node)), local.Type, node); + case N.UnaryExpression: + if (node.Value is not ("!" or "-")) Unsupported(node); + SafeCoreHirNode inner = UnwrapExpression(Child(node, 0), depth + 1); + if (node.Value == "-" && inner.Kind == N.LiteralExpression && Type(node).IsInteger) + return Literal(inner, Type(node), negate: true, origin: node); + SafeCoreMirOperand? operand = Expr(Child(node, 0), depth + 1); + return operand is null ? null : Emit(SafeCoreMirRvalue.Unary(node.Value!, operand, Type(node), Source(node)), Type(node), node); + case N.BinaryExpression: return Binary(node, depth); + case N.CastExpression: + SafeCoreMirOperand? cast = Expr(Child(node, 0), depth + 1); + Scalar(Type(node), node); + return cast is null ? null : Emit(SafeCoreMirRvalue.Cast(cast, Type(node), Source(node)), Type(node), node); + case N.CallExpression: return Call(node, depth); + case N.IfExpression: return If(node, depth); + case N.LoopExpression: + case N.WhileExpression: return Loop(node, depth); + case N.BreakExpression: + case N.ContinueExpression: return LoopControl(node, depth); + case N.ReturnExpression: + case N.ReturnStatement: + SafeCoreMirOperand? returned = node.ChildIds.Count == 0 ? null : Expr(Child(node, 0), depth + 1); + if (_current is not null) End(SafeCoreMirTerminator.Return(returned?.Type.Kind == K.Unit ? null : returned, Source(node))); + return null; + default: Unsupported(node); return null; + } + } + + private SafeCoreMirOperand? Let(SafeCoreHirNode node, int depth) + { + if (node.Modifiers.HasFlag(SafeCoreHirNodeModifiers.HasElse)) Unsupported(node); + SafeCoreHirNode pattern = UnwrapPattern(Child(node, 0)); + if (pattern.Kind is not (N.IdentifierPattern or N.WildcardPattern) || + pattern.Modifiers.HasFlag(SafeCoreHirNodeModifiers.ByReference)) Unsupported(pattern); + SafeCoreMirOperand? value = Expr(Child(node, node.ChildIds.Count - 1), depth + 1); + if (_current is null || value is null) return null; + if (pattern.Kind == N.WildcardPattern) return Unit(node); + if (pattern.DeclaredSymbol is null) Invalid(pattern); + int local = Local(pattern.Name!, Type(pattern), SafeCoreMirLocalKind.User, + pattern.Modifiers.HasFlag(SafeCoreHirNodeModifiers.Mutable), pattern); + _bindings.Add(pattern.DeclaredSymbol!, local); + Assign(local, value, node); + return Unit(node); + } + + private SafeCoreMirOperand? Binary(SafeCoreHirNode node, int depth) + { + string op = node.Value!; + if (op is "&&" or "||") return ShortCircuit(node, depth); + if (op is "=" or "+=" or "-=" or "*=" or "/=" or "%=" or "&=" or "|=" or "^=" or "<<=" or ">>=") + { + SafeCoreHirNode place = UnwrapExpression(Child(node, 0), depth + 1); + int destination = -1; + if (place.Kind != N.NameExpression || place.ReferencedSymbol is null || + !_bindings.TryGetValue(place.ReferencedSymbol, out destination)) Unsupported(place); + SafeCoreMirOperand? right = Expr(Child(node, 1), depth + 1); + if (right is null || _current is null) return null; + if (op != "=") + right = Emit(SafeCoreMirRvalue.Binary(op[..^1], + SafeCoreMirOperand.Local(destination, _locals[destination].Type, Source(place)), + right, _locals[destination].Type, Source(node)), _locals[destination].Type, node); + Assign(destination, right, node); + return Unit(node); + } + SafeCoreMirOperand? leftValue = Expr(Child(node, 0), depth + 1); + SafeCoreMirOperand? rightValue = Expr(Child(node, 1), depth + 1); + if (leftValue is null || rightValue is null || _current is null) return null; + return Emit(SafeCoreMirRvalue.Binary(op, leftValue, rightValue, Type(node), Source(node)), Type(node), node); + } + + private SafeCoreMirOperand? Call(SafeCoreHirNode node, int depth) + { + SafeCoreHirNode calleeNode = UnwrapExpression(Child(node, 0), depth + 1); + int function = -1; + if (calleeNode.Kind != N.NameExpression || calleeNode.ReferencedSymbol is null || + !_functions.TryGetValue(SymbolKey(calleeNode.ReferencedSymbol), out function)) Unsupported(calleeNode); + SafeCoreType signature = Type(_functionNodes[function]); + var arguments = new List(); + for (int index = 1; index < node.ChildIds.Count && _current is not null; index++) + { + SafeCoreMirOperand? argument = Expr(Child(node, index), depth + 1); + if (argument is not null) arguments.Add(argument); + } + if (_current is null) return null; + int? destination = signature.ReturnType.Kind is K.Unit or K.Never ? null : Temp(signature.ReturnType, node); + BlockBuilder continuation = Block(node); + End(SafeCoreMirTerminator.Call(SafeCoreMirOperand.Function(function, signature, Source(calleeNode)), + arguments, destination, continuation.Id, Source(node), cancellation)); + _current = continuation; + if (signature.ReturnType.Kind == K.Never) + { + End(SafeCoreMirTerminator.Unreachable(Source(node))); + return null; + } + return destination is int local ? SafeCoreMirOperand.Local(local, signature.ReturnType, Source(node)) : Unit(node); + } + + private SafeCoreMirOperand? If(SafeCoreHirNode node, int depth) + { + SafeCoreMirOperand? condition = Expr(Child(node, 0), depth + 1); + if (condition is null || _current is null) return null; + SafeCoreType type = EffectiveType(node); + int? destination = type.Kind is K.Unit or K.Never ? null : Temp(type, node); + BlockBuilder thenBlock = Block(Child(node, 1)); + BlockBuilder elseBlock = Block(node.ChildIds.Count == 3 ? Child(node, 2) : node); + BlockBuilder join = Block(node); + End(SafeCoreMirTerminator.Branch(condition, thenBlock.Id, elseBlock.Id, Source(node))); + _current = thenBlock; + SafeCoreMirOperand? then = Expr(Child(node, 1), depth + 1); + bool thenReturns = Join(destination, then, join, node); + _current = elseBlock; + SafeCoreMirOperand? other = node.ChildIds.Count == 3 ? Expr(Child(node, 2), depth + 1) : Unit(node); + bool elseReturns = Join(destination, other, join, node); + _current = thenReturns || elseReturns ? join : null; + return _current is null ? null : destination is int local ? SafeCoreMirOperand.Local(local, type, Source(node)) : Unit(node); + } + + private SafeCoreMirOperand? ShortCircuit(SafeCoreHirNode node, int depth) + { + SafeCoreMirOperand? left = Expr(Child(node, 0), depth + 1); + if (left is null || _current is null) return null; + int result = Temp(SafeCoreType.Primitive(K.Bool), node); + BlockBuilder rhs = Block(Child(node, 1)), shortcut = Block(node), join = Block(node); + bool and = node.Value == "&&"; + End(SafeCoreMirTerminator.Branch(left, and ? rhs.Id : shortcut.Id, and ? shortcut.Id : rhs.Id, Source(node))); + _current = shortcut; + Assign(result, SafeCoreMirOperand.Constant(SafeCoreType.Primitive(K.Bool), and ? "false" : "true", Source(node)), node); + End(SafeCoreMirTerminator.Goto(join.Id, Source(node))); + _current = rhs; + _ = Join(result, Expr(Child(node, 1), depth + 1), join, node); + _current = join; + return SafeCoreMirOperand.Local(result, SafeCoreType.Primitive(K.Bool), Source(node)); + } + + private SafeCoreMirOperand? Loop(SafeCoreHirNode node, int depth) + { + bool isWhile = node.Kind == N.WhileExpression; + SafeCoreType type = EffectiveType(node); + int? result = type.Kind is K.Unit or K.Never ? null : Temp(type, node); + BlockBuilder header = Block(node), body = Block(node), exit = Block(node); + End(SafeCoreMirTerminator.Goto(header.Id, Source(node))); + var context = new LoopContext(node.Name, header.Id, exit.Id, result); + _loops.Add(context); + _current = header; + if (isWhile) + { + SafeCoreMirOperand? condition = Expr(Child(node, 0), depth + 1); + if (_current is not null && condition is not null) + End(SafeCoreMirTerminator.Branch(condition, body.Id, exit.Id, Source(node))); + else + { + _loops.RemoveAt(_loops.Count - 1); + _current = context.HasBreak ? exit : null; + return _current is null ? null : Unit(node); + } + } + else End(SafeCoreMirTerminator.Goto(body.Id, Source(node))); + _current = body; + _ = Expr(Child(node, isWhile ? 1 : 0), depth + 1); + if (_current is not null) End(SafeCoreMirTerminator.Goto(header.Id, Source(node))); + _loops.RemoveAt(_loops.Count - 1); + _current = isWhile || context.HasBreak ? exit : null; + return _current is null ? null : result is int local ? SafeCoreMirOperand.Local(local, type, Source(node)) : Unit(node); + } + + private SafeCoreMirOperand? LoopControl(SafeCoreHirNode node, int depth) + { + LoopContext? context = null; + for (int index = _loops.Count - 1; index >= 0; index--) + { + Step(node, depth); + if (node.Name is null || node.Name == _loops[index].Label) { context = _loops[index]; break; } + } + if (context is null) Invalid(node); + if (node.Kind == N.ContinueExpression) + { + End(SafeCoreMirTerminator.Goto(context!.Header, Source(node))); + return null; + } + SafeCoreMirOperand? value = node.ChildIds.Count == 0 ? Unit(node) : Expr(Child(node, 0), depth + 1); + if (_current is null || value is null) return null; + if (context!.Result is int result) Assign(result, value, node); + context.HasBreak = true; + End(SafeCoreMirTerminator.Goto(context.Exit, Source(node))); + return null; + } + + private bool Join(int? destination, SafeCoreMirOperand? value, BlockBuilder join, SafeCoreHirNode node) + { + if (_current is null) return false; + if (destination is int local) + { + if (value is null) Invalid(node); + Assign(local, value!, node); + } + End(SafeCoreMirTerminator.Goto(join.Id, Source(node))); + return true; + } + + private SafeCoreMirOperand Literal(SafeCoreHirNode node, SafeCoreType type, bool negate = false, + SafeCoreHirNode? origin = null) + { + Scalar(type, node); + string text = node.Value ?? string.Empty; + if (text.Length is 0 or > 4_096) Unsupported(node); + string value; + if (type.Kind == K.Bool) value = text; + else if (type.Kind == K.Char || text.StartsWith("b'", StringComparison.Ordinal)) + { + string character = text[(type.Kind == K.Char ? 1 : 2)..^1]; + int scalar = character.StartsWith("\\u{", StringComparison.Ordinal) + ? int.Parse(character[3..^1].Replace("_", string.Empty, StringComparison.Ordinal), NumberStyles.HexNumber, CultureInfo.InvariantCulture) + : character.StartsWith("\\x", StringComparison.Ordinal) + ? int.Parse(character[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture) + : character switch + { + "\\n" => '\n', "\\r" => '\r', "\\t" => '\t', "\\0" => 0, + "\\\\" => '\\', "\\'" => '\'', "\\\"" => '"', + _ => Rune.GetRuneAt(character, 0).Value, + }; + value = scalar.ToString(CultureInfo.InvariantCulture); + } + else + { + string suffix = type.ToString(); + text = text.Replace("_", string.Empty, StringComparison.Ordinal); + if (text.EndsWith(suffix, StringComparison.Ordinal)) text = text[..^suffix.Length]; + if (type.IsFloat) + { + double number = double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + value = type.Kind == K.F32 ? ((float)number).ToString("R", CultureInfo.InvariantCulture) + : number.ToString("R", CultureInfo.InvariantCulture); + } + else + { + int radix = text.StartsWith("0x", StringComparison.Ordinal) ? 16 : + text.StartsWith("0o", StringComparison.Ordinal) ? 8 : text.StartsWith("0b", StringComparison.Ordinal) ? 2 : 10; + int start = radix == 10 ? 0 : 2; + BigInteger number = BigInteger.Zero; + for (int index = start; index < text.Length; index++) + { + Step(node, 0); + char c = char.ToLowerInvariant(text[index]); + int digit = c is >= 'a' and <= 'f' ? c - 'a' + 10 : c - '0'; + if (digit < 0 || digit >= radix) Invalid(node); + number = number * radix + digit; + } + value = (negate ? -number : number).ToString(CultureInfo.InvariantCulture); + } + } + return SafeCoreMirOperand.Constant(type, value, Source(origin ?? node)); + } + + private SafeCoreHirNode UnwrapExpression(SafeCoreHirNode node, int depth) + { + for (int index = 0; index <= options.MaximumNestingDepth; index++) + { + Step(node, depth + index); + if (node.Kind != N.TupleExpression || node.ChildIds.Count != 1 || + node.Modifiers.HasFlag(SafeCoreHirNodeModifiers.HasTrailingComma)) return node; + node = Child(node, 0); + } + Limit(node); return null!; + } + + private SafeCoreHirNode UnwrapPattern(SafeCoreHirNode node) + { + for (int depth = 0; depth <= options.MaximumNestingDepth; depth++) + { + Step(node, depth); + if (node.Kind != N.TuplePattern || node.ChildIds.Count != 1 || + node.Modifiers.HasFlag(SafeCoreHirNodeModifiers.HasTrailingComma)) return node; + node = Child(node, 0); + } + Limit(node); return null!; + } + + private SafeCoreMirOperand Emit(SafeCoreMirRvalue value, SafeCoreType type, SafeCoreHirNode node) + { + int local = Temp(type, node); + _current!.Statements.Add(new(local, value, Source(node))); + return SafeCoreMirOperand.Local(local, type, Source(node)); + } + + private void Assign(int local, SafeCoreMirOperand value, SafeCoreHirNode node) => + _current!.Statements.Add(new(local, SafeCoreMirRvalue.Use(value, Source(node)), Source(node))); + + private int Temp(SafeCoreType type, SafeCoreHirNode node) => Local( + $"tmp{_locals.Count.ToString(CultureInfo.InvariantCulture)}", type, SafeCoreMirLocalKind.Temporary, false, node); + + private int Local(string name, SafeCoreType type, SafeCoreMirLocalKind kind, bool mutable, SafeCoreHirNode node) + { + Step(node, 0); + Scalar(type, node); + if (_locals.Count >= options.MaximumLocalsPerFunction) Limit(node); + int id = _locals.Count; + _locals.Add(new(id, name, type, kind, mutable, Source(node))); + return id; + } + + private BlockBuilder Block(SafeCoreHirNode node) + { + Step(node, 0); + if (_blocks.Count >= options.MaximumBlocksPerFunction) Limit(node); + var block = new BlockBuilder(_blocks.Count, Source(node)); + _blocks.Add(block); + return block; + } + + private void End(SafeCoreMirTerminator terminator) + { + _current!.Terminator = terminator; + _current = null; + } + + private SafeCoreType EffectiveType(SafeCoreHirNode node) => + input.Coercions.TryGetValue(node.Id, out SafeCoreType? target) ? target : Type(node); + + private SafeCoreType Type(SafeCoreHirNode node) + { + if (!input.Types.TryGetValue(node.Id, out SafeCoreType? type) || type is null) Invalid(node); + return type!; + } + + private static void Scalar(SafeCoreType type, SafeCoreHirNode node, bool allowNever = false) + { + if (!(type.IsInteger || type.IsFloat || type.Kind is K.Unit or K.Bool or K.Char || allowNever && type.Kind == K.Never)) + Unsupported(node); + } + + private SafeCoreHirNode Child(SafeCoreHirNode node, int index) + { + if ((uint)index >= (uint)node.ChildIds.Count) Invalid(node); + int id = node.ChildIds[index]; + if ((uint)id >= (uint)input.Hir.Nodes.Count) Invalid(node); + return input.Hir.GetNode(id); + } + + private SafeCoreMirSource Source(SafeCoreHirNode node) => + new(input.Hir.SourcePath, node.Span, node.Id, input.Hir.Root!.Span.End); + + private SafeCoreMirOperand Unit(SafeCoreHirNode node) => + SafeCoreMirOperand.Constant(SafeCoreType.Primitive(K.Unit), "()", Source(node)); + + private static string SymbolKey(SafeCoreSymbol symbol) => + symbol.ResolvedImportTargetQualifiedName ?? symbol.QualifiedName; + + private void Step(SafeCoreHirNode node, int depth) + { + cancellation.ThrowIfCancellationRequested(); + if (++_operations > options.MaximumOperations || depth > options.MaximumNestingDepth || _clock.Elapsed >= options.Timeout) + Limit(node); + } + + [DoesNotReturn] + private static void Invalid(SafeCoreHirNode node) => throw new LoweringException(new(InvalidEvidence, + "MIR lowering requires complete and consistent resolved type evidence.", node.Span)); + + [DoesNotReturn] + private static void Unsupported(SafeCoreHirNode node) => throw new LoweringException(new(UnsupportedSyntax, + "This construct is outside the scalar MIR profile; aggregate, reference, closure, pattern and const lowering are not yet available.", node.Span)); + + [DoesNotReturn] + private static void Limit(SafeCoreHirNode node) => throw new LoweringException(new(LimitReached, + "MIR lowering exceeded its configured work, size, depth or time limit.", node.Span)); + } +} diff --git a/src/RustSharp.Semantics/SafeCoreMirModels.cs b/src/RustSharp.Semantics/SafeCoreMirModels.cs new file mode 100644 index 0000000..0453dd3 --- /dev/null +++ b/src/RustSharp.Semantics/SafeCoreMirModels.cs @@ -0,0 +1,177 @@ +using System.Diagnostics; +using RustSharp.Syntax; + +namespace RustSharp.Semantics; + +/// Original source evidence for a MIR element. IDs and offsets are nonnegative. +public sealed record SafeCoreMirSource(string SourcePath, TextSpan Span, int HirNodeId, int SourceLength); + +public enum SafeCoreMirLocalKind { Parameter, User, Temporary } +public enum SafeCoreMirOperandKind { Local, Constant, Function } +public enum SafeCoreMirRvalueKind { Use, Unary, Binary, Coerce, Cast, Tuple } +public enum SafeCoreMirTerminatorKind { Return, Goto, Branch, Call, Unreachable } + +/// A local slot. ID is its index in the owning function. Parameters precede other slots. +public sealed record SafeCoreMirLocal(int Id, string Name, SafeCoreType Type, + SafeCoreMirLocalKind Kind, bool IsMutable, SafeCoreMirSource Source); + +/// One immutable leaf operand. Constants use invariant decimal text, bool words, +/// decimal Unicode scalar values for char, or () for unit; they are not Rust source tokens. +public sealed record SafeCoreMirOperand(SafeCoreMirOperandKind Kind, SafeCoreType Type, + int Id, string? Value, SafeCoreMirSource Source) +{ + public static SafeCoreMirOperand Local(int localId, SafeCoreType type, SafeCoreMirSource source) => + new(SafeCoreMirOperandKind.Local, type, localId, null, source); + public static SafeCoreMirOperand Constant(SafeCoreType type, string value, SafeCoreMirSource source) => + new(SafeCoreMirOperandKind.Constant, type, -1, value, source); + public static SafeCoreMirOperand Function(int functionId, SafeCoreType type, SafeCoreMirSource source) => + new(SafeCoreMirOperandKind.Function, type, functionId, null, source); +} + +/// An explicit typed computation. Collections are copied with bounded indexed access. +public sealed class SafeCoreMirRvalue +{ + public SafeCoreMirRvalue(SafeCoreMirRvalueKind kind, SafeCoreType type, + IReadOnlyList operands, string? @operator, SafeCoreMirSource source, + CancellationToken cancellationToken = default) + { + Kind = kind; + Type = type; + Operands = SafeCoreMirCollections.Freeze(operands, cancellationToken); + Operator = @operator; + Source = source; + } + + public SafeCoreMirRvalueKind Kind { get; } + public SafeCoreType Type { get; } + public IReadOnlyList Operands { get; } + public string? Operator { get; } + public SafeCoreMirSource Source { get; } + public static SafeCoreMirRvalue Use(SafeCoreMirOperand operand, SafeCoreMirSource source) => + new(SafeCoreMirRvalueKind.Use, operand.Type, [operand], null, source); + public static SafeCoreMirRvalue Unary(string op, SafeCoreMirOperand operand, SafeCoreType resultType, SafeCoreMirSource source) => + new(SafeCoreMirRvalueKind.Unary, resultType, [operand], op, source); + public static SafeCoreMirRvalue Binary(string op, SafeCoreMirOperand left, SafeCoreMirOperand right, + SafeCoreType resultType, SafeCoreMirSource source) => + new(SafeCoreMirRvalueKind.Binary, resultType, [left, right], op, source); + public static SafeCoreMirRvalue Coerce(SafeCoreMirOperand operand, SafeCoreType resultType, SafeCoreMirSource source) => + new(SafeCoreMirRvalueKind.Coerce, resultType, [operand], null, source); + public static SafeCoreMirRvalue Cast(SafeCoreMirOperand operand, SafeCoreType resultType, SafeCoreMirSource source) => + new(SafeCoreMirRvalueKind.Cast, resultType, [operand], null, source); + public static SafeCoreMirRvalue Tuple(IReadOnlyList operands, SafeCoreType resultType, + SafeCoreMirSource source, CancellationToken cancellationToken = default) => + new(SafeCoreMirRvalueKind.Tuple, resultType, operands, null, source, cancellationToken); +} + +/// Assign a value to a local slot. Source binding mutability is checked by HIR; +/// MIR locals are storage slots and may be written on separate CFG paths. +public sealed record SafeCoreMirStatement(int DestinationLocalId, SafeCoreMirRvalue Value, SafeCoreMirSource Source); + +/// Exactly one explicit control-flow terminator ends every basic block. +/// Unused target IDs use -1; a diverging call uses continuation -1 and no destination. +public sealed class SafeCoreMirTerminator +{ + public SafeCoreMirTerminator(SafeCoreMirTerminatorKind kind, SafeCoreMirOperand? operand, + IReadOnlyList arguments, int? destinationLocalId, int targetBlockId, + int falseTargetBlockId, SafeCoreMirSource source, CancellationToken cancellationToken = default) + { + Kind = kind; + Operand = operand; + Arguments = SafeCoreMirCollections.Freeze(arguments, cancellationToken); + DestinationLocalId = destinationLocalId; + TargetBlockId = targetBlockId; + FalseTargetBlockId = falseTargetBlockId; + Source = source; + } + + public SafeCoreMirTerminatorKind Kind { get; } + public SafeCoreMirOperand? Operand { get; } + public IReadOnlyList Arguments { get; } + public int? DestinationLocalId { get; } + public int TargetBlockId { get; } + public int FalseTargetBlockId { get; } + public SafeCoreMirSource Source { get; } + public static SafeCoreMirTerminator Return(SafeCoreMirOperand? value, SafeCoreMirSource source) => + new(SafeCoreMirTerminatorKind.Return, value, [], null, -1, -1, source); + public static SafeCoreMirTerminator Goto(int target, SafeCoreMirSource source) => + new(SafeCoreMirTerminatorKind.Goto, null, [], null, target, -1, source); + public static SafeCoreMirTerminator Branch(SafeCoreMirOperand condition, int trueTarget, int falseTarget, SafeCoreMirSource source) => + new(SafeCoreMirTerminatorKind.Branch, condition, [], null, trueTarget, falseTarget, source); + public static SafeCoreMirTerminator Call(SafeCoreMirOperand callee, IReadOnlyList arguments, + int? destinationLocalId, int continuationBlockId, SafeCoreMirSource source, CancellationToken cancellationToken = default) => + new(SafeCoreMirTerminatorKind.Call, callee, arguments, destinationLocalId, continuationBlockId, -1, source, cancellationToken); + public static SafeCoreMirTerminator Unreachable(SafeCoreMirSource source) => + new(SafeCoreMirTerminatorKind.Unreachable, null, [], null, -1, -1, source); +} + +public sealed class SafeCoreMirBlock +{ + public SafeCoreMirBlock(int id, IReadOnlyList statements, + SafeCoreMirTerminator terminator, SafeCoreMirSource source, CancellationToken cancellationToken = default) + { + Id = id; + Statements = SafeCoreMirCollections.Freeze(statements, cancellationToken); + Terminator = terminator; + Source = source; + } + public int Id { get; } + public IReadOnlyList Statements { get; } + public SafeCoreMirTerminator Terminator { get; } + public SafeCoreMirSource Source { get; } +} + +public sealed class SafeCoreMirFunction +{ + public SafeCoreMirFunction(int id, string name, SafeCoreType returnType, + IReadOnlyList locals, IReadOnlyList blocks, int entryBlockId, + SafeCoreMirSource source, CancellationToken cancellationToken = default) + { + Id = id; + Name = name; + ReturnType = returnType; + Locals = SafeCoreMirCollections.Freeze(locals, cancellationToken); + Blocks = SafeCoreMirCollections.Freeze(blocks, cancellationToken); + EntryBlockId = entryBlockId; + Source = source; + } + public int Id { get; } + public string Name { get; } + public SafeCoreType ReturnType { get; } + public IReadOnlyList Locals { get; } + public IReadOnlyList Blocks { get; } + public int EntryBlockId { get; } + public SafeCoreMirSource Source { get; } +} + +/// Backend-independent typed MIR. IDs index immutable owning collections. +public sealed class SafeCoreMirProgram +{ + public SafeCoreMirProgram(IReadOnlyList functions, CancellationToken cancellationToken = default) => + Functions = SafeCoreMirCollections.Freeze(functions, cancellationToken); + public IReadOnlyList Functions { get; } +} + +public sealed class SafeCoreMirLimitException(string message) : Exception(message); + +internal static class SafeCoreMirCollections +{ + public static IReadOnlyList Freeze(IReadOnlyList items, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(items); + cancellationToken.ThrowIfCancellationRequested(); + var clock = Stopwatch.StartNew(); + int count = items.Count; + if (count is < 0 or > 100_000) throw new SafeCoreMirLimitException("MIR collection limit reached."); + var copy = new T[count]; + for (int index = 0; index < count; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + if (clock.Elapsed > TimeSpan.FromSeconds(10)) throw new SafeCoreMirLimitException("MIR construction timeout reached."); + copy[index] = items[index]; + ArgumentNullException.ThrowIfNull(copy[index]); + } + cancellationToken.ThrowIfCancellationRequested(); + if (clock.Elapsed > TimeSpan.FromSeconds(10)) throw new SafeCoreMirLimitException("MIR construction timeout reached."); + return Array.AsReadOnly(copy); + } +} diff --git a/src/RustSharp.Semantics/SafeCoreMirValidation.cs b/src/RustSharp.Semantics/SafeCoreMirValidation.cs new file mode 100644 index 0000000..117f36b --- /dev/null +++ b/src/RustSharp.Semantics/SafeCoreMirValidation.cs @@ -0,0 +1,500 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using System.Numerics; + +namespace RustSharp.Semantics; + +public sealed record SafeCoreMirValidationOptions +{ + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(10); + public CancellationToken CancellationToken { get; init; } + public int MaximumOperations { get; init; } = 1_000_000; + public int MaximumFunctions { get; init; } = 4_096; + public int MaximumLocals { get; init; } = 100_000; + public int MaximumBlocks { get; init; } = 100_000; + public int MaximumStatements { get; init; } = 100_000; + public int MaximumDiagnostics { get; init; } = 128; + public int MaximumTypeDepth { get; init; } = 128; +} + +public static class SafeCoreMirDiagnosticCodes +{ + public const string InvalidInput = "RSM0001"; + public const string LimitReached = "RSM0002"; + public const string InvalidControlFlow = "RSM1001"; + public const string TypeMismatch = "RSM1002"; + public const string InvalidSource = "RSM1003"; + public const string InvalidOperand = "RSM1004"; + public const string UnsupportedNode = "RSM1005"; +} + +public sealed record SafeCoreMirDiagnostic(string Code, string Message, SafeCoreMirSource? Source, + int FunctionId = -1, int BlockId = -1); + +public sealed class SafeCoreMirValidationResult +{ + internal SafeCoreMirValidationResult(List diagnostics, + Dictionary> reachableBlocks, bool isTruncated) + { + Diagnostics = diagnostics.AsReadOnly(); + ReachableBlocks = new ReadOnlyDictionary>(reachableBlocks); + IsTruncated = isTruncated; + } + public IReadOnlyList Diagnostics { get; } + public IReadOnlyDictionary> ReachableBlocks { get; } + public bool IsTruncated { get; } + public bool IsSuccessful => !IsTruncated && Diagnostics.Count == 0; +} + +/// Checks structural, source and type invariants, including unreachable blocks. +/// CFG cycles are legal. This is not ownership or definite-initialization analysis. +public static class SafeCoreMirValidation +{ + public static SafeCoreMirValidationResult Validate(SafeCoreMirProgram program, + SafeCoreMirValidationOptions? options = null) + { + ArgumentNullException.ThrowIfNull(program); + options ??= new(); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(options.Timeout, TimeSpan.Zero); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumOperations); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumFunctions); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumLocals); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumBlocks); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumStatements); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumDiagnostics); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.MaximumTypeDepth); + return new Validator(program, options).Run(); + } + + private sealed class Validator(SafeCoreMirProgram program, SafeCoreMirValidationOptions options) + { + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private readonly List _diagnostics = []; + private readonly Dictionary> _reachable = []; + private SafeCoreMirFunction? _function; + private SafeCoreMirBlock? _block; + private int _operations; + private int _locals; + private int _blocks; + private int _statements; + + public SafeCoreMirValidationResult Run() + { + try + { + Step(); + Limit(program.Functions.Count, options.MaximumFunctions, 4_096); + var names = new HashSet(StringComparer.Ordinal); + for (int index = 0; index < program.Functions.Count; index++) + { + Step(); + _function = program.Functions[index]; + _block = null; + Source(_function.Source); + if (_function.Id != index || string.IsNullOrEmpty(_function.Name) + || _function.Name.Length > 4_096 || !names.Add(_function.Name)) + Error(SafeCoreMirDiagnosticCodes.InvalidInput, "Function IDs must index the arena and names must be unique and bounded.", _function.Source); + if (Type(_function.ReturnType)) Function(); + } + return new(_diagnostics, _reachable, false); + } + catch (SafeCoreMirLimitException exception) + { + _diagnostics.Add(new(SafeCoreMirDiagnosticCodes.LimitReached, exception.Message, + _block?.Source ?? _function?.Source, _function?.Id ?? -1, _block?.Id ?? -1)); + return new(_diagnostics, _reachable, true); + } + } + + private void Step() + { + options.CancellationToken.ThrowIfCancellationRequested(); + if (++_operations > Math.Clamp(options.MaximumOperations, 1, 1_000_000) + || _clock.Elapsed >= (options.Timeout > TimeSpan.Zero && options.Timeout <= TimeSpan.FromMinutes(1) + ? options.Timeout : TimeSpan.FromSeconds(10))) + throw new SafeCoreMirLimitException("MIR validation work or time limit reached."); + } + + private static void Limit(int count, int requested, int ceiling) + { + if (count > Math.Clamp(requested, 1, ceiling)) throw new SafeCoreMirLimitException("MIR validation size limit reached."); + } + + private void Error(string code, string message, SafeCoreMirSource? source) + { + Step(); + if (_diagnostics.Count >= Math.Clamp(options.MaximumDiagnostics, 1, 1_024)) + throw new SafeCoreMirLimitException("MIR diagnostic limit reached."); + _diagnostics.Add(new(code, message, source, _function?.Id ?? -1, _block?.Id ?? -1)); + } + + private void Source(SafeCoreMirSource? source) + { + Step(); + if (source is null || string.IsNullOrWhiteSpace(source.SourcePath) || source.SourcePath.Length > 4_096 + || source.HirNodeId < 0 || source.SourceLength < 0 || source.Span.Start < 0 || source.Span.Length < 0 + || (long)source.Span.Start + source.Span.Length > source.SourceLength) + Error(SafeCoreMirDiagnosticCodes.InvalidSource, "MIR source evidence requires a path, HIR ID and a span within its source.", source); + } + + private bool Type(SafeCoreType? type, int depth = 0) + { + Step(); + if (depth >= Math.Clamp(options.MaximumTypeDepth, 1, 128)) throw new SafeCoreMirLimitException("MIR type nesting limit reached."); + if (type is null || type.Kind is SafeCoreSemanticTypeKind.Inference or SafeCoreSemanticTypeKind.Error) + { + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "MIR requires a fully resolved non-error type.", _block?.Source ?? _function?.Source); + return false; + } + bool valid = true; + for (int index = 0; index < type.Elements.Count; index++) valid &= Type(type.Elements[index], depth + 1); + return valid; + } + + private void Function() + { + SafeCoreMirFunction function = _function!; + _locals += function.Locals.Count; + _blocks += function.Blocks.Count; + Limit(_locals, options.MaximumLocals, 100_000); + Limit(_blocks, options.MaximumBlocks, 100_000); + bool pastParameters = false; + for (int index = 0; index < function.Locals.Count; index++) + { + Step(); + SafeCoreMirLocal local = function.Locals[index]; + Source(local.Source); + Type(local.Type); + if (local.Id != index || string.IsNullOrEmpty(local.Name) || local.Name.Length > 4_096 + || !Enum.IsDefined(local.Kind)) + Error(SafeCoreMirDiagnosticCodes.InvalidInput, "Local IDs must index the arena and local metadata must be valid.", local.Source); + if (local.Type?.Kind == SafeCoreSemanticTypeKind.Never) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "The never type cannot occupy a local slot.", local.Source); + if (local.Kind == SafeCoreMirLocalKind.Parameter && pastParameters) + Error(SafeCoreMirDiagnosticCodes.InvalidInput, "Parameter slots must precede other locals.", local.Source); + pastParameters |= local.Kind != SafeCoreMirLocalKind.Parameter; + } + Target(function.EntryBlockId, function.Source); + for (int index = 0; index < function.Blocks.Count; index++) + { + Step(); + _block = function.Blocks[index]; + Source(_block.Source); + if (_block.Id != index) Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Block IDs must index the function's block arena.", _block.Source); + _statements += _block.Statements.Count; + Limit(_statements, options.MaximumStatements, 100_000); + for (int statementIndex = 0; statementIndex < _block.Statements.Count; statementIndex++) + { + Step(); + SafeCoreMirStatement statement = _block.Statements[statementIndex]; + Source(statement.Source); + SafeCoreType? destination = LocalType(statement.DestinationLocalId, statement.Source); + if (statement.Value is null) + Error(SafeCoreMirDiagnosticCodes.InvalidInput, "An assignment requires a value.", statement.Source); + else + { + Rvalue(statement.Value); + Equal(destination, statement.Value.Type, statement.Source, "Assignment type differs from its destination."); + } + } + Terminator(_block.Terminator); + } + Reachability(); + } + + private SafeCoreType? LocalType(int id, SafeCoreMirSource? source) + { + Step(); + if (id < 0 || id >= _function!.Locals.Count) + { + Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "Local ID is outside the function's local arena.", source); + return null; + } + return _function.Locals[id].Type; + } + + private void Equal(SafeCoreType? expected, SafeCoreType? actual, SafeCoreMirSource? source, string message) + { + Step(); + if (expected is not null && actual is not null && expected != actual) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, message, source); + } + + private bool Operand(SafeCoreMirOperand operand) + { + Step(); + Source(operand.Source); + if (!Type(operand.Type)) return false; + switch (operand.Kind) + { + case SafeCoreMirOperandKind.Local: + Equal(LocalType(operand.Id, operand.Source), operand.Type, operand.Source, "Local operand type differs from its slot."); + if (operand.Value is not null) Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "A local operand cannot carry a constant payload.", operand.Source); + break; + case SafeCoreMirOperandKind.Function: + if (operand.Id < 0 || operand.Id >= program.Functions.Count) + Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "Function operand ID is outside the function arena.", operand.Source); + else + { + SafeCoreMirFunction target = program.Functions[operand.Id]; + int parameterCount = 0; + for (int index = 0; index < target.Locals.Count; index++) + { + Step(); + if (target.Locals[index].Kind != SafeCoreMirLocalKind.Parameter) break; + parameterCount++; + } + if (operand.Type.Kind != SafeCoreSemanticTypeKind.Function || operand.Type.Name != target.Name + || operand.Type.ParameterTypes.Count != parameterCount || operand.Type.ReturnType != target.ReturnType) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "Function operand does not match its declaration's nominal signature.", operand.Source); + else + for (int index = 0; index < parameterCount; index++) + Equal(target.Locals[index].Type, operand.Type.ParameterTypes[index], operand.Source, "Function parameter signature mismatch."); + } + if (operand.Value is not null) Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "A function operand cannot carry a constant payload.", operand.Source); + break; + case SafeCoreMirOperandKind.Constant: + if (operand.Id != -1 || !Constant(operand.Type, operand.Value)) + Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "Constant payload is invalid or out of range for its type.", operand.Source); + break; + default: + Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "Unknown operand kind.", operand.Source); + break; + } + return true; + } + + private static bool Constant(SafeCoreType type, string? text) + { + if (text is null || text.Length > 4_096) return false; + if (type.Kind == SafeCoreSemanticTypeKind.Unit) return text == "()"; + if (type.Kind == SafeCoreSemanticTypeKind.Bool) return text is "true" or "false"; + if (type.IsFloat) + return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value) + && double.IsFinite(value) && (type.Kind != SafeCoreSemanticTypeKind.F32 || float.IsFinite((float)value)); + if ((!type.IsInteger && type.Kind != SafeCoreSemanticTypeKind.Char) + || !BigInteger.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out BigInteger number)) return false; + if (type.Kind == SafeCoreSemanticTypeKind.Char) return number >= 0 && number <= 0x10ffff && (number < 0xd800 || number > 0xdfff); + int bits = type.Kind switch + { + SafeCoreSemanticTypeKind.I8 or SafeCoreSemanticTypeKind.U8 => 8, + SafeCoreSemanticTypeKind.I16 or SafeCoreSemanticTypeKind.U16 => 16, + SafeCoreSemanticTypeKind.I32 or SafeCoreSemanticTypeKind.U32 => 32, + SafeCoreSemanticTypeKind.I128 or SafeCoreSemanticTypeKind.U128 => 128, + _ => 64, + }; + bool signed = type.Kind is >= SafeCoreSemanticTypeKind.I8 and <= SafeCoreSemanticTypeKind.Isize; + BigInteger magnitude = BigInteger.One << (signed ? bits - 1 : bits); + return number >= (signed ? -magnitude : BigInteger.Zero) && number < magnitude; + } + + private void Rvalue(SafeCoreMirRvalue value) + { + Step(); + Source(value.Source); + if (!Type(value.Type)) return; + bool validOperands = true; + for (int index = 0; index < value.Operands.Count; index++) validOperands &= Operand(value.Operands[index]); + if (!validOperands) return; + int expectedCount = value.Kind switch + { + SafeCoreMirRvalueKind.Use or SafeCoreMirRvalueKind.Unary or SafeCoreMirRvalueKind.Coerce or SafeCoreMirRvalueKind.Cast => 1, + SafeCoreMirRvalueKind.Binary => 2, + SafeCoreMirRvalueKind.Tuple => value.Type.Kind == SafeCoreSemanticTypeKind.Unit ? 0 : value.Type.Elements.Count, + _ => -1, + }; + if (expectedCount < 0 || value.Operands.Count != expectedCount) + { + Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "Rvalue operand count or kind is invalid.", value.Source); + return; + } + if (value.Kind is not (SafeCoreMirRvalueKind.Unary or SafeCoreMirRvalueKind.Binary) && value.Operator is not null) + Error(SafeCoreMirDiagnosticCodes.InvalidOperand, "Only unary and binary computations carry an operator.", value.Source); + SafeCoreType? first = value.Operands.Count > 0 ? value.Operands[0].Type : null; + bool valid = value.Kind switch + { + SafeCoreMirRvalueKind.Use => first == value.Type, + SafeCoreMirRvalueKind.Unary => Unary(value.Operator, first!, value.Type), + SafeCoreMirRvalueKind.Binary => Binary(value.Operator, first!, value.Operands[1].Type, value.Type), + SafeCoreMirRvalueKind.Coerce => Coercion(first!, value.Type), + SafeCoreMirRvalueKind.Cast => Cast(first!, value.Type), + SafeCoreMirRvalueKind.Tuple => Tuple(value), + _ => false, + }; + if (!valid) Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "Rvalue operator and operand/result types are incompatible.", value.Source); + } + + private static bool Unary(string? op, SafeCoreType operand, SafeCoreType result) => result == operand && op switch + { + "-" => operand.IsFloat || operand.Kind is >= SafeCoreSemanticTypeKind.I8 and <= SafeCoreSemanticTypeKind.Isize, + "!" => operand.IsInteger || operand.Kind == SafeCoreSemanticTypeKind.Bool, + _ => false, + }; + + private static bool Binary(string? op, SafeCoreType left, SafeCoreType right, SafeCoreType result) => op switch + { + "+" or "-" or "*" or "/" or "%" => left == right && result == left && (left.IsInteger || left.IsFloat), + "&" or "|" or "^" => left == right && result == left && (left.IsInteger || left.Kind == SafeCoreSemanticTypeKind.Bool), + "<<" or ">>" => left.IsInteger && right.IsInteger && result == left, + "==" or "!=" or "<" or "<=" or ">" or ">=" => left == right && result.Kind == SafeCoreSemanticTypeKind.Bool + && (left.IsInteger || left.IsFloat || left.Kind is SafeCoreSemanticTypeKind.Bool or SafeCoreSemanticTypeKind.Char), + _ => false, + }; + + private bool Coercion(SafeCoreType from, SafeCoreType to) + { + Step(); + var inference = new SafeCoreTypeInference(new() + { + MaximumOperations = Math.Max(1, Math.Clamp(options.MaximumOperations, 1, 1_000_000) - _operations), + MaximumNestingDepth = Math.Clamp(options.MaximumTypeDepth, 1, 128), + Timeout = options.Timeout > TimeSpan.Zero ? options.Timeout : TimeSpan.FromSeconds(10), + CancellationToken = options.CancellationToken, + }); + try { return inference.Coerce(from, to); } + catch (SafeCoreTypeInferenceLimitException) { throw new SafeCoreMirLimitException("MIR coercion work limit reached."); } + } + + private static bool Cast(SafeCoreType from, SafeCoreType to) => + (from.IsInteger || from.IsFloat || from.Kind is SafeCoreSemanticTypeKind.Bool or SafeCoreSemanticTypeKind.Char) + && (to.IsInteger || to.IsFloat && (from.IsInteger || from.IsFloat) + || to.Kind == SafeCoreSemanticTypeKind.Char && from.Kind == SafeCoreSemanticTypeKind.U8); + + private bool Tuple(SafeCoreMirRvalue value) + { + if (value.Type.Kind == SafeCoreSemanticTypeKind.Unit) return value.Operands.Count == 0; + if (value.Type.Kind != SafeCoreSemanticTypeKind.Tuple) return false; + for (int index = 0; index < value.Operands.Count; index++) + { + Step(); + if (value.Operands[index].Type != value.Type.Elements[index]) return false; + } + return true; + } + + private bool Target(int id, SafeCoreMirSource source) + { + Step(); + if (id >= 0 && id < _function!.Blocks.Count) return true; + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Branch target is outside the function's block arena.", source); + return false; + } + + private void Terminator(SafeCoreMirTerminator? terminator) + { + Step(); + if (terminator is null) + { + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Every block requires an explicit terminator.", _block!.Source); + return; + } + Source(terminator.Source); + if (terminator.Operand is not null) Operand(terminator.Operand); + for (int index = 0; index < terminator.Arguments.Count; index++) Operand(terminator.Arguments[index]); + if (terminator.Kind != SafeCoreMirTerminatorKind.Call && (terminator.Arguments.Count != 0 || terminator.DestinationLocalId is not null)) + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Only call terminators may carry arguments or a destination.", terminator.Source); + if (terminator.Kind != SafeCoreMirTerminatorKind.Branch && terminator.FalseTargetBlockId != -1) + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Only branch terminators may carry a false target.", terminator.Source); + switch (terminator.Kind) + { + case SafeCoreMirTerminatorKind.Return: + if (terminator.TargetBlockId != -1) Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Return cannot have a successor.", terminator.Source); + if (_function!.ReturnType.Kind == SafeCoreSemanticTypeKind.Never) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "A diverging function cannot return.", terminator.Source); + else if (terminator.Operand is null) + { + if (_function.ReturnType.Kind != SafeCoreSemanticTypeKind.Unit) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "A non-unit return requires a value.", terminator.Source); + } + else Equal(_function.ReturnType, terminator.Operand.Type, terminator.Source, "Return type differs from the function result."); + break; + case SafeCoreMirTerminatorKind.Goto: + if (terminator.Operand is not null) Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Goto cannot carry an operand.", terminator.Source); + Target(terminator.TargetBlockId, terminator.Source); + break; + case SafeCoreMirTerminatorKind.Branch: + if (terminator.Operand?.Type?.Kind != SafeCoreSemanticTypeKind.Bool) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "A branch condition must be bool.", terminator.Source); + Target(terminator.TargetBlockId, terminator.Source); + Target(terminator.FalseTargetBlockId, terminator.Source); + break; + case SafeCoreMirTerminatorKind.Call: + Call(terminator); + break; + case SafeCoreMirTerminatorKind.Unreachable: + if (terminator.Operand is not null || terminator.TargetBlockId != -1) + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Unreachable cannot carry operands or successors.", terminator.Source); + break; + default: + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "Unknown terminator kind.", terminator.Source); + break; + } + } + + private void Call(SafeCoreMirTerminator terminator) + { + SafeCoreType? callee = terminator.Operand?.Type; + if (callee?.Kind != SafeCoreSemanticTypeKind.Function) + { + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "A call requires a typed function operand.", terminator.Source); + return; + } + if (callee.ParameterTypes.Count != terminator.Arguments.Count) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "Call argument count differs from the function signature.", terminator.Source); + else + for (int index = 0; index < terminator.Arguments.Count; index++) + Equal(callee.ParameterTypes[index], terminator.Arguments[index].Type, terminator.Arguments[index].Source, + "Call argument type differs from the function parameter; coercions must be explicit."); + if (terminator.DestinationLocalId is int destination) + { + Equal(LocalType(destination, terminator.Source), callee.ReturnType, terminator.Source, "Call destination type differs from the function result."); + if (callee.ReturnType.Kind == SafeCoreSemanticTypeKind.Never) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "A diverging call cannot initialize a destination.", terminator.Source); + } + else if (callee.ReturnType.Kind is not (SafeCoreSemanticTypeKind.Unit or SafeCoreSemanticTypeKind.Never)) + Error(SafeCoreMirDiagnosticCodes.TypeMismatch, "A value-returning call requires a destination, including discarded results.", terminator.Source); + if (callee.ReturnType.Kind == SafeCoreSemanticTypeKind.Never) + { + if (terminator.TargetBlockId != -1 && Target(terminator.TargetBlockId, terminator.Source) + && _function!.Blocks[terminator.TargetBlockId].Terminator?.Kind != SafeCoreMirTerminatorKind.Unreachable) + Error(SafeCoreMirDiagnosticCodes.InvalidControlFlow, "A diverging call cannot continue to executable control flow.", terminator.Source); + } + else Target(terminator.TargetBlockId, terminator.Source); + } + + private void Reachability() + { + SafeCoreMirFunction function = _function!; + var visited = new bool[function.Blocks.Count]; + var pending = new Queue(); + if (function.EntryBlockId >= 0 && function.EntryBlockId < visited.Length) pending.Enqueue(function.EntryBlockId); + for (int iterations = 0; pending.Count > 0 && iterations <= function.Blocks.Count * 2; iterations++) + { + Step(); + int id = pending.Dequeue(); + if (visited[id]) continue; + visited[id] = true; + SafeCoreMirTerminator? terminator = function.Blocks[id].Terminator; + if (terminator?.Kind is SafeCoreMirTerminatorKind.Goto or SafeCoreMirTerminatorKind.Branch + || terminator?.Kind == SafeCoreMirTerminatorKind.Call && terminator.Operand?.Type?.Kind == SafeCoreSemanticTypeKind.Function + && terminator.Operand.Type.ReturnType.Kind != SafeCoreSemanticTypeKind.Never) + { + Enqueue(terminator.TargetBlockId); + if (terminator.Kind == SafeCoreMirTerminatorKind.Branch) Enqueue(terminator.FalseTargetBlockId); + } + } + var reachable = new List(); + for (int index = 0; index < visited.Length; index++) + { + Step(); + if (visited[index]) reachable.Add(index); + } + _reachable[function.Id] = reachable.AsReadOnly(); + void Enqueue(int id) + { + if (id >= 0 && id < visited.Length && !visited[id]) pending.Enqueue(id); + } + } + } +} diff --git a/tests/RustSharp.Tests/Program.cs b/tests/RustSharp.Tests/Program.cs index f23906e..81bc1da 100644 --- a/tests/RustSharp.Tests/Program.cs +++ b/tests/RustSharp.Tests/Program.cs @@ -15,6 +15,7 @@ public static async Task Main(string[] args) [.. 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]; + tests = [.. tests, .. SafeCoreMirValidationTests.All, .. SafeCoreMirLoweringTests.All]; if (tests.Count > MaximumTestCount) { Console.Error.WriteLine($"Test count {tests.Count} exceeds the safety limit {MaximumTestCount}."); diff --git a/tests/RustSharp.Tests/SafeCoreMirLoweringTests.cs b/tests/RustSharp.Tests/SafeCoreMirLoweringTests.cs new file mode 100644 index 0000000..55303fe --- /dev/null +++ b/tests/RustSharp.Tests/SafeCoreMirLoweringTests.cs @@ -0,0 +1,268 @@ +using System.Diagnostics; +using System.Globalization; +using RustSharp.Semantics; +using RustSharp.Syntax; + +namespace RustSharp.Tests; + +internal static class SafeCoreMirLoweringTests +{ + public static IReadOnlyList All { get; } = + [ + new("MIR lowering produces deterministic typed three-address snapshots", SnapshotAsync), + new("MIR lowering preserves scalar literal spelling and original HIR evidence", LiteralsAsync), + new("MIR lowering preserves branches loops break continue and direct calls", ControlFlowAsync), + new("MIR lowering snapshots reads before later side effects", EvaluationOrderAsync), + new("MIR lowering short circuits and drops unreachable tails", DivergenceAsync), + new("MIR lowering rejects unsupported constructs at their original spans", UnsupportedAsync), + new("MIR lowering bounds work size nesting time and cancellation", LimitsAsync), + ]; + + private static Task SnapshotAsync() + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + SafeCoreTypeAnalysisProgram typed = Check("fn f() -> i32 { 1 + 2 }", cancellation.Token); + SafeCoreMirProgram first = Lower(typed, cancellation.Token); + SafeCoreMirProgram second = Lower(typed, cancellation.Token); + AssertEx.Equal(SafeCoreMirFormatting.Format(first), SafeCoreMirFormatting.Format(second)); + SafeCoreMirFunction function = first.Functions.Single(); + AssertEx.Equal("crate::f#value", function.Name); + AssertEx.Equal(1, function.Blocks.Count); + AssertEx.Equal(1, function.Locals.Count); + AssertEx.Equal("tmp0", function.Locals[0].Name); + AssertEx.Equal("i32", function.Locals[0].Type.ToString()); + SafeCoreMirStatement statement = function.Blocks[0].Statements.Single(); + AssertEx.Equal(SafeCoreMirRvalueKind.Binary, statement.Value.Kind); + AssertEx.Equal("+", statement.Value.Operator!); + AssertEx.Equal("1,2", string.Join(',', statement.Value.Operands.Select(o => o.Value))); + AssertEx.Equal(SafeCoreMirTerminatorKind.Return, function.Blocks[0].Terminator.Kind); + AssertEx.Equal(statement.DestinationLocalId, function.Blocks[0].Terminator.Operand!.Id); + return Task.CompletedTask; + } + + private static Task LiteralsAsync() + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + const string source = "fn a() -> i8 { -((128i8)) } fn b() -> i128 { -170141183460469231731687303715884105728i128 } " + + "fn c() -> u16 { 0xff_u16 } fn d() -> u16 { 0o17u16 } fn e() -> u16 { 0b1010u16 } " + + "fn f() -> f32 { 0.1f32 } fn g() -> char { '\\u{1_f980}' } fn h() -> u8 { b'\\xFF' }"; + SafeCoreTypeAnalysisProgram typed = Check(source, cancellation.Token); + SafeCoreMirProgram mir = Lower(typed, cancellation.Token); + AssertEx.Equal("-128,-170141183460469231731687303715884105728,255,15,10,0.1,129408,255", + string.Join(',', mir.Functions.Select(f => f.Blocks[0].Terminator.Operand!.Value))); + var clock = Stopwatch.StartNew(); + AssertEx.True(mir.Functions.Count <= 10, "The source-evidence audit is capped at ten functions."); + foreach (SafeCoreMirFunction function in mir.Functions) + { + cancellation.Token.ThrowIfCancellationRequested(); + AssertEx.True(clock.Elapsed < TimeSpan.FromSeconds(10), "Source audit timed out."); + SafeCoreMirSource original = function.Blocks[0].Terminator.Operand!.Source; + AssertEx.Equal("mir-lowering.rs", original.SourcePath); + AssertEx.Equal(typed.Hir.GetNode(original.HirNodeId).Span, original.Span); + AssertEx.True(original.Span.End <= original.SourceLength && original.SourceLength <= source.Length, + "MIR source provenance must stay inside the HIR document extent."); + } + return Task.CompletedTask; + } + + private static Task ControlFlowAsync() + { + Run("fn add(a: i32, b: i32) -> i32 { a + b } fn f() -> i32 { let mut x = 0; " + + "while x < 5 { x += 1; if x == 2 { continue; } if x == 4 { break; } } add(x, 3) }", 7L); + Run("fn f() -> i32 { let mut x = 0; loop { x += 1; if x == 3 { break x + 4; } } }", 7L); + Run("fn f() -> i32 { loop { let inner = loop { break 9; }; break inner; } }", 9L); + Run("fn f() -> i32 { while { return 6; true } {} 7 }", 6L); + Run("fn f() -> i32 { let x = if true { return 3; } else { 4 }; x }", 3L); + Run("mod m { pub fn add(x: i32) -> i32 { x + 2 } } use m::add as plus; fn f() -> i32 { plus(5) }", 7L); + return Task.CompletedTask; + } + + private static Task EvaluationOrderAsync() + { + Run("fn f() -> i32 { let mut x = 1; x + { x = 2; x } }", 3L); + Run("fn pair(a: i32, b: i32) -> i32 { a * 10 + b } fn f() -> i32 { let mut x = 1; pair(x, { x = 2; x }) }", 12L); + Run("fn f() -> i32 { let mut x = 1; x += { x = 2; 3 }; x }", 5L); + Run("fn f() -> i32 { let mut x = 1; { let x = 9; x; } x }", 1L); + return Task.CompletedTask; + } + + private static Task DivergenceAsync() + { + Run("fn f() -> i32 { let mut x = 0; false && { x = 1; true }; true || { x = 2; false }; x }", 0L); + Run("fn f() -> i32 { let mut x = 0; true && { x = 1; true }; false || { x = 2; false }; x }", 2L); + Run("fn f() -> i32 { false && { return 9; }; 1 }", 1L); + Run("fn f() -> i32 { loop { let x = { break 5; }; x; } }", 5L); + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + SafeCoreMirProgram mir = Lower(Check("fn unused() -> i32 { 2 } fn f() -> i32 { return 1; unused() }", cancellation.Token), cancellation.Token); + SafeCoreMirFunction f = mir.Functions.Single(f => f.Name == "crate::f#value"); + AssertEx.Equal(1, f.Blocks.Count); + AssertEx.Equal(0, f.Blocks[0].Statements.Count); + AssertEx.Equal("1", f.Blocks[0].Terminator.Operand!.Value!); + SafeCoreMirProgram never = Lower(Check("fn halt() -> ! { loop {} } fn f(flag: bool) -> i32 { if flag { halt() } else { 4 } }", cancellation.Token), cancellation.Token); + AssertEx.True(never.Functions[1].Blocks.Any(b => b.Terminator.Kind == SafeCoreMirTerminatorKind.Call), "A never-returning call must remain an explicit call terminator."); + AssertEx.True(never.Functions[1].Blocks.Any(b => b.Terminator.Kind == SafeCoreMirTerminatorKind.Unreachable), "The never continuation must terminate explicitly."); + return Task.CompletedTask; + } + + private static Task UnsupportedAsync() + { + (string Source, string Span)[] cases = + [ + ("fn f() { let x = [1, 2]; }", "[1, 2]"), + ("fn f() { let x = || 1; }", "|| 1"), + ("fn f() -> i32 { match true { true => 1, false => 2 } }", "match true { true => 1, false => 2 }"), + ("fn f() { let x = &1; }", "&1"), + ("fn f() { let x = (1, 2); }", "(1, 2)"), + ("const X: i32 = 1; fn f() -> i32 { X }", "const X: i32 = 1;"), + ("fn id() {} fn f() { let p = id; p(); }", "id"), + ]; + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + for (int index = 0; index < cases.Length; index++) + { + cancellation.Token.ThrowIfCancellationRequested(); + var item = cases[index]; + SafeCoreMirLoweringResult result = SafeCoreMirLowering.Lower(Check(item.Source, cancellation.Token), cancellationToken: cancellation.Token); + AssertEx.False(result.IsSuccessful, item.Source); + AssertEx.True(result.Program is null, "Rejected lowering must never expose a partial program."); + Diagnostic diagnostic = result.Diagnostics.Single(); + AssertEx.Equal(SafeCoreMirLowering.UnsupportedSyntax, diagnostic.Code); + AssertEx.Equal("mir-lowering.rs", diagnostic.SourcePath!); + AssertEx.Equal(item.Span, item.Source.Substring(diagnostic.Span.Start, diagnostic.Span.Length)); + } + return Task.CompletedTask; + } + + private static Task LimitsAsync() + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + SafeCoreTypeAnalysisProgram typed = Check("fn f(flag: bool) -> i32 { if flag { 1 + 2 } else { 3 } }", cancellation.Token); + SafeCoreMirLoweringOptions[] limits = + [ + new() { MaximumOperations = 1 }, new() { MaximumNestingDepth = 1 }, + new() { MaximumBlocksPerFunction = 1 }, new() { MaximumLocalsPerFunction = 1 }, + new() { Timeout = TimeSpan.FromTicks(1) }, + ]; + for (int index = 0; index < limits.Length; index++) + { + SafeCoreMirLoweringResult result = SafeCoreMirLowering.Lower(typed, limits[index], cancellation.Token); + AssertEx.True(result.IsTruncated && result.Program is null, "Each configured bound must reject without a partial program."); + AssertEx.Equal(SafeCoreMirLowering.LimitReached, result.Diagnostics.Single().Code); + } + var missing = typed with { Types = new Dictionary() }; + AssertEx.Equal(SafeCoreMirLowering.InvalidEvidence, + SafeCoreMirLowering.Lower(missing, cancellationToken: cancellation.Token).Diagnostics.Single().Code); + using var cancelled = new CancellationTokenSource(); + cancelled.Cancel(); + AssertEx.Throws(() => SafeCoreMirLowering.Lower(typed, cancellationToken: cancelled.Token)); + AssertEx.Throws(() => SafeCoreMirLowering.Lower(typed, new() { Timeout = TimeSpan.Zero })); + return Task.CompletedTask; + } + + private static SafeCoreTypeAnalysisProgram Check(string source, CancellationToken cancellation) + { + SafeCoreSyntaxResult syntax = SafeCoreSyntax.Parse(source, "mir-lowering.rs", new() { Timeout = TimeSpan.FromSeconds(5) }, cancellation); + AssertEx.True(syntax.IsSuccessful, $"Syntax: {source}\n{Format(syntax.Diagnostics)}"); + SafeCoreHirResult hir = SafeCoreHirLowering.Lower(syntax, new() + { + Timeout = TimeSpan.FromSeconds(5), CancellationToken = cancellation, + NameResolution = new() { EnableTypeSystemExtensions = true, Timeout = TimeSpan.FromSeconds(5) }, + }); + AssertEx.True(hir.IsSuccessful, $"HIR: {source}\n{Format(hir.Diagnostics)}"); + SafeCoreTypeAnalysisResult typed = SafeCoreTypeAnalysis.Check(hir, new() { Timeout = TimeSpan.FromSeconds(5) }, cancellation); + AssertEx.True(typed.IsSuccessful, $"{source}\n{Format(typed.Diagnostics)}"); + return typed.Program!; + } + + private static SafeCoreMirProgram Lower(SafeCoreTypeAnalysisProgram typed, CancellationToken cancellation) + { + SafeCoreMirLoweringResult result = SafeCoreMirLowering.Lower(typed, new() { Timeout = TimeSpan.FromSeconds(5) }, cancellation); + AssertEx.True(result.IsSuccessful, Format(result.Diagnostics)); + AssertEx.True(result.Validation is { IsSuccessful: true }, "Every successful lowering must contain explicit validator evidence."); + return result.Program!; + } + + private static string Format(IReadOnlyList diagnostics) => string.Join("; ", diagnostics.Select(d => $"{d.Code}: {d.Message}")); + + private static void Run(string source, long expected) + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + SafeCoreMirProgram mir = Lower(Check(source, cancellation.Token), cancellation.Token); + var interpreter = new ScalarInterpreter(mir, cancellation.Token); + AssertEx.Equal(expected, (long)interpreter.Invoke(mir.Functions.Single(f => f.Name == "crate::f#value").Id, [], 0)!, source); + } + + // Test-only executable semantics checks evaluation and branch behavior independently + // of how many temporary slots or empty join blocks the lowerer chooses to emit. + private sealed class ScalarInterpreter(SafeCoreMirProgram program, CancellationToken cancellation) + { + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private int _steps; + + public object? Invoke(int functionId, object?[] arguments, int depth) + { + AssertEx.True(depth <= 32, "MIR test call nesting exceeded 32."); + SafeCoreMirFunction function = program.Functions[functionId]; + var locals = new object?[function.Locals.Count]; + for (int index = 0; index < arguments.Length; index++) { Step(); locals[index] = arguments[index]; } + int blockId = function.EntryBlockId; + for (int iteration = 0; iteration < 4_096; iteration++) + { + Step(); + SafeCoreMirBlock block = function.Blocks[blockId]; + foreach (SafeCoreMirStatement statement in block.Statements) + { + Step(); + IReadOnlyList operands = statement.Value.Operands; + object? first = Read(operands[0], locals); + object? value = statement.Value.Kind switch + { + SafeCoreMirRvalueKind.Use or SafeCoreMirRvalueKind.Coerce => first, + SafeCoreMirRvalueKind.Binary => Binary(statement.Value.Operator!, first!, Read(operands[1], locals)!), + SafeCoreMirRvalueKind.Unary => statement.Value.Operator == "!" ? !(bool)first! : -(long)first!, + _ => throw new InvalidOperationException("Unexpected rvalue in bounded scalar test interpreter."), + }; + locals[statement.DestinationLocalId] = value; + } + SafeCoreMirTerminator terminator = block.Terminator; + switch (terminator.Kind) + { + case SafeCoreMirTerminatorKind.Return: return terminator.Operand is null ? null : Read(terminator.Operand, locals); + case SafeCoreMirTerminatorKind.Goto: blockId = terminator.TargetBlockId; break; + case SafeCoreMirTerminatorKind.Branch: + blockId = (bool)Read(terminator.Operand!, locals)! ? terminator.TargetBlockId : terminator.FalseTargetBlockId; break; + case SafeCoreMirTerminatorKind.Call: + object? result = Invoke(terminator.Operand!.Id, terminator.Arguments.Select(a => Read(a, locals)).ToArray(), depth + 1); + if (terminator.DestinationLocalId is int local) locals[local] = result; + blockId = terminator.TargetBlockId; break; + default: throw new InvalidOperationException("Test unexpectedly reached unreachable MIR."); + } + } + throw new InvalidOperationException("MIR test exceeded 4096 CFG steps."); + } + + private static object? Read(SafeCoreMirOperand operand, object?[] locals) => operand.Kind == SafeCoreMirOperandKind.Local + ? locals[operand.Id] ?? throw new InvalidOperationException("Test read an uninitialized MIR slot.") + : operand.Type.Kind switch + { + SafeCoreSemanticTypeKind.Unit => null, + SafeCoreSemanticTypeKind.Bool => bool.Parse(operand.Value!), + _ => long.Parse(operand.Value!, CultureInfo.InvariantCulture), + }; + + private static object Binary(string op, object left, object right) => op switch + { + "+" => (long)left + (long)right, "-" => (long)left - (long)right, + "*" => (long)left * (long)right, "/" => (long)left / (long)right, + "==" => left.Equals(right), "!=" => !left.Equals(right), + "<" => (long)left < (long)right, ">" => (long)left > (long)right, + "<=" => (long)left <= (long)right, ">=" => (long)left >= (long)right, + _ => throw new InvalidOperationException("Unexpected binary operator in MIR test."), + }; + + private void Step() + { + cancellation.ThrowIfCancellationRequested(); + AssertEx.True(++_steps <= 4_096 && _clock.Elapsed < TimeSpan.FromSeconds(5), "Bounded MIR interpreter exceeded its work or time budget."); + } + } +} diff --git a/tests/RustSharp.Tests/SafeCoreMirValidationTests.cs b/tests/RustSharp.Tests/SafeCoreMirValidationTests.cs new file mode 100644 index 0000000..0a8cae7 --- /dev/null +++ b/tests/RustSharp.Tests/SafeCoreMirValidationTests.cs @@ -0,0 +1,242 @@ +using System.Collections; +using System.Globalization; +using RustSharp.Semantics; +using RustSharp.Syntax; + +namespace RustSharp.Tests; + +internal static class SafeCoreMirValidationTests +{ + public static IReadOnlyList All { get; } = + [ + new("typed MIR preserves frozen collection ownership", FrozenInputsAsync), + new("typed MIR accepts cyclic CFG and reports deterministic reachability", CyclicFlowAsync), + new("typed MIR rejects negative and duplicate arena IDs", InvalidIdsAsync), + new("typed MIR validates dead blocks and explicit terminator shapes", DeadBlocksAsync), + new("typed MIR validates source evidence without integer overflow", SourceEvidenceAsync), + new("typed MIR enforces assignments branches and return types", ValueTypesAsync), + new("typed MIR checks scalar constant ranges and Unicode scalars", ConstantsAsync), + new("typed MIR checks nominal call signatures arguments and results", CallsAsync), + new("typed MIR keeps diverging calls terminal", DivergingCallsAsync), + new("typed MIR validates explicit coercions casts and tuple construction", ComputationsAsync), + new("typed MIR validation obeys cancellation work size diagnostic depth and time budgets", ValidationBudgetsAsync), + new("typed MIR formatter is invariant deterministic and bounded", FormattingAsync), + ]; + + private static readonly SafeCoreType Integer = SafeCoreType.Primitive(SafeCoreSemanticTypeKind.I32); + private static readonly SafeCoreType Boolean = SafeCoreType.Primitive(SafeCoreSemanticTypeKind.Bool); + private static readonly SafeCoreType Unit = SafeCoreType.Primitive(SafeCoreSemanticTypeKind.Unit); + private static readonly SafeCoreType Never = SafeCoreType.Primitive(SafeCoreSemanticTypeKind.Never); + private static readonly SafeCoreMirSource Source = new("sample.rs", new TextSpan(0, 12), 0, 12); + + private static SafeCoreMirOperand Number(string value = "1") => SafeCoreMirOperand.Constant(Integer, value, Source); + private static SafeCoreMirBlock ReturnBlock(int id = 0) => new(id, [], SafeCoreMirTerminator.Return(Number(), Source), Source); + private static SafeCoreMirFunction Function(IReadOnlyList blocks, + IReadOnlyList? locals = null, SafeCoreType? result = null, int entry = 0) => + new(0, "crate::main", result ?? Integer, locals ?? [], blocks, entry, Source); + private static SafeCoreMirProgram Program(params SafeCoreMirBlock[] blocks) => new([Function(blocks)]); + private static void Invalid(SafeCoreMirProgram program, string code) + { + SafeCoreMirValidationResult validation = SafeCoreMirValidation.Validate(program); + AssertEx.False(validation.IsSuccessful, "Malformed MIR must not pass validation."); + AssertEx.True(validation.Diagnostics.Any(diagnostic => diagnostic.Code == code), $"Expected MIR diagnostic {code}."); + } + + private static Task FrozenInputsAsync() + { + SafeCoreMirBlock[] blocks = [ReturnBlock()]; + var function = Function(blocks); + blocks[0] = new(7, [], SafeCoreMirTerminator.Goto(7, Source), Source); + SafeCoreMirFunction[] functions = [function]; + var program = new SafeCoreMirProgram(functions); + functions[0] = Function(blocks); + AssertEx.True(SafeCoreMirValidation.Validate(program).IsSuccessful, "Caller mutations cannot change a MIR snapshot."); + var indexed = new IndexOnlyList(function); + AssertEx.Equal(1, new SafeCoreMirProgram(indexed).Functions.Count); + AssertEx.Throws(() => _ = new SafeCoreMirProgram(new OversizedList())); + using var cancelled = new CancellationTokenSource(); + cancelled.Cancel(); + AssertEx.Throws(() => _ = new SafeCoreMirProgram(indexed, cancelled.Token)); + return Task.CompletedTask; + } + + private static Task CyclicFlowAsync() + { + SafeCoreMirProgram program = Program( + new(0, [], SafeCoreMirTerminator.Branch(SafeCoreMirOperand.Constant(Boolean, "true", Source), 1, 2, Source), Source), + new(1, [], SafeCoreMirTerminator.Goto(0, Source), Source), ReturnBlock(2), ReturnBlock(3)); + SafeCoreMirValidationResult result = SafeCoreMirValidation.Validate(program); + AssertEx.True(result.IsSuccessful, "CFG back edges are valid."); + AssertEx.Equal("0,1,2", string.Join(',', result.ReachableBlocks[0])); + return Task.CompletedTask; + } + + private static Task InvalidIdsAsync() + { + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Goto(-2, Source), Source)), SafeCoreMirDiagnosticCodes.InvalidControlFlow); + Invalid(Program(ReturnBlock(1)), SafeCoreMirDiagnosticCodes.InvalidControlFlow); + Invalid(new([Function([ReturnBlock()], entry: -1)]), SafeCoreMirDiagnosticCodes.InvalidControlFlow); + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(SafeCoreMirOperand.Local(-1, Integer, Source), Source), Source)), + SafeCoreMirDiagnosticCodes.InvalidOperand); + Invalid(new([Function([ReturnBlock()], [new(-1, "x", Integer, SafeCoreMirLocalKind.Parameter, false, Source)])]), + SafeCoreMirDiagnosticCodes.InvalidInput); + SafeCoreMirFunction duplicate = Function([ReturnBlock()]); + Invalid(new([duplicate, duplicate]), SafeCoreMirDiagnosticCodes.InvalidInput); + return Task.CompletedTask; + } + + private static Task DeadBlocksAsync() + { + Invalid(Program(ReturnBlock(), new(1, [], SafeCoreMirTerminator.Goto(99, Source), Source)), + SafeCoreMirDiagnosticCodes.InvalidControlFlow); + Invalid(Program(new SafeCoreMirBlock(0, [], null!, Source)), SafeCoreMirDiagnosticCodes.InvalidControlFlow); + Invalid(Program(new SafeCoreMirBlock(0, [], new(SafeCoreMirTerminatorKind.Return, Number(), [], null, 0, -1, Source), Source)), + SafeCoreMirDiagnosticCodes.InvalidControlFlow); + return Task.CompletedTask; + } + + private static Task SourceEvidenceAsync() + { + var overflow = new SafeCoreMirSource("bad.rs", new(int.MaxValue, int.MaxValue), 0, int.MaxValue); + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(Number(), overflow), Source)), SafeCoreMirDiagnosticCodes.InvalidSource); + var negative = Source with { HirNodeId = -1 }; + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(Number(), negative), Source)), SafeCoreMirDiagnosticCodes.InvalidSource); + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(Number(), Source with { SourcePath = "" }), Source)), + SafeCoreMirDiagnosticCodes.InvalidSource); + return Task.CompletedTask; + } + + private static Task ValueTypesAsync() + { + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Branch(Number(), 0, 0, Source), Source)), SafeCoreMirDiagnosticCodes.TypeMismatch); + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(null, Source), Source)), SafeCoreMirDiagnosticCodes.TypeMismatch); + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Return(null, Source), Source)], result: Never)]), SafeCoreMirDiagnosticCodes.TypeMismatch); + SafeCoreMirStatement statement = new(0, SafeCoreMirRvalue.Use(Number(), Source), Source); + Invalid(new([Function([new(0, [statement], SafeCoreMirTerminator.Return(Number(), Source), Source)], + [new(0, "x", Boolean, SafeCoreMirLocalKind.User, false, Source)])]), SafeCoreMirDiagnosticCodes.TypeMismatch); + SafeCoreType unresolved = new SafeCoreTypeInference().Fresh(); + Invalid(new([Function([ReturnBlock()], [new(0, "x", unresolved, SafeCoreMirLocalKind.Parameter, false, Source)])]), SafeCoreMirDiagnosticCodes.TypeMismatch); + SafeCoreMirOperand invalidOperand = new(SafeCoreMirOperandKind.Constant, null!, -1, "1", Source); + Invalid(Computation(SafeCoreMirRvalue.Unary("-", invalidOperand, Integer, Source)), SafeCoreMirDiagnosticCodes.TypeMismatch); + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Branch(invalidOperand, 0, 0, Source), Source)), SafeCoreMirDiagnosticCodes.TypeMismatch); + Invalid(new([new(0, "crate::invalid", null!, [], [ReturnBlock()], 0, Source)]), SafeCoreMirDiagnosticCodes.TypeMismatch); + return Task.CompletedTask; + } + + private static Task ConstantsAsync() + { + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(Number("2147483648"), Source), Source)), SafeCoreMirDiagnosticCodes.InvalidOperand); + Invalid(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(Number("1i32"), Source), Source)), SafeCoreMirDiagnosticCodes.InvalidOperand); + SafeCoreType character = SafeCoreType.Primitive(SafeCoreSemanticTypeKind.Char); + var surrogate = SafeCoreMirOperand.Constant(character, "55296", Source); + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Return(surrogate, Source), Source)], result: character)]), SafeCoreMirDiagnosticCodes.InvalidOperand); + AssertEx.True(SafeCoreMirValidation.Validate(Program(new SafeCoreMirBlock(0, [], SafeCoreMirTerminator.Return(Number("-2147483648"), Source), Source))).IsSuccessful, + "Signed minimum constant must be representable."); + return Task.CompletedTask; + } + + private static Task CallsAsync() + { + SafeCoreMirFunction callee = new(1, "crate::callee", Integer, + [new(0, "arg", Integer, SafeCoreMirLocalKind.Parameter, false, Source)], + [new(0, [], SafeCoreMirTerminator.Return(SafeCoreMirOperand.Local(0, Integer, Source), Source), Source)], 0, Source); + SafeCoreMirOperand target = SafeCoreMirOperand.Function(1, SafeCoreType.Function([Integer], Integer, "crate::callee"), Source); + SafeCoreMirLocal[] locals = [new(0, "result", Integer, SafeCoreMirLocalKind.Temporary, false, Source)]; + SafeCoreMirFunction caller = Function([ + new(0, [], SafeCoreMirTerminator.Call(target, [Number()], 0, 1, Source), Source), + new(1, [], SafeCoreMirTerminator.Return(SafeCoreMirOperand.Local(0, Integer, Source), Source), Source)], locals); + AssertEx.True(SafeCoreMirValidation.Validate(new([caller, callee])).IsSuccessful, "Typed calls should pass."); + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Call(target, [], 0, 0, Source), Source)], locals), callee]), SafeCoreMirDiagnosticCodes.TypeMismatch); + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Call(target, [Number()], null, 0, Source), Source)], locals), callee]), SafeCoreMirDiagnosticCodes.TypeMismatch); + SafeCoreMirOperand wrongNominal = target with { Type = SafeCoreType.Function([Integer], Integer, "crate::other") }; + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Call(wrongNominal, [Number()], 0, 0, Source), Source)], locals), callee]), SafeCoreMirDiagnosticCodes.TypeMismatch); + SafeCoreMirOperand negative = target with { Id = -1 }; + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Call(negative, [Number()], 0, 0, Source), Source)], locals), callee]), SafeCoreMirDiagnosticCodes.InvalidOperand); + return Task.CompletedTask; + } + + private static Task DivergingCallsAsync() + { + SafeCoreMirFunction diverge = new(1, "crate::diverge", Never, [], [new(0, [], SafeCoreMirTerminator.Goto(0, Source), Source)], 0, Source); + SafeCoreMirOperand target = SafeCoreMirOperand.Function(1, SafeCoreType.Function([], Never, "crate::diverge"), Source); + SafeCoreMirFunction caller = Function([new(0, [], SafeCoreMirTerminator.Call(target, [], null, -1, Source), Source)]); + AssertEx.True(SafeCoreMirValidation.Validate(new([caller, diverge])).IsSuccessful, "A diverging callee requires no return path."); + Invalid(new([Function([new(0, [], SafeCoreMirTerminator.Call(target, [], null, 1, Source), Source), ReturnBlock(1)]), diverge]), + SafeCoreMirDiagnosticCodes.InvalidControlFlow); + return Task.CompletedTask; + } + + private static Task ComputationsAsync() + { + SafeCoreType wider = SafeCoreType.Primitive(SafeCoreSemanticTypeKind.I64); + SafeCoreMirRvalue invalid = SafeCoreMirRvalue.Coerce(Number(), wider, Source); + Invalid(Computation(invalid), SafeCoreMirDiagnosticCodes.TypeMismatch); + AssertEx.True(SafeCoreMirValidation.Validate(Computation(SafeCoreMirRvalue.Cast(Number(), wider, Source))).IsSuccessful, + "Explicit numeric casts are distinct from implicit coercions."); + SafeCoreType tuple = SafeCoreType.Tuple([Integer, Boolean]); + AssertEx.True(SafeCoreMirValidation.Validate(Computation(SafeCoreMirRvalue.Tuple( + [Number(), SafeCoreMirOperand.Constant(Boolean, "false", Source)], tuple, Source))).IsSuccessful, "Tuple members retain their types."); + Invalid(Computation(SafeCoreMirRvalue.Binary("&&", SafeCoreMirOperand.Constant(Boolean, "true", Source), + SafeCoreMirOperand.Constant(Boolean, "false", Source), Boolean, Source)), SafeCoreMirDiagnosticCodes.TypeMismatch); + return Task.CompletedTask; + } + + private static SafeCoreMirProgram Computation(SafeCoreMirRvalue value) => new([Function([ + new(0, [new(0, value, Source)], SafeCoreMirTerminator.Return(SafeCoreMirOperand.Local(0, value.Type, Source), Source), Source)], + [new(0, "result", value.Type, SafeCoreMirLocalKind.Temporary, false, Source)], value.Type)]); + + private static Task ValidationBudgetsAsync() + { + SafeCoreMirProgram program = Program(ReturnBlock()); + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + AssertEx.Throws(() => SafeCoreMirValidation.Validate(program, new() { CancellationToken = cancellation.Token })); + AssertEx.True(SafeCoreMirValidation.Validate(program, new() { MaximumOperations = 1 }).IsTruncated, "Operation limits are explicit."); + AssertEx.True(SafeCoreMirValidation.Validate(program, new() { Timeout = TimeSpan.FromTicks(1) }).IsTruncated, "Wall time must be checked."); + AssertEx.True(SafeCoreMirValidation.Validate(Program(ReturnBlock(), ReturnBlock(1)), new() { MaximumBlocks = 1 }).IsTruncated, "Block limits are enforced."); + SafeCoreMirProgram malformed = Program(new SafeCoreMirBlock(1, [], SafeCoreMirTerminator.Goto(-1, Source), Source)); + AssertEx.True(SafeCoreMirValidation.Validate(malformed, new() { MaximumDiagnostics = 1 }).IsTruncated, "Diagnostic overflow cannot report successful validation."); + SafeCoreType nested = SafeCoreType.Tuple([SafeCoreType.Tuple([Integer])]); + SafeCoreMirProgram deep = new([Function([ReturnBlock()], [new(0, "arg", nested, SafeCoreMirLocalKind.Parameter, false, Source)])]); + AssertEx.True(SafeCoreMirValidation.Validate(deep, new() { MaximumTypeDepth = 1 }).IsTruncated, "Nested types consume depth budget."); + AssertEx.Throws(() => SafeCoreMirValidation.Validate(program, new() { MaximumOperations = 0 })); + return Task.CompletedTask; + } + + private static Task FormattingAsync() + { + SafeCoreMirProgram program = Program(ReturnBlock()); + const string expected = "safe-core-mir-v1\nfn @0 crate::main -> i32 entry bb0 [sample.rs:0+12/12 hir#0] {\n bb0 [sample.rs:0+12/12 hir#0]:\n return const 1:i32 [sample.rs:0+12/12 hir#0]\n}\n"; + CultureInfo previous = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("ar-EG"); + AssertEx.Equal(expected, SafeCoreMirFormatting.Format(program)); + } + finally { CultureInfo.CurrentCulture = previous; } + AssertEx.Equal(expected, SafeCoreMirFormatting.Format(program)); + AssertEx.Throws(() => SafeCoreMirFormatting.Format(program, new() { MaximumCharacters = 1 })); + AssertEx.Throws(() => SafeCoreMirFormatting.Format(program, new() { MaximumOperations = 1 })); + AssertEx.Throws(() => SafeCoreMirFormatting.Format(program, new() { Timeout = TimeSpan.FromTicks(1) })); + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + AssertEx.Throws(() => SafeCoreMirFormatting.Format(program, new() { CancellationToken = cancellation.Token })); + return Task.CompletedTask; + } + + private sealed class IndexOnlyList(T item) : IReadOnlyList + { + public int Count => 1; + public T this[int index] => index == 0 ? item : throw new ArgumentOutOfRangeException(nameof(index)); + public IEnumerator GetEnumerator() => throw new InvalidOperationException("Unbounded enumeration is forbidden."); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private sealed class OversizedList : IReadOnlyList + { + public int Count => 100_001; + public T this[int index] => throw new InvalidOperationException("Oversized collections must be rejected before access."); + public IEnumerator GetEnumerator() => throw new InvalidOperationException("Unbounded enumeration is forbidden."); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +}