Skip to content

CEP-0067: Comptime-Known Mixed Integer Arithmetic

Draft

Draft proposal to allow mixed concrete integer arithmetic when both operand values are known at compile time. Current V1 primitive runtime arithmetic remains unchanged until this proposal is accepted.

Summary

Catalyst should allow otherwise-incompatible mixed concrete integer arithmetic when both operand values are compile-time-known, the operation can be evaluated during sema or comptime execution, and the resulting value is representable in the expression's expected integer type.

Runtime mixed concrete integer arithmetic remains strict: if neither operand type can represent every value of the other, the operator is rejected unless the programmer writes an explicit conversion.

Motivation

The current primitive integer rule correctly rejects runtime expressions such as u32 + i32, because neither operand type can represent every value of the other:

fn add(unsigned: u32, signed: i32) i64 {
  return unsigned + signed // rejected: runtime u32 + i32 has no shared operand type
}

However, the same operand types can appear in source where both actual values are already known:

const unsigned: u32 = 0
const signed: i32 = 10
const sum: i64 = unsigned + signed

In this form there is no runtime choice to make. The compiler can evaluate 0 + 10 exactly, then check whether the value fits the expected destination. Requiring a conversion in this case is noisy and loses a useful distinction that Zig makes: runtime mixed signedness stays rejected, while compile-time-known mixed values may be folded and then checked against context.

Proposed Direction

Primitive integer arithmetic keeps the existing result-selection rule first:

  • same concrete integer type preserves that type;
  • if one operand type represents every value of the other, the result is that existing wider operand type;
  • if one operand is comptime_int, it may contextualize to the concrete operand type when representable;
  • if both operands are comptime_int, the operation evaluates as comptime_int.

After those rules fail, sema may use the comptime-known mixed-integer rule when all of these are true:

  • both operands are primitive integer values;
  • both operand values are known at compile time, even if their declared types are concrete integer types;
  • neither operand expression has side effects or runtime-dependent value production;
  • the operation can be evaluated exactly under Catalyst's comptime_int limits;
  • the expression has an expected integer type, and the computed result is representable in that expected type.

When the rule applies, the operator expression has the expected integer type and stores the computed constant value.

This rule does not synthesize a new primitive arithmetic conformance such as Add(u32, i32) -> i64. It is a constant-evaluation rule for a specific expression occurrence.

Example

Accepted if this proposal is adopted:

const unsigned: u32 = 0
const signed: i32 = 10

const sum: i64 = unsigned + signed // ok: result value 10 fits i64

Still rejected:

fn add(unsigned: u32, signed: i32) i64 {
  return unsigned + signed // rejected: operands are runtime values
}

Also rejected:

const unsigned: u32 = 4_000_000_000
const signed: i32 = -1

const too_small: i32 = unsigned + signed // rejected: result does not fit i32

If no expected integer type is available, acceptance remains an open question. The conservative policy is to keep the expression rejected so typed concrete operands do not unexpectedly produce a comptime_int value in an unannotated position.

Diagnostics

When a mixed concrete integer expression is rejected because a value is not compile-time-known, the diagnostic should keep pointing at the incompatible operand types:

mixed primitive integer arithmetic requires one operand type to represent the other

When both values are known but the computed result does not fit the expected type, the diagnostic should report the destination representability failure instead of the mixed-type rule.

V1 Compatibility

Current accepted V1 behavior remains the stricter rule documented in Arithmetic Contracts and Built-In Conformances: mixed concrete integer arithmetic is accepted only when one operand type can represent every value of the other.

Accepting this CEP would be a source-compatible relaxation. Code that currently compiles keeps the same meaning. Some code that currently requires an explicit conversion would become valid only when the operands are compile-time-known and the result fits the expected integer type.

Open Questions

  • Should this rule require an expected integer type, or may a successful expression remain comptime_int when no expected type is available?
  • Should the rule apply to all primitive integer arithmetic operators, including division and remainder, or only to +, -, and * in its first accepted form?
  • Should the same rule later apply to mixed integer/float expressions when both values are compile-time-known?
  • Should diagnostics mention that adding an expected type may make a compile-time-known mixed expression valid?