Alternative-runtime iteration contracts
CompilationOptions.RuntimeIterationContract selects a nominal synchronous iteration
protocol. It is opt-in; null preserves Raven's existing .NET interface and pattern
iteration. The API is separate from MetadataImportOptions, which controls assembly
resolution. Alternate targets should supply their own metadata references and explicit
core configuration as well as this protocol.
options = options.WithRuntimeIterationContract(new RuntimeIterationContract(
"NeoCLR.CoreProbe",
"System.Collections.Iterable`1",
"System.Collections.Iterator`1"));
The record also configures acquisition, advance and element-property names, defaulting to GetIterator, MoveNext and Current. The compiler contains no neoCLR-specific type names. The same option is available through evaluated project properties; there are no standalone CLI switches.
For for value in values, the collection must implement exactly one instantiation
of the selected iterable interface. The selected declarations must be accessible
one-parameter interfaces from the configured assembly name. Acquisition must be an
accessible parameterless instance method returning the corresponding Iterator
The bound loop stores the resolved method symbols and element type. Existing codegen emits their calls, so no target-specific opcode rewrite is needed. Semantic queries see the same inferred loop element type. Optional copies retain the contract, and changing it blocks transfer of previous semantic state. No syntax or TextMate grammar changed.
Known discrepancy: iterator disposal
Inspection on 2026-09-12 found that EmitEnumeratorForLoop currently emits acquisition,
MoveNext and Current but no automatic Dispose call, including on the default .NET path.
This is a behavior gap, not the desired contract. The target option does not fix it or
make a new guarantee about cleanup. Normal completion, break and return can execute
without disposing the cursor; explicit iterator use can still call Dispose itself.
Plan a separate correction covering normal exhaustion, break, return, nesting and fault/exception exits. Keep .NET's disposal/unwinding expectations on the default target; neoCLR needs a defined terminal-fault cleanup policy. Do not silently omit cleanup when a target lacks a disposal member or exception-handling import support. The author asked that this discrepancy be recorded and considered for a fix; no cleanup implementation is claimed by this slice.
The subsequent author discussion proposed defer as a possible language construct for
neoCLR, which has no recoverable guest exception flow. Record this for now; no defer
syntax or cleanup mechanism is being added here. Cleanup still needs runtime support
for the relevant exits. A finally-only CLI region is one candidate representation and
does not require a catch clause or a guest Exception hierarchy; compare
C# try-finally
and CLI endfinally
(primary sources consulted 2026-09-12). Which faults unwind, and what happens if cleanup
faults, remain open. Defer could lower onto that shared mechanism after its contract is
settled, rather than being treated as a substitute for it.
Project configuration
A project can select the protocol for both workspace compilation and the language server:
<PropertyGroup>
<RavenIterationAssemblyName>NeoCLR.CoreProbe</RavenIterationAssemblyName>
<RavenIterationIterableType>System.Collections.Iterable`1</RavenIterationIterableType>
<RavenIterationIteratorType>System.Collections.Iterator`1</RavenIterationIteratorType>
</PropertyGroup>
Optional RavenIterationAcquisitionMethod, RavenIterationAdvanceMethod and
RavenIterationCurrentProperty override GetIterator, MoveNext and Current. With all
six properties absent, existing .NET selection is unchanged. Partial configuration is
retained and diagnosed when binding an affected loop, rather than silently falling back.
These properties select compiler contracts; they do not install a target runtime or
configure an executable importer.
Future deterministic resource cleanup for neoCLR must be target-specific and opt-in. It must not change existing .NET/CLR behavior as a side effect. Any correction to the existing .NET disposal gap is a separate work item. Scope cleanup, object destruction, and deconstruction are distinct; Disposable/Closable are candidate hooks, not a newly implemented lifetime rule. Aliases alone do not establish cleanup ownership.
Vector interface projection
An alternative runtime that implements the selected Iterable contract for managed
vectors can set ArraysImplementIterable: true, or the evaluated project property
<RavenIterationArraysImplementIterable>true</RavenIterationArraysImplementIterable>.
The default is false. This adds the assembly-qualified interface to vector symbols
for conversions, generic inference and extension lookup. It does not apply to
rectangular arrays, add covariance, or implement the contract in the target runtime.
In this mode standard .NET vector-specific generic collection interfaces are not
advertised in place of the selected contract. Default .NET targeting is unchanged.
The neoCLR experiment supplies the runtime dispatch and iterator library support.
A target with a generic array metadata shape can instead set ArrayShapeTypeName
(or <RavenIterationArrayShapeType>System.Array`1</RavenIterationArrayShapeType>).
Raven resolves that one-parameter class in the selected contract assembly and
projects its implemented interfaces, substituting the vector element type. The
interface inheritance closure is included. This takes precedence over the Boolean
capability; missing, wrong-assembly, wrong-kind or wrong-arity shapes contribute no
vector-specific interfaces, so incompatible conversions fail normally.
The neoCLR experiment uses this path. It lets a target add array contracts in its
reference metadata without adding compiler rules for each interface. The cost is
that the target must keep the declaration and runtime implementations aligned.
The configured generic class is also a source/signature spelling of the vector:
System.Array<T> and T[] resolve to the same array semantics and emit CLI vector
signatures. Ordinary array members and the declared interface members are available;
interface member calls retain their interface owner and dispatch normally. Allocation
uses array expressions, not a constructor on the generic metadata declaration.
Public methods and properties declared on the configured shape are now projected as
well, retaining their metadata owner in member references. Interface members keep
interface dispatch; projected members take precedence over inherited names. Private
members and constructors are excluded. This enables an instance ForEach and static
Empty property without changing vector signatures, indexed loops or default .NET
array contracts. It remains experimental configured-target behavior.
The experimental metadata emitter also preserves nominal System.Void when used as a generic argument, rather than substituting the CLI no-result marker. Ordinary void method returns retain their existing encoding. neoCLR's array API probe exercises Func<T,Void> method groups and expression-bodied lambdas through execution. Interfaces declared by the ordinary array base type remain visible in either mode.
Cleanup boundary
This contract selects iteration symbols; it does not select an exception model or change cleanup lowering. Iterator disposal and structured cleanup must be validated separately for a target. A target lacking the required cleanup facilities cannot gain correct disposal merely by renaming its iterable interfaces.
Selected generator bodies (2026-10-11)
RuntimeAsyncIterationContract.AllowGeneratorBodies defaults to false. A backend
that implements producer lowering can explicitly enable it. Async functions returning
the selected nominal iterable or iterator interface then bind as async generators;
this includes generic element arguments and namespace-level functions. Async
yield from classifies the selected consumption protocol before synchronous fallback.
Assembly identity, interface kind and generic arity remain checked.
This is a binding capability, not a portable producer implementation. Ordinary .NET continues to use IAsyncEnumerable/IAsyncEnumerator and its existing state machines. A custom target must supply synthesis and qualify disposal, cancellation and lifetime independently. The neoCLR retained-body experiment opts in at its backend boundary; its source-method generic limitations do not constrain this shared binding contract.
Validation: 36 RuntimeIterationContractTests pass on .NET 11, including opt-in and opt-out generator binding for both return shapes and namespace functions. The prior 70-case runtime-iteration/async-iterator/delegation baseline passed before this change. These binding tests do not claim executable custom-generator support on .NET.
Selected iterator bodies do not report RAV2706 solely because they lack an explicit
await: their yield is a suspension boundary supplied by the selected backend.
This diagnostic rule requires both an iterator body and AllowGeneratorBodies; it
leaves ordinary awaitless Task warnings and default .NET behavior intact. There is
no change to bound await flags, metadata or emission. Focused contract regressions
cover method/namespace generators and an awaitless Task control.