Generated reference › API — ICoreBlocks/ICoreMath/Symbolic
kind: generated#api#icoreblocks-icoremath-symbolic

API — ICoreBlocks/ICoreMath/Symbolic

The public contract of 1 header(s) under src/ICoreBlocks/ICoreMath/Symbolic — 1 class/struct definition(s), 42 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.

HeaderDefinesDeclarationsBases
ICoreSymbolic.hICoreSymbolic42—

ICoreSymbolic.h#

src/ICoreBlocks/ICoreMath/Symbolic/ICoreSymbolic.h

ICoreSymbolic#

ICoreSymbolic.h:46 · class · pImpl · 42 declaration(s)

The console's SYMBOLIC value: MATLAB's sym (toolbox rows T4.1-T4.5, T4.11, T4.12, T4.14, over decision T0.5 (A)).

class ICoreSymbolic {
public:

    ICoreSymbolic();
    ~ICoreSymbolic();

    // Symbolic values are stored in ICoreValue by value and copied through
    // every expression, so all four are written out in the .cpp (the
    // unique_ptr residue deletes the implicit ones).
    ICoreSymbolic(const ICoreSymbolic& other);
    ICoreSymbolic& operator=(const ICoreSymbolic& other);
    ICoreSymbolic(ICoreSymbolic&& other) noexcept;
    ICoreSymbolic& operator=(ICoreSymbolic&& other) noexcept;

    // ---- construction -------------------------------------------------------

    // `sym('x')` -- a free variable. The name must be a valid identifier;
    // MATLAB refuses `sym('2x')` with "Each character vector or string in
    // input arguments must specify a variable or a number", and so does the
    // caller, using isVariableName().
    static ICoreSymbolic symbol(const std::string& name);
    static bool isVariableName(const std::string& text);

    // `sym(3)`, `sym(1/3)`, `sym(0.1)` -- MATLAB's DEFAULT `'r'` conversion,
    // which is a rational RECOGNITION and not a cast (T4.1's trap):
    //
    //     sym(0.1)         1/10          a short rational that reproduces it
    //     sym(pi)          pi            a rational multiple of pi
    //     sym(sqrt(2))     2^(1/2)       a square root of a short rational
    //     sym(1.23456789)  5559999489367579/4503599627370496
    //                                    nothing short reproduces it, so the
    //                                    EXACT binary value of the double
    //
    // all four measured against R2026a. `flag` is 'r' (the default), 'f' (the
    // exact binary rational always), 'd' (a decimal of `digits` places) or
    // 'e' -- which is refused, naming T4.15: `eps/40 + 1/10` needs the symbol
    // `eps` and an error term this console has no row for.
    static ICoreSymbolic number(double value);
    static bool numberWithFlag(double value, char flag, ICoreSymbolic& out,
                               std::string* whyNot = nullptr);
    static ICoreSymbolic integer(long long value);
    static ICoreSymbolic rational(long long numerator, long long denominator);

    // `str2sym('x^2 + 1')` (T4.14) -- and the reader for every stored Sym, so
    // a value that round-trips through the workspace comes back as itself.
    // The grammar is the console's own: numbers, names, + - * / ^, unary
    // minus, parentheses, function calls, and the imaginary unit `1i`/`i`.
    // `allowAbstractCalls` lets `y(t)` through as an APPLICATION of a symbolic
    // function that has no formula (T4.9, T4.17). It is off for `str2sym`,
    // where an unknown name is a typo and saying so is the useful answer, and
    // on for the variables space reading back its own stored text -- which is
    // machine-written and is the only place a stored symfun comes from.
    static bool parse(const std::string& text, ICoreSymbolic& out,
                      std::string* whyNot = nullptr, bool allowAbstractCalls = false);

    // `char(f)` for a symfun is its FORMULA; this is its binding, and the
    // round-trip form the variables space stores. Identical to text() for
    // everything that is not a symfun.
    std::string storageText() const;

    // ---- shape --------------------------------------------------------------

    size_t rows() const;
    size_t cols() const;
    bool   isScalar() const;
    // Element (r, c), as a 1 x 1 Sym. Out of range answers the zero scalar,
    // which no caller should reach -- every one of them checks the shape.
    ICoreSymbolic at(size_t row, size_t column) const;
    // A rows x columns Sym from its elements, ROW BY ROW. Every element must
    // itself be a scalar; false with `whyNot` otherwise (a matrix of matrices
    // is not a shape MATLAB has either).
    static bool fromElements(const std::vector<ICoreSymbolic>& elements,
                             size_t rows, size_t columns, ICoreSymbolic& out,
                             std::string* whyNot = nullptr);

    // ---- printing -----------------------------------------------------------

