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.
Debug and Release compilation
Section titled “Debug and Release compilation”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.
Canonical array and counted loops
Section titled “Canonical array and counted loops”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 slotContextual numeric literals and decimal
Section titled “Contextual numeric literals and decimal”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 = 42let large = 5_000_000_000let ratio = 1.25
let price: decimal = 19.99let position: float = 19.99let measurement: double = 19.99Context 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 interoperability
Section titled “Span interoperability”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 Numericsusing "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 Numericsusing "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) * n → localloc → new 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.
By-ref-like safety
Section titled “By-ref-like safety”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 Numericsusing "System"
class InvalidBuffer { values: Span<float> // ES2230}namespace Numericsusing "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 Numericsusing "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}Allocation and copy diagnostics
Section titled “Allocation and copy diagnostics”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.afunc 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:
| Code | Cost surfaced |
|---|---|
ES8001 | by-value parameter of at least 32 bytes |
ES8002 | value receiver snapshot of at least 32 bytes |
ES8003 | value return of at least 32 bytes |
ES8004 | boxing to an interface or object |
ES8005 | captured function literal/display class |
ES8006 | generic IEnumerable lowering that may allocate an enumerator |
ES8007 | managed address promoted to a durable heap-backed pointer cell |
Explicit BCL SIMD
Section titled “Explicit BCL SIMD”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 Numericsusing "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 Numericsusing "System.Runtime.Intrinsics.X86"
func avx2Available() -> bool = Avx2.IsSupportedFloating-point contract and contraction
Section titled “Floating-point contract and contraction”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 + cThe 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 Numericsusing "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.