Problem
Context merging is currently fragile and has caused recurring bugs (at least 10 instances) when one Context's bindings need to be visible in another Context's compilation/instantiation. The latest manifestation is #1021: the OPS Wheeler recurrence had to thread the Riccati muntz.context all the way down through three constructor layers because Context.registerFunction("m", m) discards the source Expression on the mapping, and any inner self-instance allocated lazily by the compiled class has no way to resolve m from the OPS-local context.
Context.mergeFrom(Context) exists but only handles the easy case: same name → same instance is coalesced; same name → different instance throws CompilerException. There is no rename, no scope qualification, no conflict-resolution strategy. Callers are forced to either (a) thread one context through every constructor (current OPS workaround), (b) accept the failure mode, or (c) write ad-hoc copy loops at every site.
Root Cause: Context Has No Package Identity
The fundamental problem is that Context has no notion of identity as a mathematical object — it is a flat bag of name→binding mappings with no scope hierarchy and no way to express "these bindings form a coherent unit." Merge is fragile because there is no first-class concept of a context package that defines the boundary of what merges atomically. Until Context has that self-knowledge, mergeFrom is just string-matching on a flat map.
This must be solved before the merge semantics in the sections below, and independently of any disk-caching of compiled classes (that is a downstream consequence, not a prerequisite).
Prerequisite Fix: Context.packageName + Expression.internalName()
Step 1 — Add packageName to Context
public String packageName = null;
public Context(String packageName) {
this();
this.packageName = packageName;
}
Context callers that represent named mathematical objects (e.g. Jacobi polynomials, Riccati solvers) pass the package at construction:
context = new Context("arb.jacobi");
Step 2 — Add internalName() to Expression
Expression.className stays as the short name ("P", "a", etc.) — that is what the functions map, FunctionMapping.functionName, and all cross-expression symbolic references use. A single derived accessor computes the bytecode-level identity:
public String internalName() {
return (context != null && context.packageName != null)
? context.packageName.replace('.', '/') + "/" + className
: className;
}
Step 3 — All ASM emit sites use internalName() instead of className
Every ClassWriter.visit(...), visitTypeInsn(NEW, ...), visitFieldInsn(..., owner, ...), visitMethodInsn(..., owner, ...), and every field descriptor of the form "L" + className + ";" in Expression.generate() must use internalName() instead of bare className. Since Expression owns the ClassWriter, all these sites are local to it.
Step 4 — registerBytecodes is called with internalName()
Wherever Expression.generate() calls classLoader.registerBytecodes(className, bytecodes), change to classLoader.registerBytecodes(internalName(), bytecodes). Now pendingBytecodes is keyed by e.g. "arb/jacobi/P".
Step 5 — Normalize the key in ExpressionClassLoader.findClass
The JVM calls findClass("arb.jacobi.P") (dots). pendingBytecodes is keyed with slashes. One normalization line fixes it:
byte[] bytecodes = pendingBytecodes.remove(name.replace('.', '/'));
Result
className remains the short symbolic name used throughout expression logic. internalName() is the bytecode-level identity. Two contexts with different packageName values can never have a genuine class identity clash — arb.jacobi.P and arb.riccati.P are different classes. This gives the merge semantics below a coherent foundation: merging two contexts means merging two named mathematical objects, not colliding flat symbol tables.
What is needed (merge semantics, now unblocked)
A first-class Context merge feature that handles every realistic case:
- Identical bindings — same name, same instance → silent coalesce (current behaviour, keep).
- Compatible bindings — same name, different but equivalent instances (same class, same expression source) → coalesce with a configurable policy: prefer-this | prefer-other | error.
- Name clashes with rename — same name, genuinely different bindings → automatically rename the incoming binding (e.g.
m → m_2) and rewrite every dependent Expression / FunctionMapping that referenced the old name in the incoming context. The rename must propagate through:
Expression.referencedFunctions keys and node graphs
Expression.expressionString / source representation
- Every downstream
FunctionMapping instance whose compiled class field of that name was emitted
- Already-instantiated functions: their reflective field for the renamed binding has to be re-bound, or a fresh instance allocated and wired in
- Unrenameable clashes — when rename is impossible (e.g. the incoming binding's name appears in a hand-written Java field, or its rewrite would collide with an already-bound name in this context that is itself unrenameable) → throw a structured exception that names both sides and explains why rename failed.
- Variable AND function bindings — both namespaces must be handled. A variable named
p and a function named p can coexist (different field descriptors), so the merge must operate per-namespace.
- Transitive dependencies — when an incoming
FunctionMapping's expression references functions that are themselves in the incoming context, those must be merged together as a unit (topological merge), not one at a time, so partially-merged contexts never exist.
API sketch
public enum ConflictPolicy { PREFER_THIS, PREFER_OTHER, RENAME_INCOMING, ERROR }
public MergeReport mergeFrom(Context other, ConflictPolicy policy);
public record MergeReport(
Set<String> coalesced, // names that matched identically
Map<String, String> renamed, // old name → new name in `this`
Set<String> imported, // names new to `this`
List<MergeFailure> failures // populated only with policy=ERROR or unrenameable
) {}
Acceptance criteria
- All existing
mergeFrom(Context) callers continue to work with the default policy.
- A new test class
ContextMergeTest covers each of the five cases above.
- The OPS
#1021 workaround (threading muntz.context through three constructor layers) can be replaced by opsContext.mergeFrom(muntz.context, RENAME_INCOMING) at the construction site, and the OPS test passes without the constructor threading.
- The variable-namespace and function-namespace fixtures both have rename coverage.
- Documentation: a
Context.md describing the merge model, with a worked example covering each policy.
References
arb.expressions.Context.mergeFrom(Context) — current implementation
arb.expressions.Context.registerFunctionMapping(...) — see comment about replace=true semantics (Context.java:485-498) for the precedent that already partially handles overwrite
arb.functions.polynomials.orthogonal.complex.OrthogonalPolynomialMomentFunctionalSequence — current #1021 workaround that this issue would obsolete
arb.functions.complex.RiccatiMuntzPadeFunctional.partialDerivativeWithRespectToV — another caller that depends on careful context lifetime management
arb.expressions.ExpressionClassLoader — pendingBytecodes and findClass are the load-side of the package identity fix
Problem
Context merging is currently fragile and has caused recurring bugs (at least 10 instances) when one Context's bindings need to be visible in another Context's compilation/instantiation. The latest manifestation is
#1021: the OPS Wheeler recurrence had to thread the Riccatimuntz.contextall the way down through three constructor layers becauseContext.registerFunction("m", m)discards the sourceExpressionon the mapping, and any inner self-instance allocated lazily by the compiled class has no way to resolvemfrom the OPS-local context.Context.mergeFrom(Context)exists but only handles the easy case: same name → same instance is coalesced; same name → different instance throwsCompilerException. There is no rename, no scope qualification, no conflict-resolution strategy. Callers are forced to either (a) thread one context through every constructor (current OPS workaround), (b) accept the failure mode, or (c) write ad-hoc copy loops at every site.Root Cause: Context Has No Package Identity
The fundamental problem is that
Contexthas no notion of identity as a mathematical object — it is a flat bag of name→binding mappings with no scope hierarchy and no way to express "these bindings form a coherent unit." Merge is fragile because there is no first-class concept of a context package that defines the boundary of what merges atomically. UntilContexthas that self-knowledge,mergeFromis just string-matching on a flat map.This must be solved before the merge semantics in the sections below, and independently of any disk-caching of compiled classes (that is a downstream consequence, not a prerequisite).
Prerequisite Fix:
Context.packageName+Expression.internalName()Step 1 — Add
packageNametoContextContextcallers that represent named mathematical objects (e.g. Jacobi polynomials, Riccati solvers) pass the package at construction:Step 2 — Add
internalName()toExpressionExpression.classNamestays as the short name ("P","a", etc.) — that is what thefunctionsmap,FunctionMapping.functionName, and all cross-expression symbolic references use. A single derived accessor computes the bytecode-level identity:Step 3 — All ASM emit sites use
internalName()instead ofclassNameEvery
ClassWriter.visit(...),visitTypeInsn(NEW, ...),visitFieldInsn(..., owner, ...),visitMethodInsn(..., owner, ...), and every field descriptor of the form"L" + className + ";"inExpression.generate()must useinternalName()instead of bareclassName. SinceExpressionowns theClassWriter, all these sites are local to it.Step 4 —
registerBytecodesis called withinternalName()Wherever
Expression.generate()callsclassLoader.registerBytecodes(className, bytecodes), change toclassLoader.registerBytecodes(internalName(), bytecodes). NowpendingBytecodesis keyed by e.g."arb/jacobi/P".Step 5 — Normalize the key in
ExpressionClassLoader.findClassThe JVM calls
findClass("arb.jacobi.P")(dots).pendingBytecodesis keyed with slashes. One normalization line fixes it:Result
classNameremains the short symbolic name used throughout expression logic.internalName()is the bytecode-level identity. Two contexts with differentpackageNamevalues can never have a genuine class identity clash —arb.jacobi.Pandarb.riccati.Pare different classes. This gives the merge semantics below a coherent foundation: merging two contexts means merging two named mathematical objects, not colliding flat symbol tables.What is needed (merge semantics, now unblocked)
A first-class
Contextmerge feature that handles every realistic case:m→m_2) and rewrite every dependentExpression/FunctionMappingthat referenced the old name in the incoming context. The rename must propagate through:Expression.referencedFunctionskeys and node graphsExpression.expressionString/ source representationFunctionMappinginstance whose compiled class field of that name was emittedpand a function namedpcan coexist (different field descriptors), so the merge must operate per-namespace.FunctionMapping'sexpressionreferences functions that are themselves in the incoming context, those must be merged together as a unit (topological merge), not one at a time, so partially-merged contexts never exist.API sketch
Acceptance criteria
mergeFrom(Context)callers continue to work with the default policy.ContextMergeTestcovers each of the five cases above.#1021workaround (threadingmuntz.contextthrough three constructor layers) can be replaced byopsContext.mergeFrom(muntz.context, RENAME_INCOMING)at the construction site, and the OPS test passes without the constructor threading.Context.mddescribing the merge model, with a worked example covering each policy.References
arb.expressions.Context.mergeFrom(Context)— current implementationarb.expressions.Context.registerFunctionMapping(...)— see comment aboutreplace=truesemantics (Context.java:485-498) for the precedent that already partially handles overwritearb.functions.polynomials.orthogonal.complex.OrthogonalPolynomialMomentFunctionalSequence— current#1021workaround that this issue would obsoletearb.functions.complex.RiccatiMuntzPadeFunctional.partialDerivativeWithRespectToV— another caller that depends on careful context lifetime managementarb.expressions.ExpressionClassLoader—pendingBytecodesandfindClassare the load-side of the package identity fix