    // `char(expr)`, and what the console echoes for the value (T4.1).
    //
    // ⚠ **THE TERM ORDER IS THE CONSOLE'S OWN, and it agrees with MATLAB's on
    // the ordinary cases rather than by construction.** MATLAB has no
    // documented print order -- it prints from its internal DAG, so
    // `x^2 + 3*x + 1` comes back as `3*x + x^2 + 1` there while
    // `expand((x+1)^3)` comes back as `3*x + 3*x^2 + x^3 + 1`. Both are
    // ASCENDING in the degree of the main variable with the constant last, and
    // that is the rule transcribed here (measured on R2026a 2026-09-04, and
    // the same measurements are on T4.1's outcome note). Parity does not rest
    // on it: T0.5 compares MEANING through `isAlways`, never text.
    std::string text() const;
    // `latex(expr)` -- MATLAB's own LaTeX spelling: \frac, \sqrt, \left( ...
    std::string latex() const;
    // `pretty(expr)` -- the 2-D character layout, as lines.
    std::vector<std::string> pretty() const;
    // The body of `matlabFunction(expr)`: MATLAB's ELEMENTWISE spelling
    // (`.*`, `./`, `.^`) with numbers printed the way MATLAB prints them in a
    // generated handle (`1.0`, not `1`). T4.14's trap, transcribed.
    std::string functionBody() const;

    // ---- structure ----------------------------------------------------------

    // `symvar(expr)` -- every free variable, ALPHABETICALLY (MATLAB's order).
    std::vector<std::string> variables() const;
    // `symvar(expr, 1)` -- MATLAB's "closest to x": nearest in the alphabet,
    // and the letter AFTER x wins a tie (`symvar(w + y, 1)` is y, measured).
    // "" when the expression holds no variable.
    static std::string closestToX(const std::vector<std::string>& names);
    // The default variable of an expression -- closestToX(variables()) -- and
    // what `diff(f)`, `int(f)` and `coeffs(f)` use when none is given.
    std::string defaultVariable() const;

    bool isNumber() const;                       // a scalar with no variable
    // `double(expr)`: false when a variable survives, with MATLAB's own
    // sentence ("Unable to convert expression containing symbolic variables
    // into double array. Apply 'subs' function first ...").
    bool toDouble(double& out, std::string* whyNot = nullptr) const;
    // `sym2poly(p)`: the coefficients HIGHEST POWER FIRST. Refuses a
    // non-polynomial and a polynomial in more than one variable, with MATLAB's
    // two sentences.
    bool polynomialCoefficients(std::vector<double>& descending,
                                std::string* whyNot = nullptr) const;

