mutable design

Design goal

The mutable facade gives the execution-oriented half of luna-poly a single import, and the layer behind it lets algorithms update polynomials in place while computing exactly what the immut layer computes. Performance comes first in this layer, but its side effects must stay where the caller can see them: in methods whose names say they mutate.

Mathematical background

A mutable polynomial is a variable whose value is a polynomial; the values are the same mathematical objects as in the immutable layer, in the same canonical forms. An in-place operation is an assignment p←p∘qp \leftarrow p \circ q, and the contract of the layer is

p.op_inplace(q)  leaves p equal to p_before op q,\texttt{p.op\_inplace(q)}\ \text{ leaves } p \text{ equal to } \texttt{p\_before op q},

with p_before op q computed by the immutable algorithms. Every observation (degree, terms, evaluation, equality) depends only on the current value.

Design decisions

A facade with the same names as immut

The facade re-exports core, the luna-generic algebra traits and the four mutable representations under the same names as the immut facade. Switching an algorithm between layers is mostly a change of import; generic code written against the capability traits or the operation records runs on both.

Mutation is opt-in and named

Only set_coefficient, clear, add_term_inplace and the *_inplace methods change their receiver. Operators and every other method return new values. x + y never changes x, even for mutable types, so arithmetic expressions read the same in both layers.

Canonical form is restored before every return

Each mutating method leaves the container canonical: trimmed coefficient arrays, sorted and merged term arrays, zero-free sparse maps. The invariants are the same as in immut, so ==, degree, size and the shapes have the same meaning.

Delegate the algorithms, specialize the updates

Non-trivial algorithms (Karatsuba, composition, derivative, powers, multivariate products, all of the context logic) are implemented once in immut and reached by conversion. The mutable packages implement directly only what benefits from in-place storage: coefficient setters, dense in-place addition, sparse single-term updates. The consistency tests check that both layers agree.

Ownership at the boundaries

Every conversion copies or rebuilds storage, except where the stored value is itself immutable (the context cell), so no mutable container ever shares storage with an immutable value or with another container:

  • from_immut and to_immut copy (dense, term, sparse) or share an immutable value (context);
  • copy() gives an independent container;
  • query methods such as to_coefficients and to_terms return fresh arrays.

API symmetry with immut

The layers match in names, parameter order and checked-variant conventions. The documented differences are:

Areaimmutmutable
conversionsnonefrom_immut, to_immut on every type
copy and resetvalues need neithercopy, clear (Copyable, Clearable, MutablePolynomial)
dense updatesrebuild with from_coefficientsset_coefficient (no checked form), add_inplace, mul_inplace, scale_inplace
term updates+, scaleadd_term_inplace, add_inplace, mul_inplace, scale_inplace
sparse updatesadd_term (returns a new value)set_coefficient, add_term_inplace, add_inplace, mul_inplace, scale_inplace
context binary opsadd_checked, mul_checkedadd_inplace, mul_inplace (aborting); checked forms via ops()
context bindingtakes immutable term/sparse polynomialstakes mutable term/sparse polynomials
substitution payloadimmut ContextSubstitutionValueits own ContextSubstitutionValue holding mutable polynomials
sparse copy bound—needs Eq + AddMonoid coefficients
ExponentVector, Variable, VariableContextcore typesthe same core types

Correctness / invariants

  • Canonical form after every public call, in every container.
  • For every operation, the mutable result converted with to_immut equals the immutable result on the converted inputs.
  • No mutable container shares mutable storage with any other value.
  • Passing a container as its own argument (p.add_inplace(p), p.mul_inplace(p)) gives 2p2p and p2p^2.

Alternatives rejected

  • Mutable-only types without an immutable twin. Value semantics is the safer default; mutation is an optimization chosen per call site.
  • Mutating operators. They would make every a * b a possible side effect.
  • Independent algorithm implementations for the mutable layer would double the code that must agree.

Boundaries

  • The facade adds no functions or types of its own.
  • Containers are not snapshots: assigning a container to a second binding shares it; use copy().
  • Delegated operations pay a conversion cost; the layer optimizes updates, not the algorithms themselves.
  • Everything the immutable layer does not do (division, factorization, Gröbner bases), this layer does not do either.