Skip to content

Arithmetic Contracts

Accepted

Accepted for V1 arithmetic contract names, operator lowering, lookup, result selection, primitive numeric conformance shape, and deferred arithmetic boundaries.

This document records the prelude contract families used by arithmetic operator syntax.

Contract Families

V1 arithmetic operators are contract-backed source syntax, but Catalyst does not support arbitrary symbolic operator overloading. The accepted arithmetic contract families are:

fn Add(comptime Rhs: Type, comptime Out: Type) Type {
  require_concrete_sized(Rhs, "Add right operand type must be concrete and sized")
  require_concrete_sized(Out, "Add result type must be concrete and sized")

  return contract {
    fn add(self: *const Self, rhs: *const Rhs) Out
      if ContractSurface.current().is_static()
  }
}

fn Sub(comptime Rhs: Type, comptime Out: Type) Type {
  require_concrete_sized(Rhs, "Sub right operand type must be concrete and sized")
  require_concrete_sized(Out, "Sub result type must be concrete and sized")

  return contract {
    fn sub(self: *const Self, rhs: *const Rhs) Out
      if ContractSurface.current().is_static()
  }
}

fn Mul(comptime Rhs: Type, comptime Out: Type) Type {
  require_concrete_sized(Rhs, "Mul right operand type must be concrete and sized")
  require_concrete_sized(Out, "Mul result type must be concrete and sized")

  return contract {
    fn mul(self: *const Self, rhs: *const Rhs) Out
      if ContractSurface.current().is_static()
  }
}

fn Div(comptime Rhs: Type, comptime Out: Type) Type {
  require_concrete_sized(Rhs, "Div right operand type must be concrete and sized")
  require_concrete_sized(Out, "Div result type must be concrete and sized")

  return contract {
    fn div(self: *const Self, rhs: *const Rhs) Out
      if ContractSurface.current().is_static()
  }
}

fn Rem(comptime Rhs: Type, comptime Out: Type) Type {
  require_concrete_sized(Rhs, "Rem right operand type must be concrete and sized")
  require_concrete_sized(Out, "Rem result type must be concrete and sized")

  return contract {
    fn rem(self: *const Self, rhs: *const Rhs) Out
      if ContractSurface.current().is_static()
  }
}

fn Neg(comptime Out: Type) Type {
  require_concrete_sized(Out, "Neg result type must be concrete and sized")

  return contract {
    fn neg(self: *const Self) Out
      if ContractSurface.current().is_static()
  }
}

The helper require_concrete_sized follows the same prelude helper shape used by other contract families. Rhs and Out must be concrete and sized. The implementing Self is governed by ordinary implementation rules.

Each base arithmetic contract has one required operation and no default method. There is no universal default for arithmetic operations.

Arithmetic contract operations are static-only in V1. Their operations are absent from dynamic contract surfaces even when Rhs and Out are concrete sized types. A standalone dyn Add(...), dyn Sub(...), dyn Mul(...), dyn Div(...), dyn Rem(...), or dyn Neg(...) surface would therefore be empty and is not dyn-safe under the empty-dynamic-surface rule. This is a language policy for arithmetic operator syntax, not a claim that return-by-value operations are inherently dyn-unsafe.

Operator Lowering

Arithmetic operators lower through the canonical prelude contract identities:

Source form Required contract operation
a + b Add(Rhs, Out).add
a - b Sub(Rhs, Out).sub
a * b Mul(Rhs, Out).mul
a / b Div(Rhs, Out).div
a % b Rem(Rhs, Out).rem
-a Neg(Out).neg

The compiler recognizes the canonical prelude contract identities, not arbitrary local bindings named Add, Sub, Mul, Div, Rem, or Neg. Shadowing those names does not change operator lowering. Explicit source references to the contracts still follow normal name resolution.

Operator availability is left-operand driven. For a + b, the left operand type must visibly implement Add(Rhs, Out) where Rhs is the right operand type. The compiler does not search the right operand for a reflected operation and does not invent bidirectional relationships. Libraries that need both operand orders must provide both implementations explicitly.

Arithmetic operators borrow operands. A binary operation receives self: *const Self and rhs: *const Rhs; unary negation receives self: *const Self. Operator syntax does not consume operands by default. Types that need consuming arithmetic should expose named APIs rather than use the base operator contracts.

Operator lowering may materialize const temporaries for rvalue operands so pointer-based operations can be called:

foo() + bar()

is conceptually checked like:

const lhs_tmp = foo()
const rhs_tmp = bar()
lhs_tmp.add(&rhs_tmp)

These temporaries are the semantic model, not a required runtime calling convention. Optimized code should avoid unnecessary storage, pointer traffic, or calls when the selected implementation can be inlined.

Result Selection

For a non-primitive arithmetic expression, sema first resolves the left operand type, operator, and right operand type without using arithmetic operator candidates to choose operand types or insert implicit conversions.

Then sema gathers visible implementations for the left operand type and applied operator family. If exactly one visible implementation matches the operand types, that implementation selects the result type. If multiple visible implementations differ only by Out, an explicit expected type may select the unique candidate whose Out is assignable to that expected type.

Expected type may select Out only when the expected type is already determined by an enclosing context such as an explicit annotation, a selected function parameter type, or an explicit function return type. Expected type does not participate in open-ended generic inference, callable selection, or operand conversion search.

If no candidate is visible, the operator is unavailable. If multiple candidates remain after applying an available expected type, the expression is ambiguous and the diagnostic should list candidate implementations and relevant imports.