    // ---- arithmetic ---------------------------------------------------------
    //
    // Scalar-only: a non-scalar operand refuses naming T4.13, except that a
    // SCALAR combines with a matrix elementwise, which is what `2*sym([1 2])`
    // means on both sides.
    static bool add(const ICoreSymbolic& a, const ICoreSymbolic& b, ICoreSymbolic& out,
                    std::string* whyNot = nullptr);
    static bool subtract(const ICoreSymbolic& a, const ICoreSymbolic& b, ICoreSymbolic& out,
                         std::string* whyNot = nullptr);
    static bool multiply(const ICoreSymbolic& a, const ICoreSymbolic& b, ICoreSymbolic& out,
                         std::string* whyNot = nullptr);
    static bool divide(const ICoreSymbolic& a, const ICoreSymbolic& b, ICoreSymbolic& out,
                       std::string* whyNot = nullptr);
    static bool power(const ICoreSymbolic& a, const ICoreSymbolic& b, ICoreSymbolic& out,
                      std::string* whyNot = nullptr);
    static bool negate(const ICoreSymbolic& a, ICoreSymbolic& out,
                       std::string* whyNot = nullptr);
    // One of the elementary functions this engine knows, applied to a Sym:
    // sin cos tan cot sec csc asin acos atan sinh cosh tanh asinh acosh atanh
    // exp log log2 log10 sqrt, and T4.19's gamma psi erf erfc factorial
    // nchoosek. An unknown name is refused BY NAME rather than carried as an
    // opaque node, because an opaque node is what `diff` would then have to
    // guess at -- and that is why `besselj`, `zeta` and `hypergeom` are not
    // here: T4.19 puts them out of scope (Z8), there being no numeric value
    // on this console to `double()` them with.
    static bool applyFunction(const std::string& name, const std::vector<ICoreSymbolic>& args,
                              ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool knowsFunction(const std::string& name);
    // The known names a call may give ANY number of arguments (T4.16's
    // `piecewise` and `dirac`). Asked before `takesTwoArguments`.
    static bool takesManyArguments(const std::string& name);
    // The known names a call may give TWO arguments (T4.19's `nchoosek`).
    // The console asks before it routes a call to the symbolic arm, because
    // its own numeric `nchoosek(n, k)` is a different function of the same
    // name and only the kinds of the arguments tell them apart.
    static bool takesTwoArguments(const std::string& name);

    // Structural equality after canonical simplification -- `x + 1` and
    // `1 + x` are equal, `(x+1)^2` and `x^2+2*x+1` are NOT (that is what
    // `simplify` is for, and MATLAB's `logical(a == b)` answers the same way).
    bool sameAs(const ICoreSymbolic& other) const;

    // ---- algebra (T4.2) -----------------------------------------------------

    // `expand(expr)` -- distribute products over sums, expand integer powers
    // of sums, and apply the addition formulas for sin/cos/exp/log.
    // `arithmeticOnly` is MATLAB's `'ArithmeticOnly', true`: the arithmetic
    // half alone, so `expand(sin(2*x), 'ArithmeticOnly', true)` stays put.
    static ICoreSymbolic expand(const ICoreSymbolic& expression, bool arithmeticOnly = false);
    // `simplify(expr)` -- the rule set T0.5 named: rational normalisation with
    // gcd cancellation, the Pythagorean identity, exp/log inverses, and the
    // canonical form the constructors already impose. An expression it cannot
    // reduce comes back UNCHANGED, which is MATLAB's contract too.
    static ICoreSymbolic simplify(const ICoreSymbolic& expression);
    // `factor(expr)` -- the factors as a ROW, MATLAB's shape. Polynomials over
    // the rationals (rational roots and square-free content) and integers;
    // `factor(x^2 - 2)` stays whole, exactly as MATLAB leaves it.
    static bool factorize(const ICoreSymbolic& expression, ICoreSymbolic& out,
                          std::string* whyNot = nullptr);
    // `collect(expr, x)` -- like powers of x gathered, coefficients kept
    // symbolic: `collect(a*x + b*x + c, x)` is `(a + b)*x + c`.
    static bool collect(const ICoreSymbolic& expression, const std::string& variable,
                        ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `combine(expr, target)` -- the inverse of expand for one family:
    // "exp", "log", "sincos". MATLAB's no-target form combines nothing on
    // these expressions (measured), so the target is required here and its
    // absence is refused rather than guessed.
    static bool combine(const ICoreSymbolic& expression, const std::string& target,
                        ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `[n, d] = numden(expr)` -- the expression over a common denominator,
    // split. Both come back with no common factor.
    static void numeratorDenominator(const ICoreSymbolic& expression,
                                     ICoreSymbolic& numerator, ICoreSymbolic& denominator);
    // `partfrac(expr, x)` -- the partial-fraction expansion of a rational
    // function over the rationals, INCLUDING repeated real roots. A pole this
    // console cannot place exactly (an irreducible quadratic) keeps its
    // quadratic term, which is what MATLAB does too.
    static bool partialFractions(const ICoreSymbolic& expression, const std::string& variable,
                                 ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `[c, t] = coeffs(p, x)` -- the coefficients and their terms.
    //
    // ⚠ **THE ORDER DEPENDS ON HOW MANY OUTPUTS ARE ASKED FOR**, which is not
    // a rule anyone would guess and cost this row a parity failure. Measured
    // on R2026a 2026-09-05:
    //
    //     coeffs(2*x^3 + 5*x, x)              [5, 2]         ASCENDING
    //     [c, t] = coeffs(2*x^3 + 5*x, x)     [2, 5]  [x^3, x]   descending
    //     coeffs(2*x^3 + 5*x, x, 'All')       [2, 0, 5, 0]       descending
    //     coeffs(a*x^2 + b, x)                [b, a]         ASCENDING
    //
    // This function answers the DESCENDING pair, which is the two-output form
    // and the `'All'` form; the console's one-output arm reverses it, and the
    // measurement is written out beside that reversal too.
    static bool coefficients(const ICoreSymbolic& expression, const std::string& variable,
                             bool all, ICoreSymbolic& values, ICoreSymbolic& terms,
                             std::string* whyNot = nullptr);
    // `rewrite(expr, target)` -- "exp", "sincos", "sqrt", "log". Rewriting the
    // trigonometric functions to exponentials needs the imaginary unit, which
    // this tree has.
    static bool rewrite(const ICoreSymbolic& expression, const std::string& target,
                        ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `horner(p, x)` -- the nested form, `x*(x + 3) + 1`.
    static bool horner(const ICoreSymbolic& expression, const std::string& variable,
                       ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ---- the matrix algebra (T4.13) -----------------------------------------
    //
    // Everything here works over the field of rational FUNCTIONS -- the
    // entries may be expressions -- so `det(sym('A', [2 2]))` is
    // `A1_1*A2_2 - A1_2*A2_1` rather than a number. Elimination pivots on an
    // entry the engine cannot see to be zero, which is what MATLAB does too
    // and is why `rank(sym('A', [2 2]))` is 2: nothing says A1_1 is zero.

    // `sym('A', [2 2])` names its entries A1_1 ... A2_2; a pattern holding a
    // format directive is used as one, so `sym('a%d%d', [2 2])` gives a11.
    static bool namedMatrix(const std::string& pattern, size_t rows, size_t columns,
                            ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool matrixProduct(const ICoreSymbolic& a, const ICoreSymbolic& b,
                              ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool transposed(const ICoreSymbolic& a, ICoreSymbolic& out,
                           std::string* whyNot = nullptr);
    static bool determinant(const ICoreSymbolic& a, ICoreSymbolic& out,
                            std::string* whyNot = nullptr);
    static bool adjugate(const ICoreSymbolic& a, ICoreSymbolic& out,
                         std::string* whyNot = nullptr);
    // ⚠ A SINGULAR MATRIX IS NOT AN ERROR ON EITHER SIDE, and the two
    // answers DIFFER -- a divergence measured rather than assumed. MATLAB
    // answers [Inf, 0; 0, Inf] for inv(sym([1 2; 2 4])) (with a warning); this
    // console answers [Inf, Inf; Inf, Inf], because every entry of the
    // adjugate is divided by an exact zero and that is Inf here. Both say the
    // matrix has no inverse; neither number means anything.
    static bool inverse(const ICoreSymbolic& a, ICoreSymbolic& out,
                        std::string* whyNot = nullptr);
    static bool rankOf(const ICoreSymbolic& a, size_t& rank, std::string* whyNot = nullptr);
    // ⚠ A FULL-COLUMN-RANK MATRIX HAS AN EMPTY NULL SPACE, which MATLAB
    // answers with an empty sym and this console has no value for (C3.9), so
    // it says so rather than answering something else.
    static bool nullSpace(const ICoreSymbolic& a, ICoreSymbolic& out,
                          std::string* whyNot = nullptr);
    // `charpoly(A)` is MATLAB's COEFFICIENT ROW, highest power first;
    // `charpoly(A, x)` is the polynomial in x. An empty variable asks for the
    // row.
    static bool characteristicPolynomial(const ICoreSymbolic& a, const std::string& variable,
                                         ICoreSymbolic& out, std::string* whyNot = nullptr);
    // The roots of the characteristic polynomial, so this inherits T4.8's
    // scope exactly -- radicals for a quadratic, `root(...)` beyond -- which
    // is also what MATLAB answers.
    static bool eigenvalues(const ICoreSymbolic& a, ICoreSymbolic& out,
                            std::string* whyNot = nullptr);

    // ---- sums and products (T4.7) -------------------------------------------

    // `symsum(f, k, a, b)` and `symprod(f, k, a, b)` (`product` chooses).
    // SCOPE: two whole-number bounds evaluate (up to ten thousand terms); a
    // symbolic upper bound closes a SUM of a polynomial in k, each power
    // through its own Faulhaber polynomial -- reached by interpolation rather
    // than by Bernoulli numbers, so the arithmetic stays in this engine's own
    // rationals -- and a PRODUCT that is free of the index or is the index
    // itself. An infinite bound answers 1/k^2, 1/k^4, 1/k^6, the divergent
    // 1/k, a geometric series with a NUMERIC ratio, and x^k/k!.
    //
    // ⚠ 1/k^3 IS zeta(3) AND IS REFUSED BY NAME. MATLAB answers it; this
    // console has no zeta (T4.19, Z8), and the refusal says which powers do
    // answer rather than leaving the user to guess.
    static bool sumOf(const ICoreSymbolic& body, const std::string& index,
                      const ICoreSymbolic& lower, const ICoreSymbolic& upper,
                      bool product, ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `symsum(f)` with no bounds -- the ANTIDIFFERENCE, which is the sum from
    // 1 to k - 1: MATLAB answers `k^2/2 - k/2` for `symsum(k)` (measured).
    static bool indefiniteSum(const ICoreSymbolic& body, const std::string& index,
                              bool product, ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ---- limits and series (T4.6) -------------------------------------------

    // `limit(f, x, a)` and `limit(f, x, a, 'left' | 'right')` -- `side` is -1,
    // 0 or +1. The SCOPE, stated because everything outside it answers NaN the
    // way MATLAB's does: substitution where the expression is continuous; a
    // rational function's removable singularity by cancellation; the ratio of
    // two Taylor series where both vanish (sin(x)/x, (1 - cos(x))/x^2,
    // (exp(x) - 1)/x); an infinity whose SIGN a one-sided approach knows; and
    // at +-Inf, the degrees of a rational function, the ordering
    // exp > any power > log, a bounded numerator over a growing denominator,
    // and (1 + a/x)^(b*x).
    //
    // ⚠ A ONE-SIDED LIMIT IS DONE BY SUBSTITUTION rather than by a second
    // algorithm: `x -> a` from the left is `x = a - t` with `t` assumed
    // POSITIVE, and heaviside, sign and abs then fold themselves through
    // T4.15's prover. That is why this row needed the assumption set.
    static bool limitOf(const ICoreSymbolic& expression, const std::string& variable,
                        const ICoreSymbolic& point, int side, ICoreSymbolic& out,
                        std::string* whyNot = nullptr);

    // `taylor(f, x)`, `taylor(f, x, a)`, `taylor(f, x, 'Order', n)`.
    // ⚠ MATLAB'S 'Order' IS AN ORDER AND NOT A DEGREE -- the default 6 gives
    // terms up to x^5, which is the trap this row records.
    static bool taylorOf(const ICoreSymbolic& expression, const std::string& variable,
                         const ICoreSymbolic& point, int order, ICoreSymbolic& out,
                         std::string* whyNot = nullptr);

    // ---- solving (T4.8) -----------------------------------------------------

    // `solve(eqn, x)`. The answer is a COLUMN, MATLAB's shape, and the SCOPE
    // is stated because everything outside it is refused by name: polynomials
    // with rational coefficients exactly (rational roots, then radicals for a
    // quadratic factor and MATLAB's own `root(p, z, k)` notation for an
    // irreducible one of higher degree -- which is what MATLAB writes for
    // `solve(x^5 - x + 1)` too); linear and quadratic polynomials whose
    // COEFFICIENTS are expressions; rational equations through their
    // numerator, with a root that vanishes the denominator dropped; and
    // equations an elementary function can be peeled off one at a time.
    static bool solve(const ICoreSymbolic& equation, const std::string& variable,
                      ICoreSymbolic& out, std::string* whyNot = nullptr);

    // `[x, y] = solve(eq1, eq2, x, y)` -- a SQUARE LINEAR system, solved by
    // Gaussian elimination over the expressions themselves, so the
    // coefficients may be symbols: solve(a*x + b*y == c, x - y == 0, x, y)
    // answers in a, b and c. A variable appearing to a power, or two unknowns
    // multiplying each other, is refused naming the row.
    static bool solveSystem(const std::vector<ICoreSymbolic>& equations,
                            const std::vector<std::string>& variables,
                            std::vector<ICoreSymbolic>& out, std::string* whyNot = nullptr);

    // ---- relations and assumptions (T4.8's seam, T4.15) ---------------------

    // `x == 1`, `x^2 >= 0` -- a RELATION is a symbolic value on both sides
    // (`class(x == 1)` is `sym` in MATLAB, measured), not a true/false. Only
    // four relations are ever stored: MATLAB rewrites `>` and `>=` by swapping
    // the sides, which is why `char(x^2 >= 0)` reads `0 <= x^2` there and
    // here. `op` is "eq", "ne", "lt", "le", "gt" or "ge" going in.
    static bool relation(const std::string& op, const ICoreSymbolic& a, const ICoreSymbolic& b,
                         ICoreSymbolic& out, std::string* whyNot = nullptr);
    bool isRelation() const;
    bool relationParts(std::string& op, ICoreSymbolic& left, ICoreSymbolic& right) const;
    // `logical(cond)` (mathematically = false) and `isAlways(cond)` (true).
    //
    // ⚠ THE TWO ARE DIFFERENT QUESTIONS AND MATLAB ANSWERS THEM DIFFERENTLY.
    // `logical` is STRUCTURAL: `logical(x + 1 == 1 + x)` is true because both
    // sides canonicalise the same, and `logical((x+1)^2 == x^2+2*x+1)` is
    // FALSE even though the two are the same function. `isAlways` is the one
    // that tries to prove it, and answers true there.
    //
    // ⚠ AND FAILING TO PROVE IS A LEGAL ANSWER: MATLAB warns "Unable to prove"
    // and returns false. So does this, for everything outside the four rules
    // the .cpp states beside `provableSign`.
    bool decide(bool mathematically, bool& answer, std::string* whyNot = nullptr) const;

    // `assume(x, 'real')`, `syms x positive`, `assume(x, 'clear')`.
    // `replace` is what `assume` does and `assumeAlso` does not.
    static void assume(const std::string& name, const std::string& fact, bool replace);
    // `assume(x > 0)` and `assumeAlso(x > 2)` -- a bound rather than a class.
    static bool assumeRelation(const ICoreSymbolic& condition, bool replace,
                               std::string* whyNot = nullptr);
    // `assumptions(x)`, in MATLAB's own spelling: `in(x, 'real')` for a class
    // fact and the relation itself for a bound (measured).
    static std::vector<std::string> assumptionsOf(const std::string& name);
    // Every symbol that carries one, for the bare `assumptions`.
    static std::vector<std::string> assumedSymbols();
    // `assume(x, 'clear')`, and `syms x` re-declaring a symbol. An empty name
    // clears the whole table, which is what `clear all` means for it.
    static void clearAssumptions(const std::string& name);

    // ---- number theory and fractions (T4.18) --------------------------------

    // `gcd(a, b)` and `lcm(a, b)`, elementwise over the shapes the operators
    // take. One rule covers three kinds of argument, and it is NOT the gcd
    // over the field of rationals: MATLAB multiplies the gcd of the CONTENTS
    // by the gcd of the PRIMITIVE parts, so `gcd(2*x^2 - 2, 4*x + 4)` is
    // `2*x + 2` and not `x + 1`, `gcd(sym(1)/2, sym(1)/3)` is `1/6`, and
    // `gcd(x^2 - 1, sym(2))` is 1. A polynomial in MORE THAN ONE variable is
    // refused naming the row (MATLAB answers `gcd(x*y, x^2)` with `x`).
    static bool greatestCommonDivisor(const ICoreSymbolic& a, const ICoreSymbolic& b,
                                      ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool leastCommonMultiple(const ICoreSymbolic& a, const ICoreSymbolic& b,
                                    ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `[g, c, d] = gcd(a, b)` -- Bezout's identity, `a*c + b*d == g`, over two
    // integers. MATLAB's numeric gcd has this form too and this console had
    // neither until T4.18.
    static bool bezout(const ICoreSymbolic& a, const ICoreSymbolic& b, ICoreSymbolic& divisor,
                       ICoreSymbolic& leftFactor, ICoreSymbolic& rightFactor,
                       std::string* whyNot = nullptr);
    // `divisors(n)` -- every POSITIVE divisor ascending, as a row; and for a
    // polynomial, every product of its factors, unexpanded and with the
    // numeric content dropped (`divisors(2*x)` is `[1, x]`, MATLAB's answer).
    static bool divisorsOf(const ICoreSymbolic& value, ICoreSymbolic& out,
                           std::string* whyNot = nullptr);
    // `simplifyFraction(f)` and `simplifyFraction(f, 'Expand', false)`.
    static bool simplifyFraction(const ICoreSymbolic& expression, bool expandResult,
                                 ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ---- substitution and evaluation (T4.3) ---------------------------------

    // `subs(expr, x, 2)` and `subs(expr, [x y], [1 2])`. The answer is a SYM
    // and not a double -- `subs(x^2, x, 2)` is `sym(4)` -- which is T4.3's
    // trap and the reason `double()` exists beside it. A double VALUE is
    // converted by the `'r'` rule on the way in, so `subs(x^2, x, 0.5)` is
    // `1/4` on both sides.
    static bool substitute(const ICoreSymbolic& expression,
                           const std::vector<std::string>& names,
                           const std::vector<ICoreSymbolic>& values,
                           ICoreSymbolic& out, std::string* whyNot = nullptr);

    // `vpa(expr[, d])` -- every exact number in the expression replaced by a
    // DECIMAL of `digits` significant figures, correctly rounded.
    //
    // The arithmetic behind it is exact: rationals by long division, `pi` and
    // `e` from stored digits, and square roots by integer Newton, all in this
    // file's own decimal integers. What it will NOT do is evaluate a
    // transcendental function to more digits than a double holds --
    // `vpa(sin(2), 32)` is refused naming T4.3 -- because printing 32 digits
    // of a 17-digit answer is the one thing `vpa` must never do.
    static bool variablePrecision(const ICoreSymbolic& expression, int digits,
                                  ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ---- calculus (T4.4, T4.5, T4.12) ---------------------------------------

    // `diff(expr, x, n)`. The chain, product and quotient rules over the
    // elementary table; `diff(x^x)` is `x*x^(x-1) + x^x*log(x)`, MATLAB's own
    // answer. Refuses `abs`/`sign` naming T4.16, where MATLAB's answer is in
    // terms of `conj` and this console has no row for it yet.
    static bool differentiate(const ICoreSymbolic& expression, const std::string& variable,
                              int order, ICoreSymbolic& out, std::string* whyNot = nullptr);

    // `int(expr, x)` -- the SCOPE T4.5 names: polynomials and rational
    // functions (through partfrac, to log and atan terms), the elementary
    // antiderivative table, linear substitution f(a*x + b), and integration by
    // parts for x^n against exp/sin/cos/log. Beyond that the answer is the
    // UNEVALUATED `int(f(x), x)`, which is MATLAB's own answer when its engine
    // cannot -- never a wrong closed form.
    static bool integrate(const ICoreSymbolic& expression, const std::string& variable,
                          ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `int(expr, x, a, b)` -- the antiderivative at the two limits. Infinite
    // limits are taken as the limit of the antiderivative where the engine can
    // see it, and refused naming T4.6 where it cannot.
    static bool integrateDefinite(const ICoreSymbolic& expression, const std::string& variable,
                                  const ICoreSymbolic& lower, const ICoreSymbolic& upper,
                                  ICoreSymbolic& out, std::string* whyNot = nullptr);
    // `jacobian(f, v)`, and the four that are built on it: `hessian`,
    // `gradient` (the jacobian of a scalar, as a COLUMN), `divergence`,
    // `curl` and `laplacian`. Each takes the variable list explicitly; the
    // caller supplies symvar's answer when MATLAB would.
    static bool jacobian(const ICoreSymbolic& functions, const std::vector<std::string>& variables,
                         ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool hessian(const ICoreSymbolic& scalar, const std::vector<std::string>& variables,
                        ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool gradient(const ICoreSymbolic& scalar, const std::vector<std::string>& variables,
                         ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool divergence(const ICoreSymbolic& field, const std::vector<std::string>& variables,
                           ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool curl(const ICoreSymbolic& field, const std::vector<std::string>& variables,
                     ICoreSymbolic& out, std::string* whyNot = nullptr);
    static bool laplacian(const ICoreSymbolic& scalar, const std::vector<std::string>& variables,
                          ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ---- symbolic FUNCTIONS (T4.17), and the ODE they carry (T4.9) ----------
    //
    // `f(x) = x^2 + 1` is a symfun: an expression WITH ITS ARGUMENT LIST, so
    // `f(2)` is 5 and `f(t)` is `t^2 + 1`. It is not a second value kind --
    // T0.5 admits one, and X3 landed it -- but a distinguished node inside a
    // `Sym`, which is what lets a symfun be stored, printed, differentiated
    // and crossed to MATLAB through machinery that already exists.
    //
    // ⚠ **`text()` PRINTS THE BODY AND `storageText()` PRINTS THE BINDING, and
    // the two must not be swapped.** MATLAB's `char(f)` for a symfun is its
    // formula (`x1^2 + 1`, measured) and so is what it echoes, so `text()` has
    // to answer that; but the console stores a variable as `sym(<text>)` and
    // reads it back with `parse`, so a symfun stored by its body would come
    // back a plain expression with its arguments gone and `f(2)` would stop
    // being a call. `storageText()` is the round-trip form and the ONLY thing
    // the variables space may write.
    static bool makeFunction(const ICoreSymbolic& body, const std::vector<std::string>& arguments,
                             ICoreSymbolic& out, std::string* whyNot = nullptr);
    bool isFunction() const;
    // `argnames(f)`, in order. Empty for anything that is not a symfun.
    std::vector<std::string> functionArguments() const;
    // `formula(f)` -- the body. A value that is not a symfun IS its own
    // formula, which is MATLAB's answer for a plain sym too.
    ICoreSymbolic functionFormula() const;
    // `f(2)`, `f(t)`, `g(2, 3)` -- positional, one value per argument.
    static bool callFunction(const ICoreSymbolic& function,
                             const std::vector<ICoreSymbolic>& values,
                             ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ⚠ **AN ABSTRACT `y(t)` IS A NODE THIS ENGINE CANNOT EVALUATE, and that
    // is the point.** `syms y(t)` declares a function with no formula, which
    // is what an ODE is written over: `diff(y, t)` then has to answer the
    // UNEVALUATED `diff(y(t), t)` rather than 0 or an error. Such a node is
    // built only here and by `parse` under `allowAbstractCalls` -- never by
    // `str2sym`, where an unknown name is a typo and must stay one.
    static bool makeAbstractFunction(const std::string& name,
                                     const std::vector<std::string>& arguments,
                                     ICoreSymbolic& out, std::string* whyNot = nullptr);
    // The name of the abstract function this symfun applies, "" when its body
    // is an ordinary expression.
    std::string abstractName() const;

    // `finverse(f)` and `finverse(f, x)`: the y with f(y) = x, written in x.
    // MATLAB warns that an inverse is not unique and takes ONE branch; so does
    // this, and it takes the same one (T4.17).
    static bool functionInverse(const ICoreSymbolic& expression, const std::string& variable,
                                ICoreSymbolic& out, std::string* whyNot = nullptr);

    // `dsolve(eqn)` and `dsolve(eqn, conditions)` (T4.9). SCOPE, and it is the
    // decision rather than a limit that was hit: LINEAR equations with
    // CONSTANT coefficients up to second order, by the characteristic
    // polynomial (real, repeated and complex roots, in MATLAB's own `exp`,
    // `t*exp`, `exp*cos`/`exp*sin` forms), plus a first-order linear equation
    // with a variable coefficient through the integrating factor, plus the
    // separable `y' = g(t)h(y)` where both halves integrate. The arbitrary
    // constants are MATLAB's `C1` and `C2`. Everything else refuses naming the
    // row -- MATLAB may well answer it, and an honest scope is T0.5's rule.
    //
    // `conditions` are relations: `y(0) == 1`, and `subs(diff(y, t), t, 0) == 0`
    // reaching here as the derivative's value at a point.
    static bool solveDifferential(const ICoreSymbolic& equation,
                                  const std::vector<ICoreSymbolic>& conditions,
                                  ICoreSymbolic& out, std::string* whyNot = nullptr);

    // ---- the transforms (T4.10) ---------------------------------------------
    //
    // `laplace`, `ilaplace`, `ztrans` and `iztrans` are ONE entry point with
    // four tables, because they share everything that is hard: the default
    // variable rule, the term-by-term linearity, and the contract for a term
    // the table cannot do.
    //
    // ⚠ **A TERM THE TABLE CANNOT DO COMES BACK UNEVALUATED, and that is
    // MATLAB's own contract rather than an exemption from T0.5's refusal
    // rule** -- the same one `int` follows (T4.5). `laplace(g(t))` answers
    // `laplace(g(t), t, s)` on R2026a, and a SUM answers term by term, so
    // `laplace(t + g(t))` is `1/s^2 + laplace(g(t), t, s)` on both sides.
    // What the console does NOT do is answer a term wrongly, which is why the
    // recognisers below are exact-shape rather than best-effort.
    //
    // `fourier` and `ifourier` joined on 2026-09-29 (FEATURES_TO_ADD.md BF23.3)
    // over the same entry point and the same contract. Their table is not
    // rational -- the Gaussian, exp(-a*|x|), the causal exponential, the step,
    // the impulse, sign, 1/(x^2 + a^2) and sin(b*x)/x -- and their answers carry
    // `1i` and `dirac`, which the symbolic kind holds even though no block wire
    // does (BF23.1, BF23.2).
    enum class Transform { Laplace, InverseLaplace, ZTransform, InverseZTransform,
                           Fourier, InverseFourier };

    // MATLAB's DEFAULT variable pair, which is a rule and not a constant, and
    // the one trap on this row (measured on R2026a, 2026-09-05):
    //
    //     laplace(t^2)     2/s^3        t -> s, the ordinary case
    //     laplace(exp(x))  1/(s - 1)    no t: symvar(f, 1) is the source
    //     laplace(exp(s))  1/(z - 1)    ...and when THAT is `s`, the
    //                                   transform variable moves to `z`
    //     ztrans(z^2)      w-shaped     the same rule, n -> z -> w
    //     ilaplace(1/(t+1)) exp(-x)     and s -> t -> x downward
    //     fourier(exp(-t^2))  w-shaped  x -> w, and v when the source IS w
    //     ifourier(exp(-x^2)) t-shaped  w -> x, and t when the source IS x
    //
    // `from` is the preferred source (`t`, `s`, `n`, `z`, `x`, `w`) when the
    // expression mentions it and `symvar(expr, 1)` otherwise; `to` is the
    // preferred target (`s`, `t`, `z`, `n`, `w`, `x`) unless `from` already IS
    // it, in which case it is the escape (`z`, `x`, `w`, `k`, `v`, `t`).
    static void transformVariables(Transform which, const ICoreSymbolic& expression,
                                   std::string& from, std::string& to);

    // The transform itself, over the variable pair the caller chose. Maps
    // elementwise over a non-scalar, the way MATLAB does. False only for a
    // reason the user can act on; an expression OUTSIDE the table is `true`
    // with an unevaluated answer, per the contract above.
    static bool transform(Transform which, const ICoreSymbolic& expression,
                          const std::string& from, const std::string& to,
                          ICoreSymbolic& out, std::string* whyNot = nullptr);

    // The real and imaginary parts of each element, with EVERY VARIABLE TAKEN AS REAL -- the
    // reading a block wire gives them, and not a console function: MATLAB's `real(f)` keeps
    // `real(w)` standing unless `w` is assumed real. Products, sums, whole powers (a negative
    // one through the conjugate), exp, sin, cos, sinh, cosh and abs of a complex argument, and a
    // positive constant to a complex power are split; anything else that holds the imaginary
    // unit is refused with a reason. A function of real arguments is left as it is, so an
    // unevaluated transform or a `dirac` comes through for its reader to name (BF23.4).
    static bool complexParts(const ICoreSymbolic& expression, ICoreSymbolic& real,
                             ICoreSymbolic& imaginary, std::string* whyNot = nullptr);

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};