immut/context design
Design goal
ContextPolynomial[A] lets users write polynomials in named variables and transform them by evaluation, partial evaluation and substitution, while the arithmetic stays positional and reuses the term and sparse representations. It is also the place where luna-poly meets Luna-Flow/type_theory: substitutions can be keyed by type_theory names, but canonical forms, storage and coefficient arithmetic remain owned by this package.
Mathematical background
Polynomials over a context
A VariableContext names the variables . A context polynomial is a pair with
A named term such as [(x, 1), (y, 2), (x, 1)] is a word in the free commutative monoid on the names; the context maps it to an exponent vector by adding the exponents of each variable at its index. This map is a monoid homomorphism (concatenating words adds exponent vectors), so repeated factors of one variable combine as , and permuted factors give the same monomial.
Substitution is the universal property
Let be commutative. For any choice of polynomials there is exactly one ring homomorphism that fixes and sends , namely
It is a homomorphism. Additivity holds by construction. On monomials,
and both sides of are bilinear in , so the identity extends from monomials to all polynomials.
It is unique. A homomorphism fixing is determined on by multiplicativity and on sums by additivity, so its values on determine it.
A substitution assigns replacements to a subset of the variables. It determines the images
where a scalar is read as the constant polynomial , and hence the homomorphism . substitute(σ) computes term by term exactly as in the formula: each factor becomes (not replaced), the constant (scalar), or (polynomial).
Design decisions
Simultaneous, one-pass substitution
Problem. Given , should become or ?
Choice. Simultaneous: every is taken from as given, and the replacement polynomials are not themselves substituted into. This is the homomorphism above, so
The sequential reading is a composition of two homomorphisms, and the composition rule
holds because both sides are homomorphisms fixing that agree on every . To substitute sequentially, call substitute twice: . Simultaneous substitution is the primitive because it is order-independent: permuting the entries of cannot change the result.
Duplicate entries are errors, not overrides
A list that maps the same variable twice does not define a function . Rather than picking the first or last entry, substitute_checked returns None (and substitute aborts). The same rule applies after name resolution, so two different Name values with the same text, or the same name twice, are rejected.
Partial evaluation keeps the context
Problem. After assigning in , the result does not depend on . It could live in or stay in .
Choice. eval_partial is substitution with scalar images only, so its result is in with the same context. The assigned variables simply have degree . Keeping means the result can be added to, multiplied with and substituted into other polynomials over without any context surgery, and it makes partial evaluation compose with full evaluation. Write for a partial assignment on and for an assignment of the remaining variables. Then
because both sides are homomorphisms fixing , and on generators for and otherwise. Any value gives to an already assigned variable is irrelevant, which is why the examples evaluate the partial result with x = 0.
Names from type_theory resolve to variables first
substitute_names and eval_partial_named map every Name to a variable with VariableContext::variable_by_type_theory_name and then call the variable-based form. The name round trip guarantees that this is lossless inside one context: resolving v.to_type_theory_name() gives back v. An unknown name makes the whole call fail, so a typo can never silently leave a variable unreplaced. No capture can occur, since polynomials have no binders.
What makes a call invalid
Each checked operation returns None exactly in these situations:
| Operation | Rejected when |
|---|---|
from_named_terms_*_checked, variable_checked | a variable is not contained in the context |
eval_named_checked | a variable is outside the context; a variable with index below arity() is unassigned or assigned twice |
substitute_checked, eval_partial_checked | a variable is outside the context or listed twice; a replacement polynomial has a different context |
*_names_checked | additionally, a name is not in the context |
add_checked, mul_checked | the contexts differ |
The Option result records only that the call failed. The aborting forms check the same conditions.
Storage is chosen at construction, results are storage-independent
The polynomial is held as a term array or a sparse map behind a private enum, selected by from_named_terms_as_terms / _as_sparse or by binding an existing TermPolynomial / SparsePolynomial. Both storages satisfy the same canonical-form invariants, so every observable result (terms as a set, evaluation, substitution) is the same; only costs and the order of to_terms() differ. Binary operations keep the storage when both operands agree and use sparse storage for mixed operands, converting the term-stored side. Substitution and the constructors constant and variable produce sparse storage.
Contexts must be equal, not merged
Binary operations require equal contexts. Merging and automatically would need an injection of both into a union context and a renumbering of every exponent vector, and the union is not unique when the same name appears at different positions. Requiring equality keeps every operation a plain operation in one ring . Because contexts compare structurally, polynomials built from separately created but identical contexts combine freely.
Correctness / invariants
- Context invariant. Every exponent vector of a context polynomial has length at most . The named constructors guarantee it.
from_term_polynomialandfrom_sparse_polynomialdo not check it; a polynomial that violates it makeseval_named(_checked)andto_stringabort. Callers must ensurepolynomial.arity() <= context.size(). - Substitution computes , the unique ring endomorphism with , for commutative coefficients; the result is canonical (zero terms vanish, as when substituting ).
- Partial evaluation satisfies .
- Named evaluation equals indexed evaluation at the values listed by index.
- Arithmetic is that of the underlying storage and inherits its laws.
- Cost. Substitution evaluates every term as a product of powers and adds it to an accumulator; each addition renormalizes the accumulator. For terms and a result of terms, the additions alone cost , on top of the polynomial products. Named lookups are linear in the context size.
Alternatives rejected
- Sequential substitution. Its result depends on the order of the entries, and it is expressible as two simultaneous substitutions.
- Projecting away assigned variables. It would change the context of the result and break composition with other polynomials over .
- Last-entry-wins for duplicates. It hides mistakes; rejecting duplicates keeps a function.
- Using
type_theorysubstitution machinery. Its capture-avoiding substitution solves a problem polynomials do not have, and its terms would not carry polynomial canonical forms. - Implicit context union. See above: not unique, and it would make every binary operation renumber exponents.
Boundaries
- No equality instance; compare
context()and the term lists explicitly. - No dense univariate storage; a context polynomial is always multivariate.
- No elimination of variables from a context, and no renaming or reordering of contexts.
- Substitution needs commutative coefficients to be a homomorphism; the code does not check commutativity.
- Errors carry no reason (
Option), andfrom_term_polynomial/from_sparse_polynomialtrust their arity.