Sagebrush · Articles · Sage

How the Sage implementation works

Written by Claude Opus 5.5, an AI model made by Anthropic, from the code, tests, coverage reports and commit history of the Sagebrush project, led by William Stein (SageMath, Inc.).

Code: src/sagepre.ts (the preparser), lib/sage_all.py and lib/_sage_*.py (the library, about 69,000 lines of Python), and the Rust engines under engine/: sym (symbolic calculus), mpoly (multivariate polynomials), arith and bigint (exact arithmetic), and the number theory crates (classgroup, modsym, galois, hmf and others).

Type this into a Sagebrush notebook in Sage mode:

1/3 + 1/6
factor(2^64 - 1)
R.<t> = QQ[]
K.<a> = NumberField(t^2 + 5)
K.class_number()
integrate(x*exp(x), x)
RealField(100)(2).sqrt()

What comes back is what SageMath prints:

1/2
3 * 5 * 17 * 257 * 641 * 65537 * 6700417
2
(x - 1)*e^x
1.4142135623730950488016887242

None of it comes from SageMath's code. The rest of this page explains how, and how close it gets.

1. The language: a preparser

Sage is Python with a few additions, and Sage rewrites a program into plain Python before running it. Sagebrush does the same, from the rules in SageMath's documentation:

Strings and comments are never rewritten, and line numbers stay as they were, so error messages point at the line you wrote.

In the browser, part of this is done by the Python compiler itself, not by text rewriting: in Sage mode, ^ compiles to a power, and / between two integers compiles to an exact division. A second preparser, in Python, does the whole job when the library runs under CPython, as sagebrush.sage. Any place that runs Sage code given as a string, such as timeit("..."), has to go through the same machinery. Otherwise it would time a different computation: 2^3 would be exclusive or.

2. Numbers and parents

Integers. A Sage integer is a Python integer. In the browser, Sagebrush's Python gives the integer type Sage's methods (factor, is_prime, digits, nth_root and many more). CPython cannot extend its int, so there Integer is a subclass of it with the same methods. Ordinary integer arithmetic (+, *, ^) is the runtime's own: in the browser, JavaScript's BigInt; under CPython, CPython's int. The Rust engines have big integers of their own, Sagebrush's code (see the foundations), for the computations they do: factoring, exact linear algebra, number theory.

Rationals are exact fractions, always reduced, with Sage's printing and methods. Where Sage's rules for differ from Python's, the library follows Sage. For example, gcd(2/3, 4/9) is , the gcd of the numerators over the lcm of the denominators.

Real numbers. RealField(prec) is real arithmetic of any precision, written from scratch: a number is with an integer of exactly prec bits, correctly rounded after every operation, in each of the rounding modes. The elementary functions and the constants , , , Euler's and Catalan's constant are computed in fixed point with guard bits. At the default 53 bits a real number is a machine double, for speed, and it switches to the general representation only when a value leaves the double range. Numbers print with Sage's number of digits for their precision.

Parents. Every Sage element knows its parent: the ring or field it belongs to (ZZ, QQ, RR, GF(7), QQ['x'], a number field). Parents decide what arithmetic means and how mixed operations coerce: an integer plus a rational is a rational, and an element of plus an integer stays in . Sagebrush implements the parents and coercions that programs use, so that parent(), base_ring(), change_ring and conversions such as QQ(x) and K(a) give Sage's answers.

Other rings. Univariate and multivariate polynomials, power series, finite fields and their extensions, -adic numbers, number fields and their orders and ideals, algebraic numbers (QQbar), and matrices over all of these are each a module of the library. The heavy computations (factoring, Gröbner bases, exact linear algebra) run in the Rust engines.

3. Symbolic calculus

var('x'), sin(x), diff, integrate, limit, taylor, series, solve, simplify and the rest are backed by a symbolic engine written in Rust (engine/sym). An Expression holds the engine's representation, and each operation is one stateless call into the engine. The engine covers:

Printing follows Sage: the order of terms in a sum, the placement of signs and the form of a fraction were measured on a corpus of expressions run through SageMath, and then reproduced.

A rewrite must not change where an expression is defined. For example, is , which is defined at . The form would raise an error there.

4. Algebra and number theory

Number fields, class groups and units, modular symbols and newforms, elliptic curves, Galois groups, Gröbner bases and varieties are computed by engines written from the literature, and checked against tables and established systems. These engines have their own articles:

The Sage library is the layer that turns them into Sage's objects: NumberField, EllipticCurve, ModularSymbols, ideal(...).groebner_basis() and so on, with Sage's names, arguments and printed forms. A few of these layers, in more detail:

Number fields, class groups and units. A field is for a monic irreducible . Its elements, integral bases, prime ideals, class groups, unit groups and regulators print as Sage prints them, and the arithmetic runs in the Rust engine (engine/classgroup). Polynomials over a number field have:

Class groups and regulators assume GRH, as PARI's do and as Sage's do with proof=False; the methods are Jacobson's sieve for imaginary quadratic fields and Buchmann's subexponential method for general fields (see class groups). With proof=True, the engine returns an unconditional certificate when it can find one within its limits; otherwise it raises NotImplementedError and says why. It never returns the GRH value as proven. unit_group() gives the rank, proven from the signature by certified root isolation, and the roots of unity; fundamental units are not available yet.

Algebraic numbers. QQbar and AA are exact. An element is a polynomial in a chosen root of an irreducible integer polynomial, and it carries the 64-bit interval that Sage displays. When an operation combines elements of different fields, the minimal polynomial of the result is a factor of the characteristic polynomial of a Kronecker product of companion matrices; the right factor is chosen by a numerical test at increasing precision.

Varieties. I.variety() lists the points of a zero-dimensional ideal as Sage's list of dictionaries: the Gröbner engine computes a lex basis, and back-substitution finds the roots of each univariate polynomial in turn over the base field, such as or . A positive-dimensional ideal raises Sage's ValueError.

I.variety(ring) also works over and (RR, CC, RealField(p), ComplexField(p), RDF, CDF) and over AA and QQbar, through a rational univariate representation (Rouillier, 1999):

  1. A separating linear form is chosen.
  2. The minimal polynomial of multiplication by is computed on the normal basis of the radical (the ideal plus the squarefree parts of its eliminants).
  3. Each coordinate is then a rational function of a root of the squarefree , so every point is exact.

Over AA and QQbar the points are exact algebraic numbers; over the real and complex fields they are those exact points, evaluated exactly and then rounded, with a certified error of at most one unit in the last place at the field's precision. The real points come first, then the rest in increasing order, as Sage orders AA and QQbar points. Every size is capped before the work it bounds (an eliminant's degree at most 4096, at most 100 points, at most 32 tries for a separating form, at most 20,000 digits), and anything over a cap raises NotImplementedError rather than running away or returning a partial list. Varieties over other number fields are refused for now.

Graphs. Graph and DiGraph cover simple graphs with loops and edge labels (multigraphs are refused), with the graphs.* and digraphs.* families: degrees and neighbours, adjacency and Laplacian matrices, shortest paths with or without weights, connected and strongly connected components, spanning-tree counts by Kirchhoff's theorem, the Tutte polynomial by deletion and contraction, and isomorphism. Isomorphism uses colour refinement and then backtracking: a returned isomorphism is an explicit vertex map, and False comes from an exhaustive search, so both answers are exact.

