Skip to content

Numerics and performance

This page specifies E#‘s semantics-preserving performance foundation. Performance modes do not add a second arithmetic language: Debug and Release accept the same .es program and have the same observable behavior. SIMD is requested through the BCL, and zero-copy access is requested through the existing pointer forms.

An .esproj selects the compiler policy through the ordinary MSBuild configuration:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>true</Optimize>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>

The standalone compiler accepts --configuration Debug|Release. Debug emits the CLR’s default debug-tracking policy and favors stable source mapping without globally disabling JIT optimization. Release emits IgnoreSymbolStoreSequencePoints metadata. Both modes may use compact IL opcodes; Release may additionally remove redundant locals or temporaries only when the transformation preserves semantics.

namespace Numerics
func sumSquares(values: double[]) -> double {
var total: double = 0.0
for value in values { total += value * value }
return total
}

Release shall not change overflow behavior, operand evaluation order, exception behavior, externally visible mutation, value-copy semantics, or the operation tree of a floating-point expression. In particular it shall not reassociate arithmetic or silently introduce a fused multiply-add. The only exception is a source declaration that explicitly permits the direct FMA contraction specified below.

for value in array is an array operation, not an IEnumerable<T> operation. The collection expression is evaluated once, its length is read once, and the loop lowers to an index, a cached length, ldlen, and a direct element load. It has no enumerator, interface conversion, boxing, disposal region, or per-loop allocation. This lowering applies in both Debug and Release.

After the current element or range value is bound, the compiler advances its hidden counter before executing the source loop body. Consequently continue always makes progress; the hidden counter is not source-visible, so this ordering does not change the value observed by the body.

namespace Numerics
func sum(values: int[]) -> int {
var total = 0
for value in values { total += value }
return total
}

An integer range also evaluates both endpoints once and retains a canonical counted-loop shape:

namespace Numerics
func initialize(size: int) -> double[] {
let values = double[](size)
for i in 0..size { values[i] = double(i) }
return values
}

Native numeric conversions use the target type as a call and are checked when the destination is integral:

let real = double(i)
let money = decimal(19.99)
let count = int(money)
let character = char(65)

The supported targets are byte, sbyte, short, ushort, int, uint, long, ulong, nint, nuint, float, double, decimal, and char. A floating-point or decimal source truncates toward zero before an integral range check. A dynamic out-of-range conversion throws OverflowException; an out-of-range literal is ES2236. bool, enums, nullable types, pointers, reference types, Half, and user-defined conversions are not native conversion targets. Explicit alternatives such as byte.CreateTruncating(value) and byte.CreateSaturating(value) remain ordinary BCL calls.

A reflected BCL member whose return is an unsigned, wide, or native integer — uint, ushort, ulong, sbyte, nint, nuint — or decimal maps to the corresponding E# primitive, not an opaque external type, so the value feeds a T(...) conversion and the signedness-aware operators directly:

let n = int(BinaryPrimitives.ReadUInt32LittleEndian(buf)) // uint return → int(...)
let v = ushort(count) // → ushort argument slot

Numeric tokens retain their source digits until binding. Without a numeric context, integral literals infer int, then long, then ulong; fractional and exponent literals infer double. An expected primitive numeric type parses the token directly as that type:

let integer = 42
let large = 5_000_000_000
let ratio = 1.25
let price: decimal = 19.99
let position: float = 19.99
let measurement: double = 19.99

Context flows through arguments, returns, assignments, fields, constants, collection elements, and the operands of an arithmetic expression. If exactly one arithmetic operand is an unbound literal, the literal binds to the other operand’s concrete numeric type in either operand order. Two nonliteral values of different numeric types still require an explicit conversion.

decimal is a primitive E# value type mapped to System.Decimal. It participates in nullable types, arrays, fields, generics, boxing, defaults, constants, arithmetic, unary negation, equality, and ordering. Decimal operations retain System.Decimal behavior: base-10 finite precision, checked overflow, and throwing division by zero; there is no NaN, infinity, signed zero, FMA, or fast-math relaxation. Decimal literals are constructed from their exact coefficient, sign, and scale and never pass through binary floating point. Public constants and optional defaults use CLR DecimalConstantAttribute metadata.

