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.
| Header | Defines | Declarations | Bases |
|---|---|---|---|
ICoreSymbolic.h | ICoreSymbolic | 42 | — |
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;
};