Laboratoire d'Informatique
UMR 7161 CNRS
Campus de l'École polytechnique
91128 Palaiseau Cedex
France
Email:
vdhoeven@lix.polytechnique.fr
Laboratoire d'Informatique
UMR 7161 CNRS
Campus de l'École polytechnique
91128 Palaiseau Cedex
France
Email:
gregoire.lecerf@math.cnrs.fr
. This work has
been partly supported by the
Interfacing
Preliminary version of November 3, 2023
Abstract
In this paper, we give a detailed description of the interface between
the
Keywords: Mathemagix, C++, generic programming, template library
A.M.S. subject classification: 68W30
Until the mid nineties, the development of computer algebra systems
tended to exploit advances in the area of programming languages, and
sometimes even influenced the design of new languages. The
The
After this initial period, computer algebra systems have been less keen
on exploiting new ideas in language design. One important reason is that
a good language for computer algebra is more important for developers
than for end users. Indeed, typical end users tend to use computer
algebra systems as enhanced pocket calculators, and rarely write
programs of substantial complexity themselves. Another reason is
specific to the family of systems that grew out of
In our opinion, this has led to an unpleasant current situation in
computer algebra: there is a dramatic lack of a modern, sound and fast
general purpose programming language. The major systems
The absence of modern languages for computer algebra is even more
critical whenever performance is required. Nowadays, many important
computer algebra libraries (such as
For these reasons, we have started the design of a new software,
One major design goal of the
We already stated that
On the one hand,
On the other hand,
In
The ability to transform a C++ template library which only permits
static instantiation into a fully generic template library is somewhat
surprising. Part of the magic occurs in the specification of the
interface itself. Indeed, the interface should in particular provide the
missing type information about the parameters of the C++ templates. In
this paper, we will describe in more details how this mechanism works.
We think that similar techniques can be applied for the generic
importation of C++ templates into other languages such as
The paper is organized as follows. In Section 2, we
describe how to import and export non templated classes and functions
from and to
Different programming languages have different conventions for compiling programs, organizing projects into libraries, and mechanisms for separate compilation.
C++ is particularly complex, since the language does not provide any
direct support for the management of big projects. Instead, this task is
delegated to separate configuration and Makefile systems, which are
responsible for the detection and specification of external and internal
dependencies, and the determination of the correct compilation flags.
Although these tasks may be facilitated up to a certain extent when
using integrated development environments such as
Whenever we import functionality from C++ into
For instance, the numerix library of
foreign cpp import { cpp_flags "`numerix-config --cppflags`"; cpp_libs "`numerix-config --libs`"; cpp_include "numerix/integer.hpp"; … } |
On installation of the numerix library, a special script numerix-config is installed in the user's path. In the above example, we use this script in order to retrieve the compilation and linking flags. Notice also that numerix/integer.hpp is the C++ header file for basic arbitrary precision integer arithmetic.
Ideally speaking, the bulk of an interface between
Assume for instance that we want to map the C++ class integer
from integer.hpp into the
foreign cpp import { cpp_flags "`numerix-config --cppflags`"; cpp_libs "`numerix-config --libs`"; cpp_include "numerix/integer.hpp"; class Integer == integer; literal_integer: Literal -> Integer == make_literal_integer; prefix -: Integer -> Integer == prefix -; infix +: (Integer, Integer) -> Integer == infix +; infix -: (Integer, Integer) -> Integer == infix -; infix *: (Integer, Integer) -> Integer == infix *; … } |
The special constructor literal_integer allows us to write literal integer constants such as 12345678987654321 using the traditional notation. This literal constructor corresponds to the C++ routine
integer make_literal_integer (const literal&); |
where literal is a special C++ class for string symbols.
The syntax of C++ is quite rigid and often directly related to
implementation details. For instance, in C++ the notation p.x
necessarily presupposes the definition of a class or structure with a
field x. In
Another example is type inheritance. In C++, type inheritance can only
be done at the level of class definitions. Furthermore, type inheritance
induces a specific low level representation in memory for the
corresponding class instances. In
When importing a C++ class T into
The first compulsory operator is flatten: T ->
Syntactic, which converts instances of type T into
syntactic expression trees which can then be printed in several formats
(ASCII,
Simple
foreign cpp export { class Point == point; point: (Double, Double) -> Point == keyword constructor; postfix .x: Point -> Double == get_x; postfix .y: Point -> Double == get_y; middle: (Point, Point) -> Point == middle; } |
Before we discuss the importation of C++ template libraries into
forall (M: Monoid) cube (x: M): M == x*x*x; |
This function can be applied to any element x whose type M is a monoid. For instance, we may write
c: Int == cube 3; |
The parameters of generic functions are necessarily typed. In our example, the parameter M is a type itself and the type of M a category. The category Monoid specifies the requirements which are made upon the type M, and a typical declaration would be the following:
category Monoid == { infix *: (This, This) -> This; } |
Hence, a type M is considered to have the structure of a Monoid in a given context, as soon as the function infix *: (M, M) -> M is defined in this context. Notice that the compiler does not provide any means for checking mathematical axioms that are usually satisfied, such as associativity.
Already on this simple example, we notice several important differences with the C++ “counterpart” of the declaration of cube:
template<typename M> M cube (const M& x) { return x*x*x; } |
First of all, C++ does not provide a means for checking that M admits the structure of a monoid. Consequently, the correctness of the body return x*x*x can only be checked for actual instantiations of the template. In particular, it is not possible to compile a truly generic version of cube.
By default,
class Monoid_rep: public rep_struct { inline Monoid_rep (); virtual inline ~Monoid_rep (); virtual generic mul (const generic&, const generic&) const = 0; … }; |
A concrete monoid is a “managed pointer” (i.e.
the objects to which they point are reference counted) to a derived
class of Monoid_rep with an actual implementation of the
multiplication mul. Instances of the
generic cube (const Monoid& M, const generic& x) { // x is assumed to contain an object "of type M" return M->mul (x, M->mul (x, x)); } |
The declaration c: Int == cube 3; gives rise to the automatic generation of a class Int_Monoid_rep which corresponds to the class Int with the structure of a Monoid:
struct Int_Ring_rep: public Ring_rep { … generic mul (const generic& x, const generic& y) const { return as_generic<int> (from_generic<int> (x) * from_generic<int> (y)); } … }; |
The declaration itself corresponds to the following C++ code:
Monoid Int_Ring= new Int_Ring_rep (); int c= from_generic<int> (cube (Int_Ring, as_generic<int> (3))); |
Notice that we did not generate any specific instantiation of cube for the Int type. This may lead to significantly smaller executables with respect to C++ when the function cube is applied to objects of many different types. Indeed, in the case of C++, a separate instantiation of the function needs to be generated for each of these types. In particular, the function can only be applied to a finite number of types in the course of a program.
Remark
Remark
class Complex (R: Ring) == { re: R; im: R; constructor complex (r: R, i: R) == { re == r; im == i; } } |
Again, only the generic version of this class is compiled by default. In particular, the internal representation of the corresponding C++ class is simply a class with two fields re and im of type generic.
Regarding functions and templates, there are a few other important
differences between C++ and
Dependencies are allowed between function and template parameters and return values, as in the following example:
forall (R: Ring, M: Module R) infix * (c: R, v: Vector M): Vector M == [ c * x | x: M in v ]; |
Template parameters can be arbitrary types or (not necessarily constant) instances. For instance, one may define a container Vec (R: Ring, n: Int) for vectors with a fixed size.
Functions can be used as arguments and as values:
compose (f: Int -> Int, g: Int -> Int) (x: Int): Int == f g x; |
Notice that
One of the most interesting aspects of our interface between
Before coming to the technical details, let us first give a small
example of how to import part of the univariate polynomial arithmetic
from the C++ template library algebramix, which is
shipped with
foreign cpp import { … class Pol (R: Ring) == polynomial R; forall (R: Ring) { pol: Tuple R -> Pol R == keyword constructor; upgrade: R -> Pol R == keyword constructor; deg: Pol R -> Int == deg; postfix []: (Pol R, Int) -> R == postfix []; prefix -: Pol R -> Pol R == prefix -; infix +: (Pol R, Pol R) -> Pol R == infix +; infix -: (Pol R, Pol R) -> Pol R == infix -; infix *: (Pol R, Pol R) -> Pol R == infix *; … } } |
As is clear from this example, the actual syntax for template imports is
a straightforward extension of the syntax of usual imports and the
syntax of generic declarations on the
Actually, the above code is still incomplete: in order to make it work, we also have to specify how the ring operations on R should be interpreted on the C++ side. This is done by exporting the category Ring to C++:
foreign cpp export category Ring == { convert: Int -> This == keyword constructor; prefix -: This -> This == prefix -; infix +: (This, This) -> This == infix +; infix -: (This, This) -> This == infix -; infix *: (This, This) -> This == infix *; } |
This means that the ring operations in C++ are the constructor from int and the usual operators +, - and *. The programmer should make sure that the C++ implementations of the imported templates only rely on these ring operations.
The first thing the compiler does with the above C++ export of Ring is the creation of a C++ class capable of representing generic instances of arbitrary ring types. Any mechanism for doing this has two components: we should not only store the actual ring elements, but also the rings themselves to which they belong. This can actually be done in two ways.
The most straightforward idea is to represent an instance of a generic ring by a pair , where is the actual ring (similar to the example of the C++ counterpart of a monoid in Section 3) and an actual element of . This approach has the advantage of being purely functional, but it requires non trivial modifications on the C++ side.
Indeed, whenever a function returns a ring object, we should be able to
determine the underlying ring from the input
arguments. In the case of a function such as postfix []: (Pol
R, Int) -> R, this means that R has to be read
off from the coefficients of the input polynomial. But the most
straightforward implementation of the zero polynomial does not have any
coefficients! In principle, it is possible to tweak all C++ containers
so as to guarantee the ability to determine the underlying generic
parameters from actual instances. We have actually implemented this
idea, but it required a lot of work, and it violates the principle that
writing a
The second approach is to store the ring in a global variable, whose value will frequently be changed in the course of actual computations. In fact, certain templates might carry more than one parameter of type Ring, in which case we need more than one global ring. For this reason, we chose to implement a container instance<Cat,Nr> for generic instances of a type of category Cat, with an additional integer parameter Nr for distinguishing between various parameters of the same category Cat. The container instance<Cat,Nr> is really a wrapper for generic:
template<typename Cat, int Nr> class instance { public: generic rep; static Cat Cur; inline instance (const instance& prg2): rep (prg2.rep) {} inline instance (const generic& prg): rep (prg) {} instance (); template<typename C1> instance (const C1& c1); … }; |
For instance, objects of type instance<Ring,2> are instances of the second generic Ring parameter of templates. The corresponding underlying ring is stored in the global static variable instance<Ring,2>::Cur.
When exporting the Ring category to C++, the
template<int Nr> inline instance<Ring,Nr> operator * (const instance<Ring,Nr> &a1, const instance<Ring,Nr> &a2) { typedef instance<Ring,Nr> Inst; return Inst (Inst::Cur->mul (a1.rep, a2.rep)); } |
Since all C++ compilers do not allow us to directly specialize constructors of instance<Cat,Nr>, we provide a general default constructor of instance<Cat,Nr> from an arbitrary type T, which relies on the in place routine
void set_as (instance<Ring,Nr>&, const T&); |
This routine can be specialized for particular categories. For instance, the converter convert: Int -> This from Ring gives rise to following routine, which induces a constructor for instance<Ring,Nr> from int:
template<int Nr> inline void set_as (instance<Ring,Nr> &ret, const int &a1) { typedef instance<Ring,Nr> Inst; ret = Inst (Inst::Cur->cast (a1)); } |
In this example, Inst::Cur->cast represents the function that sends an int into an element of the current ring.
Now that we have a way to represent arbitrary
forall (R: Ring) infix *: (Pol R, Pol R) -> Pol R; |
The compiler essentially generates the following C++ code for this import:
polynomial<generic> mul (const Ring &R, const polynomial<generic> &p1, const polynomial<generic> &p2) { typedef instance<Ring,1> Inst; Ring old_R= Inst::Cur; Inst::Cur= R; polynomial<Inst> P1= as<polynomial<Inst> > (p1); polynomial<Inst> P2= as<polynomial<Inst> > (p2); polynomial<Inst> R = P1 * P2; polynomial<generic> r= as<polynomial<generic> > (R); Inst::Cur= old_R; return r; } |
There are two things to be observed in this code. First of all, for the computation of the actual product P1 * P2, we have made sure that Inst::Cur contains the ring R corresponding to the coefficients of the generic coefficients of the inputs p1 and p2. Moreover, the old value of Inst::Cur is restored on exit. Secondly, we notice that polynomial<Inst> and polynomial<generic> have exactly the same internal representation. The template as simply casts between these two representations. In the actual code generated by the compiler, these casts are done without any cost, directly on pointers.
The above mechanism provides us with a fully generic way to import C++
templates. However, as long as the template parameters are themselves
types which were imported from C++, it is usually more efficient to
shortcut the above mechanism and directly specialize the templates on
the C++ side. For instance, the
p: Pol Integer == …; q: Pol Integer == p * p; |
is compiled into the following C++ code:
polynomial<integer> p= …; polynomial<integer> q= p * p; |
Currently, most of the mathematical features available in
C++ libraries of the
The
In the following example we illustrate simple calculations with analytic functions. We use the notation ==> for macro definitions. We first construct the polynomial indeterminate of , and convert it into the analytic function indeterminate . We display , , and on the standard output mmout. Internal computations are performed up to bits of precision, but printing is restricted to decimal digits. Analytic functions are displayed as their underlying power series at the origin, for which we set the output order to .
include "basix/fundamental.mmx"; include "numerix/floating.mmx"; include "numerix/complex.mmx"; include "continewz/analytic.mmx"; R ==> Floating; C ==> Complex R; Pol ==> Polynomial C; Afun ==> Analytic (R, C); bit_precision := 256; x: Pol == polynomial (complex (0.0 :> R), complex (1.0 :> R)); z: Afun == x :> Pol; f: Afun == exp z; significant_digits := 5; set_output_order (x :> (Series C), 5); mmout << "f= " << f << lf; mmout << "f (1)= " << f (1.0 :> C) << lf; mmout << "f (1 + z)= " << move (f, 1.0 :> C) << lf; |
Compiling and running this program in a textual terminal yields:
f= 1.0000 + 1.0000 * z + 0.50000 * z^2 + 0.16667 * z^3 + 0.041667 * z^4 + O (z^5) f (1)= 2.7183 f (1 + z)= 2.7183 + 2.7183 * z + 1.3591 * z^2 + 0.45305 * z^3 + 0.11326 * z^4 + O (z^5)
Importing a library that is completely external to the
However, when introducing new types and functions, one usually wants
them to interact naturally with other librairies. For instance, if
several libraries have their own arbitrarily long integer type,
straightforward interfaces introduce several
For C and C++ libraries involving only a finite number of types, we
prefer to design lower level interfaces to the C++ libraries of
When libraries contain many data types, functions, and have their own
memory management, the interface quickly becomes tedious. This situation
happened with the
include "basix/fundamental.mmx"; include "mpari/pari.mmx"; Pol ==> Polynomial Integer; x: Pol == polynomial (0 :> Integer, 1 :> Integer); p: Pol == x^2 + x - 1001; mmout << pari_nf_basis p << lf; |
[1, 1 / 3 * x - 1 / 3]
The current mechanism for importing C++ template libraries has been
tested for the standard mathematical libraries which are shipped with
We would like to thank Jean-Charles Faugère for helping us in the
interface with
Axiom computer algebra system. Software available from http://wiki.axiom-developer.org.
E. Bond, M. Auslander, S. Grisoff, R. Kenney, M. Myszewski, J. Sammet, R. Tobey, and S. Zilles. FORMAC an experimental formula manipulation compiler. In Proceedings of the 1964 19th ACM national conference, ACM '64, pages 112.101–112.1019, New York, NY, USA, 1964. ACM.
D. Cade, X. Pujol, and D. Stehlé. Fplll, library for LLL-reduction of Euclidean lattices. Software available from http://perso.ens-lyon.fr/damien.stehle/fplll, 1998.
Y. Chicha, F. Defaix, and S. M. Watt. Automation of the Aldor/C++ interface: User's guide. Technical Report Research Report D2.2.2c, FRISCO Consoritum, 1999. Available from http://www.csd.uwo.ca/~watt/pub/reprints/1999-frisco-aldorcpp-ug.pdf.
J.-C. Faugère. FGb: A Library for Computing Gröbner Bases. In K. Fukuda, J. van der Hoeven, M. Joswig, and N. Takayama, editors, Mathematical Software - ICMS 2010, Third International Congress on Mathematical Software, Kobe, Japan, September 13-17, 2010, volume 6327 of Lecture Notes in Computer Science, pages 84–87. Springer Berlin / Heidelberg, 2010.
L. Fousse, G. Hanrot, V. Lefèvre, P. Pélissier, and P. Zimmermann. MPFR: A multiple-precision binary floating-point library with correct rounding. ACM Transactions on Mathematical Software, 33(2), 2007. Software available from http://www.mpfr.org.
M. Gaëtano and S. M. Watt. An object model correspondence for Aldor and C++. Technical Report Research Report D2.2.1, FRISCO Consortium, 1997. Available from http://www.csd.uwo.ca/~watt/pub/reprints/1997-frisco-aldorcppobs.pdf.
R. Garcia, J. Järvi, A. Lumsdaine, J. G. Siek, and J. Willcock. A comparative study of language support for generic programming. In Proceedings of the 2003 ACM SIGPLAN conference on Object-oriented programming, systems, languages, and applications (OOPSLA'03), October 2003.
J. Y. Girard. Une extension de l'interprétation de Gödel à l'analyse, et son application à l'élimination de coupures dans l'analyse et la théorie des types. In J. E. Fenstad, editor, Proceedings of the Second Scandinavian Logic Symposium, pages 63–92. North-Holland Publishing Co., 1971.
T. Granlund and the GMP development team. GNU MP: The GNU Multiple Precision Arithmetic Library. Software available from http://gmplib.org, 1991.
J. H. Griesmer, R. D. Jenks, and D. Y. Y. Yun. SCRATCHPAD User's Manual. Computer Science Department monograph series. IBM Research Division, 1975.
W. Hart. An introduction to Flint. In K. Fukuda, J. van der Hoeven, M. Joswig, and N. Takayama, editors, Mathematical Software - ICMS 2010, Third International Congress on Mathematical Software, Kobe, Japan, September 13-17, 2010, volume 6327 of Lecture Notes in Computer Science, pages 88–91. Springer Berlin / Heidelberg, 2010.
J. van der Hoeven. Overview of the Mathemagix type system. In Electronic proc. ASCM '12, Beijing, China, October 2012. Available from http://hal.archives-ouvertes.fr/hal-00702634.
J. van der Hoeven, G. Lecerf, B. Mourrain, et al. Mathemagix, 2002. Software available from http://www.mathemagix.org.
J. van der Hoeven, G. Lecerf, B. Mourrain, Ph. Trébuchet, J. Berthomieu, D. Diatta, and A. Manzaflaris. Mathemagix, the quest of modularity and efficiency for symbolic and certified numeric computation. ACM Commun. Comput. Algebra, 45(3/4):186–188, 2012.
R. D. Jenks. The SCRATCHPAD language. SIGPLAN Not., 9(4):101–111, 1974.
R. D. Jenks. MODLISP – an introduction (invited). In Proceedings of the International Symposium on Symbolic and Algebraic Computation, EUROSAM '79, pages 466–480, London, UK, UK, 1979. Springer-Verlag.
R. D. Jenks and R. Sutor. AXIOM: the scientific computation system. Springer-Verlag, New York, NY, USA, 1992.
R. D. Jenks and B. M. Trager. A language for computational algebra. SIGPLAN Not., 16(11):22–29, 1981.
Maple user manual. Toronto: Maplesoft, a division of Waterloo Maple Inc., 2005–2012. Maple is a trademark of Waterloo Maple Inc. http://www.maplesoft.com/products/maple.
W. A. Martin and R. J. Fateman. The MACSYMA system. In Proceedings of the second ACM symposium on symbolic and algebraic manipulation, SYMSAC '71, pages 59–75, New York, NY, USA, 1971. ACM.
P. Martin-Löf. Constructive mathematics and computer programming. Logic, Methodology and Philosophy of Science VI, pages 153–175, 1979.
Maxima, a computer algebra system (free version). Software available from http://maxima.sourceforge.net, 2011.
R. Milner. A theory of type polymorphism in programming. Journal of Computer and System Sciences, 17:348–375, 1978.
J. Moses. Macsyma: A personal history. Journal of Symbolic Computation, 47(2):123–130, 2012.
W. A. Stein et al. Sage Mathematics Software. The Sage Development Team, 2004. Software available from http://www.sagemath.org.
R. S. Sutor and R. D. Jenks. The type inference and coercion facilities in the Scratchpad II interpreter. SIGPLAN Not., 22(7):56–63, 1987.
The PARI Group, Bordeaux. PARI/GP, 2012. Software available from http://pari.math.u-bordeaux.fr.
S. Watt, P. A. Broadbery, S. S. Dooley, P. Iglio, S. C. Morrison, J. M. Steinbach, and R. S. Sutor. A first report on the A# compiler. In Proceedings of the international symposium on symbolic and algebraic computation, ISSAC '94, pages 25–31, New York, NY, USA, 1994. ACM.
S. Watt et al. Aldor programming language. Software available from http://www.aldor.org, 1994.
S. Wolfram. Mathematica: A System for Doing Mathematics by Computer. Addison-Wesley, second edition, 1991. Mathematica is a trademark of Wolfram Research, Inc. http://www.wolfram.com/mathematica.