Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Operator lowering and performance

Operator hooks use the same typed call operations, loan checks, argument ABI, caller attribution, and reachability/cache machinery as named calls. Reflected operations evaluate arguments in source order and reorder their already computed values for the call. They never rerun operand expressions. Dispatch uses the specialized nominal types; it does not add vtables or runtime type tests.

Keep four costs distinct:

  • Dispatch: an ordinary direct call, eligible for the same bounded inlining as an explicitly named call.
  • Borrowing: shared parameters lend existing storage without copying matrix payloads. The compiler may need an addressable temporary for a computed owner.
  • Results: inline class result storage uses the normal return ABI. Returning a large matrix still requires whatever allocation its implementation specifies.
  • Expression chains: (a + b) + c invokes two operations and may build two results. This implementation does not fuse loops, build expression templates, or silently change evaluation order or floating-point association. Named destination-writing methods remain useful for large numerical workloads.

Bounded optimization and evidence

The release leaf inliner accepts shared class receivers that only project scalar fields, alongside its existing scalar inputs and arithmetic. It still requires a scalar output, no locals, no calls or writes, and at most 24 non-marker typed operations. Tail calls to eligible leaves also substitute their body when the caller has no managed locals to clean up before the call. This benefits ordinary named methods as well as operator hooks. Managed aggregates, matrix allocation, and result-producing ordinary class constructors remain ordinary calls. Quantity values use their storage type’s scalar eligibility.

The regression probe compares a + b, a.__add__(b), and a.x + b.x for a class containing one f64. Operator and named calls have identical native instructions in both build modes. In release mode all three produce identical 12-instruction x86-64 bodies, with no call or jump to the hook. Before the inliner extension, both call spellings added a five-instruction tail-call wrapper. This is code-generation evidence, not a claim about matrix-kernel throughput. Separate tests disable heap allocation across owned class addition and in-place updates, and check destructor and failure paths.

Quantity erasure

The builtin Quantity carries its storage type’s native ABI. Its constructor and numeric projection lower directly, with no call, allocation, dimension tag, or constructor state. Direct get and float to retain argument validation and receiver loans while avoiding addressable receiver temporaries for named numeric values. Module unit constants substitute their folded storage literal and type retag at each read.

tests/test_quantity_erasure.rs compares complete release instruction listings for Quantity and plain f32/f64 functions: a + b, a * b, q.to(unit), and q.to(km) with a constant coherent value of 1000.0. On x86-64 the paired listings are identical, including parameter/result registers, with no Quantity constructor, method, constant-getter or caller-context calls. The test uses the host backend; native ARM execution remains part of ARM qualification rather than evidence from this x86-64 run.

Other tests exercise every numeric width and representative integer domains through direct/indirect and tail calls, references, aggregates, collections, closures and generators in both build modes. Signed zero, NaN, infinity and worker transfers retain their numeric payloads. Existing allocation-denial, checked-arithmetic, formatting and diagnostic tests cover the language contracts. This evidence does not imply identical instructions for arbitrary programs or shared bodies for different dimension instances.

Design sources

Python supplies the familiar hook names and the separation between ordinary, reflected, and in-place operations. Plenty deliberately uses static resolution, shared value-producing receivers, and unit-returning mutable in-place methods instead of Python’s dynamic NotImplemented and replacement-result behavior. See the Python data model.

D’s operator specification rewrites operators into named member calls and has separate right-hand and assignment hooks. Its operator-string templates reduce declaration repetition; they do not themselves eliminate allocation or intermediate results. Plenty uses ordinary named methods without adding string-template machinery.

The C++ operator rules likewise transform operator notation to function calls while preserving the operator’s sequencing rules. That separation motivates keeping source evaluation order independent of which operand owns the selected hook. Inlining remains an optimization: even Clang’s always-inline attribute does not guarantee substitution. Plenty reuses its bounded leaf substitution rather than adding an operator-only optimizer or promising that every call vanishes.

Rust’s Add trait separates the right-hand operand and output types. Plenty’s signatures likewise need not return the receiver type; this permits scalar results and explicit Result values without introducing associated types for this feature.