Matrices. Over and , determinants, ranks, characteristic polynomials, echelon forms, kernels and solving run in the Rust engine (engine/arith), with multimodular and -adic algorithms, and every answer is certified. Hermite and Smith forms with their transforms, elementary divisors and LLL come from the number field engine. As in Sage, echelon_form() and rref() differ over : echelon_form() stays in and is the Hermite normal form, while rref() works over the fraction field , so its pivots are 1 (matrix(ZZ, 1, 1, [2]) has rref() equal to [1] over and echelon_form() equal to [2]). Empty matrices keep their shape: matrix(QQ, 0, 3, []).rref() is , and the basis matrix of a zero kernel in is ; this holds for the echelon, Hermite and Smith forms, LLL, change_ring, solve_right, block and random matrices and basis matrices. matrix(R, r, c, f), with a function f of (i, j), checks the shape against the size limits before the first call, and over and converts each value strictly as it is returned, so a value that does not convert raises its error at once. Matrices over a number field use exact Gaussian elimination over , and scalars meet them by Sage's coercion; matrices over a ring of integers (an order) are refused before any entry is read. Matrices over finite fields use Gaussian elimination over the field, with echelon forms, kernels, inverses, solving, characteristic polynomials and multiplicative orders. Over with composite, which is not a field, the ring arithmetic works, but the same elimination can meet a pivot that is not a unit, and then it raises an error instead of answering. For example, the determinant of matrix(Zmod(6), [[2, 0], [0, 3]]) (which is ) is not computed yet.

Hilbert modular forms. HilbertModularForms(N) gives the forms of weight over . It ports William Stein's PSAGE implementation, with his permission, and uses the definite method: by Jacquet–Langlands and class number one, is the space of functions on , where is the icosian ring (Dembélé 2005). The engine (engine/hmf) also gives for primes dividing the level, true new dimensions, and weights . Over any other real quadratic field, HilbertModularForms(F, N) uses the general definite method of Dembélé and Voight (2013): a certified maximal order in a totally definite quaternion algebra, its right ideal classes by neighbours and the mass formula, and the Hecke operators. Newforms come as Galois orbits, and each eigenvalue is proven by checking in integers.

5. Proofs, and refusing instead of guessing

Two rules govern every answer.

A wrong answer is never acceptable. If Sagebrush cannot compute something correctly, it raises an error (usually NotImplementedError, saying what is missing) rather than return something plausible. An empty list where roots exist, a truncated series without its term, or a number of the wrong precision are all wrong answers, and each is treated as a bug.

proof=True means proven. Where Sage distinguishes proven from probable results, Sagebrush does too:

A result is never presented as proven when it rests on an unproven step.

6. How it was tested

SageMath was used as a black-box oracle: Sagebrush's authors ran it and compared outputs, but never read its source. There are four layers.

Program tests. About 30 Sage programs (sage-tests/) print results across the library. Their output under SageMath 10.10 is committed, and the test suite requires Sagebrush's output to be identical, byte for byte.

Docstrings, checked in both systems. About 4,000 examples in the library's docstrings run in Sagebrush's own doctester on every change. A separate script runs the same examples in SageMath, to check that each expected output is what SageMath prints. Examples of Sagebrush-only features are marked and skipped there.

Coverage corpora. Every example in two public documents is run through both systems and compared:

Generated hunts. Thousands of random cases (integers and rationals, polynomials over several rings, matrices, finite fields, symbolic expressions) are generated, run in both systems, and every difference is triaged by hand. These hunts find the wrong answers that documentation examples miss, because they try the cases nobody writes down: negative arguments, empty matrices, rationals where integers are usual.

7. How close

The latest coverage run was made on 16 October 2026, at commit e4a9ce94, with SageMath 10.10 as the oracle.

corpus scored match different error refused by design timeout
Sage Tutorial 1,069 784 (73.3%) 45 (4.2%) 194 (18.1%) 45 (4.2%) 1 0
Computational Mathematics with SageMath 2,615 1,666 (63.7%) 174 (6.7%) 719 (27.5%) 47 (1.8%) 6 3

How to read the columns:

By design are the few cases where Sagebrush deliberately answers differently. For example, solve(sin(x) == 0, x) returns every solution, x == pi*n1 for an integer n1, where SageMath returns only x == 0. A timeout is a case that did not finish within the harness's time limit.

The book's weakest chapters are numerical integration, combinatorics and graph theory, where much is still missing. Its strongest are the first steps, programming, and finite fields and number theory, where 86–94% of the cases match.

What would have saved time

Updated 2026-10-16 · source on GitHub