Primitive numeric arithmetic uses a single canonical result implementation for each accepted operand pair. Expected type validates or coerces the selected primitive result under ordinary numeric coercion rules; it does not select alternate primitive result widths. For primitive numeric expressions only, comptime_int operands are contextualized to a concrete primitive operand type before conformance matching when that conversion is representable.

Primitive Numeric Arithmetic

Primitive numeric arithmetic conformances are ordinary generic prelude impls over the primitive numeric type factories documented in Built-In Types. Their operation bodies call compiler arithmetic intrinsics so static operator lowering remains aggressively optimizable.

For primitive integers, the prelude provides generic implementations over the signed/signed, signed/unsigned, unsigned/signed, and unsigned/unsigned family pairs for Add, Sub, Mul, Div, and Rem only when the result type can be one of the existing operand types.

Primitive integer result selection:

Operand types Result
same concrete integer type same type
one operand type represents every value of the other the existing wider operand type
neither operand type represents the other rejected

The narrower operand is checked as the wider existing operand type. Primitive integer arithmetic never synthesizes a third result type such as u33 or i65.

Examples:

Expression shape Result
u16 + u32 u32
u32 + u16 u32
i16 + i32 i32
u16 + i32 i32
u32 + i32 rejected
u64 + i64 rejected

When one primitive integer operand is a comptime_int value and the other is a concrete primitive integer, sema first checks whether the comptime_int value is representable in the concrete operand type. If it is representable, operator checking proceeds as same-type primitive arithmetic and the result is the concrete operand type. If it is not representable, the expression is rejected before primitive conformance matching. When both operands are comptime_int, the operation is evaluated as a comptime_int expression and remains comptime-only until later context resolves it to a concrete type.

var n: u32 = 10
n - 1 // u32

var i: usize = 0
i + 1 // usize

var b: u8 = 250
b + 5   // u8; overflow is checked in Checked safety mode
b + 300 // error: 300 is not representable as u8

Integer division and remainder follow the language's primitive division and remainder semantics. Integer division truncates toward zero. Rem means remainder, not Euclidean modulo. Divide-by-zero follows the existing safety-mode and illegal-behavior rules documented in Built-In Types. Any primitive overflow or underflow cases that remain after result selection follow the same safety-mode rule.

Signed integers implement Neg(Self). Negating the minimum value of a signed integer follows the existing primitive signed overflow safety rule. Unsigned integers do not implement Neg in V1, so -1u8 remains invalid.

Primitive float arithmetic implements Add, Sub, Mul, and Div for f32 and f64 combinations that follow the existing float widening rule:

  • f32 op f32 -> f32
  • f32 op f64 -> f64
  • f64 op f32 -> f64
  • f64 op f64 -> f64

Primitive floats implement Neg(Self). Primitive floats do not implement Rem in V1; use named std.math APIs for specific float remainder behavior if they are accepted later.

Mixed integer/float primitive arithmetic exists only when the integer operand can losslessly coerce to the float type under the existing numeric coercion rule. The result is the float type. Mixed int/float operations that would require lossy conversion are rejected unless the programmer writes an explicit conversion.

Primitive arithmetic impls are visible to conformance reflection and tooling as prelude impls backed by compiler intrinsics. After specialization and inlining, optimized codegen should emit the same primitive arithmetic IR it would have emitted for a hardcoded primitive operator.

User Implementations

Generic code may require arithmetic with the same contracts that operator syntax uses:

fn double(comptime T: Add(T, T), value: T) T {
  return value + value
}

Users may implement arithmetic contracts for their own types and may provide adapter implementations for primitive-left expressions through ordinary impl visibility and coherence rules. Such implementations are sharp tools: importing an adapter namespace may make expressions such as 1 + value available. Diagnostics and tooling should show the selected arithmetic implementation for operator expressions.

The compiler and prelude do not provide arithmetic conformances for optionals, arrays, slices, function values, arbitrary dynamic contract objects, or other non-numeric type forms. Users may define domain-specific arithmetic where the general implementation model allows it. Dynamic arithmetic through dyn Add(...) is not available in V1 because standalone arithmetic dynamic surfaces are empty and therefore not dyn-safe.

Base arithmetic contracts define source availability and lowering only. They do not imply algebraic laws such as associativity, commutativity, distributivity, identity elements, inverse elements, or allocation freedom. Stronger algebraic contracts such as Semigroup, Monoid, Ring, Field, or vector-space-style APIs may be defined later by std.math or domain libraries.

User-defined arithmetic operations are not required by the base contracts to be pure or allocation-free. However, operator implementations should not hide surprising allocation, IO, global state, blocking behavior, or other effects unless the type's API documents that behavior. Realtime, no-allocation, and effect-sensitive policies belong to linting, capabilities, or future effect tooling rather than the base operator contracts.

Deferred

V1 does not include unary plus. Future unary plus design is tracked by CEP-0032: User-Defined Operator Overloading.

Compound assignment forms such as +=, -=, *=, /=, and %= are deferred. Future compound assignment sugar and optional mutating override contracts are tracked by CEP-0055: Compound Assignment Operators.

Bitwise and shift operator spellings are not part of the V1 expression operator surface. Their future contract-backed shape is tracked by CEP-0056: Bitwise and Shift Operators.

Explicit wrapping, checked-overflow, and saturating arithmetic APIs remain deferred to CEP-0013: Explicit Numeric Arithmetic Modes.