Span<T> and ReadOnlySpan<T> are ordinary external CLR generic value types and may be constructed, indexed, and passed to BCL methods. Their indexers use the CLR ref-return contract directly; indexing does not box, and a span[i] read yields the element value T (the ref-return getter is emitted, then the element is loaded) — so int(span[i]) and let b = span[i] see a byte, not a managed reference.

The CLR’s implicit span conversions are implicit in E# and inserted at argument, assignment, and return positions: T[]Span<T> / ReadOnlySpan<T>, and Span<T>ReadOnlySpan<T>. Each lowers to the framework op_Implicit (a call, no copy), so a heap array or a stackalloc scratch flows into a ReadOnlySpan<T> parameter directly (buf.Write(scratch), BinaryPrimitives.ReadInt32LittleEndian(bytes)). The same conversion applies to an extension-method receiver: a Span<T> or T[] receiver on a this ReadOnlySpan<T> extension is coerced before the call, so span.SequenceEqual(other) type-checks with a Span<byte> receiver and a byte[] argument (see Extension-method calls). Spans are invariant in T, so the element must match exactly. See Type system → conversions.

namespace Numerics
using "System"
func sum(values: ReadOnlySpan<double>) -> double {
var total: double = 0.0
for i in 0..values.Length { total += values[i] }
return total
}
func scale(values: Span<float>, factor: float) {
for i in 0..values.Length { values[i] *= factor }
}

Calls including Slice, CopyTo, and Clear resolve through CLR metadata. A loop written as for i in 0..span.Length uses the same cached-end counted shape as any other range loop. A range in index position slices — values[a..b], values[a..], values[..b] — lowering to Slice(start, length) against the span’s own Length (a span has no this[Range] indexer).

A stack buffer is spelled stackalloc T[](n), yielding a frame-local Span<T> (or ReadOnlySpan<T> by target typing) — the zero-alloc scratch primitive, with no heap traffic and no pointer:

namespace Numerics
using "System"
using "System.Buffers.Binary"
func encodeLong(v: long) -> int {
let scratch = stackalloc byte[](8) // localloc — Span<byte>, frame-local
BinaryPrimitives.WriteInt64LittleEndian(scratch, v)
return scratch.Length
}

It lowers to sizeof(T) * nlocallocnew Span<T>(void*, int) (never newarr), and is subject to the identical by-ref-like safety rules below. Its full specification is in Low-level & unmanaged → stack-allocated spans.

The compiler recognizes CLR IsByRefLike metadata. A by-ref-like value cannot be boxed, stored in a heap field, captured by a function literal, or carried in an async state machine.

namespace Numerics
using "System"
class InvalidBuffer {
values: Span<float> // ES2230
}
namespace Numerics
using "System"
func invalidCapture(values: Span<float>) {
let read = func(i: int) -> float { return values[i] } // ES2233
}

An async function that would carry a live by-ref-like parameter or local across suspension is ES2232, and boxing a by-ref-like value is ES2234.

A by-ref-like return is governed by return-lifetime analysis rather than a blanket rejection. A Span<T> / ReadOnlySpan<T> return is well-formed when the returned span provably derives from a heap array, a receiver field, or a Span<T>/array/readonly *T parameter — its lifetime is that source, which the frame does not own. A span rooted in a stackalloc buffer or the address of a local escapes the frame and is ES2231. The rule is inferred, with no lifetime syntax; see Low-level & unmanaged → returning a span.

namespace Numerics
using "System"
func firstTwo(xs: int[]) -> ReadOnlySpan<int> {
return ReadOnlySpan<int>(xs)[0..2] // heap-array-rooted → returnable
}
func invalidReturn() -> Span<int> {
let scratch = stackalloc int[](4)
return scratch // ES2231 — stack-rooted, escapes the frame
}

Value semantics remain explicit and stable. Passing or returning a struct by value copies its value; the compiler shall not silently substitute a reference parameter. readonly *T is the existing zero-copy, non-mutating spelling:

namespace Numerics
struct SampleBlock { a: double, b: double, c: double, d: double }
func copied(block: SampleBlock) -> double = block.a
func borrowed(block: readonly *SampleBlock) -> double = block.a
func evaluate(sample: SampleBlock) -> double {
return borrowed(&sample)
}

Set <EsharpShowAllocations>true</EsharpShowAllocations> in an .esproj, or pass --show-alloc to esharpc compile-il, to request warnings. The warnings describe source-visible costs but never rewrite the program:

CodeCost surfaced
ES8001by-value parameter of at least 32 bytes
ES8002value receiver snapshot of at least 32 bytes
ES8003value return of at least 32 bytes
ES8004boxing to an interface or object
ES8005captured function literal/display class
ES8006generic IEnumerable lowering that may allocate an enumerator
ES8007managed address promoted to a durable heap-backed pointer cell

E# does not define vector literals, vector operators, architecture-width keywords, or automatic vectorization. Portable SIMD uses System.Numerics.Vector<T> explicitly. Because E# has no operator overloading, use BCL methods such as Vector.Multiply and Vector.Add.

namespace Numerics
using "System"
using "System.Numerics"
func saxpy(destination: Span<float>, x: ReadOnlySpan<float>, y: ReadOnlySpan<float>, scale: float) {
let width = Vector<float>.Count
var i = 0
while i + width <= destination.Length {
let vx = Vector<float>(x.Slice(i, width))
let vy = Vector<float>(y.Slice(i, width))
let scaled = Vector.Multiply(vx, scale)
let result = Vector.Add(scaled, vy)
result.CopyTo(destination.Slice(i, width))
i += width
}
while i < destination.Length {
destination[i] = x[i] * scale + y[i]
i += 1
}
}

External generic value construction, static members such as Vector<float>.Count, generic static BCL calls, value-type instance calls, and Span arguments all retain their closed CLR types without boxing. Architecture-specific intrinsics are also ordinary explicit BCL APIs and must be guarded:

namespace Numerics
using "System.Runtime.Intrinsics.X86"
func avx2Available() -> bool = Avx2.IsSupported

float and double follow the CLR and ECMA-334 floating-point model:

  • Values are IEC 60559-compatible and include NaN, infinities, signed zero, and subnormal values subject to the target runtime’s permitted subnormal policy.
  • Ordinary floating-point overflow, underflow, division by zero, and invalid operations do not throw.
  • The CLR/JIT may evaluate an intermediate with greater precision or range than its source type.
  • Storing or returning converts the value to the destination type’s representable value according to the CLR/runtime contract.
  • Release does not reassociate operations and does not silently introduce fused multiply-add.
namespace Numerics
func multiplyAdd(a: double, b: double, c: double) -> double = a * b + c

The function above preserves a multiply followed by an add in IL. Fused behavior may be requested through the BCL and may differ in low-order bits:

namespace Numerics
using "System"
func fusedMultiplyAdd(a: double, b: double, c: double) -> double =
Math.FusedMultiplyAdd(a, b, c)

Release builds may also contract a directly written multiply/add shape when its function or containing class/struct carries @floatMode(contractFma: true):

namespace Numerics
@floatMode(contractFma: true)
func multiplyAdd(a: double, b: double, c: double) -> double = a * b + c
@floatMode(contractFma: true)
struct Kernel {
func apply(a: float, b: float, c: float) -> float = c + a * b
}

Compiler directives are distinct from CLR [Attributes] and are not emitted into metadata. They may be attached to functions, classes, structs, and interfaces; on a class or struct, floatMode is inherited by concrete methods unless a method supplies its own directive. An interface retains and validates the directive for source and tooling consistency, but has no arithmetic body to contract.

Only Release contracts, and only the same-typed float/double shapes a*b+c, c+a*b, a*b-c, and c-a*b. Source operand evaluation order is preserved. Debug, unannotated declarations, decimal operations, mixed types, nested function literals, and user-defined operators remain strict. An async function carries its mode into its generated state-machine body.

There are no fast, strict, checked, or unchecked E# keywords. Integer arithmetic retains the CLR’s default unchecked behavior; checked-by-default applies to explicit native conversions, not arithmetic.