source: sage/misc/sagex.py @ 4802:85e527366f8e

Revision 4802:85e527366f8e, 11.6 KB checked in by William Stein <wstein@…>, 6 years ago (diff)

Improvements to sagex'ing spyx files. Got rid of caching so .sage/spyx doesn't get extremely cluttered; make it so files in current directory can be included in spyx files.

Line 
1"""
2SageX Compiled SAGE Code
3
4AUTHORS:
5    -- William Stein, 2006-01-18
6
7"""
8
9#*****************************************************************************
10#       Copyright (C) 2006 William Stein <wstein@gmail.com>
11#
12#  Distributed under the terms of the GNU General Public License (GPL)
13#
14#                  http://www.gnu.org/licenses/
15#*****************************************************************************
16
17import os, sys
18
19from misc import SPYX_TMP, SAGE_ROOT
20
21def cblas():
22    if os.environ.has_key('SAGE_CBLAS'):
23        return os.environ['SAGE_CBLAS']
24    elif os.path.exists('/usr/lib/libcblas.dylib') or \
25         os.path.exists('/usr/lib/libcblas.so'):
26        return 'cblas'
27    elif os.path.exists('/usr/lib/libblas.dll.a'):   # untested.
28        return 'blas'
29    else:
30        # This is very slow  (?), but *guaranteed* to be available.
31        return 'gslcblas' 
32   
33
34include_dirs = ['%s/local/include'%SAGE_ROOT,  \
35                '%s/local/include/python%s'%(SAGE_ROOT, sys.version[:3]), \
36                '%s/devel/sage/sage/ext'%SAGE_ROOT, \
37                '%s/devel/sage/'%SAGE_ROOT, \
38                '%s/devel/sage/sage/gsl'%SAGE_ROOT]
39
40
41standard_libs = ['mpfr', 'gmp', 'gmpxx', 'stdc++', 'pari', 'm', \
42                 'mwrank', 'gsl', cblas(), 'ntl', 'csage']
43
44offset = 0
45
46def parse_keywords(kwd, s):
47    j = 0
48    v = []
49    while True:
50        i = s[j:].find(kwd)
51        if i == -1: break
52        j = i + j
53        s = s[:j] + '#' + s[j:]
54        j += len(kwd) + 1
55        k = s[j:].find('\n')
56        if k == -1:
57            k = len(s)
58        for X in s[j:j+k].split():
59            if X[0] == '#':   # skip rest of line
60                break
61            v.append(X)
62    return v, s
63
64def environ_parse(s):
65    i = s.find('$')
66    if i == -1:
67        return s
68    j = s[i:].find('/')
69    if j == -1:
70        j = len(s)
71    else:
72        j = i + j
73    name = s[i+1:j]
74    if os.environ.has_key(name):
75        s = s[:i] + os.environ[name] + s[j:]
76    else:
77        return s
78    return environ_parse(s)
79
80def pyx_preparse(s):
81    lang = parse_keywords('clang', s)
82    if lang[0]:
83        lang = lang[0][0]
84    else:
85        lang = "c"
86
87    v, s = parse_keywords('clib', s)
88    libs = v + standard_libs
89
90    additional_source_files, s = parse_keywords('cfile', s)
91   
92    v, s = parse_keywords('cinclude', s)
93    inc = [environ_parse(x.replace('"','').replace("'","")) for x in v] + include_dirs
94    s = """
95include "cdefs.pxi"
96""" + s
97    if lang != "c++": # has issues with init_csage()
98        s = """
99include "interrupt.pxi"  # ctrl-c interrupt block support
100include "stdsage.pxi"  # ctrl-c interrupt block support
101""" + s
102    return s, libs, inc, lang, additional_source_files
103
104################################################################
105# If the user attaches a .spyx file and changes it, we have
106# to reload an .so.
107#
108# PROBLEM: Python does not allow one to reload an .so extension module.
109# Solution, we create a different .so file and load that one,
110# overwriting the definitions of everything in the original .so file.
111#
112# HOW: By using a sequence_number for each .spyx file; we keep
113# these sequence numbers in a dict.
114#
115################################################################
116
117sequence_number = {}
118
119def sagex(filename, verbose=False, compile_message=False,
120          use_cache=False, create_local_c_file=False):
121    if filename[-5:] != '.spyx':
122        print "File (=%s) must have extension .spyx"%filename
123
124    clean_filename = sanitize(filename)
125    base = os.path.split(os.path.splitext(clean_filename)[0])[1]
126    abs_base = os.path.abspath(os.path.split(os.path.splitext(clean_filename)[0])[0])
127
128    build_dir = '%s/%s'%(SPYX_TMP, base)
129    if os.path.exists(build_dir):
130        # There is already a module here.  Maybe we do not have to rebuild?
131        # Find the name.
132        if use_cache:
133            prev_so = [F for F in os.listdir(build_dir) if F[-3:] == '.so']
134            if len(prev_so) > 0:
135                prev_so = prev_so[0]     # should have length 1 because of deletes below
136                if os.path.getmtime(filename) <= os.path.getmtime('%s/%s'%(build_dir, prev_so)):
137                    # We do not have to rebuild.
138                    return prev_so[:-3], build_dir
139    else:
140        os.makedirs(build_dir)
141    for F in os.listdir(build_dir):
142        G = '%s/%s'%(build_dir,F)
143        if not os.path.isdir(G):
144            os.unlink(G)
145
146    cmd = 'cd "%s"; ln -s "%s"/* .'%(build_dir, abs_base)
147    os.system(cmd)
148
149    if compile_message:
150        print "Compiling %s..."%filename
151       
152    F = open(filename).read()
153
154    F, libs, includes, language, additional_source_files = pyx_preparse(F)
155
156    # add the working directory to the includes so custom headers etc. work
157    includes.append(os.path.split(os.path.splitext(filename)[0])[0])
158
159    if language == 'c++':
160        extension = "cpp"
161    else:
162        extension = "c"
163
164    global sequence_number
165    if not sequence_number.has_key(base):
166        sequence_number[base] = 0
167    name = '%s_%s'%(base, sequence_number[base])
168
169    # increment the sequence number so will use a different one next time.
170    sequence_number[base] += 1
171
172    additional_source_files = ",".join(["'"+os.path.abspath(os.curdir)+"/"+fname+"'" \
173                                        for fname in additional_source_files])
174   
175    pyx = '%s/%s.pyx'%(build_dir, name)
176    open(pyx,'w').write(F)
177    setup="""
178# Build using 'python setup.py'
179import distutils.sysconfig, os, sys
180from distutils.core import setup, Extension
181
182if not os.environ.has_key('SAGE_ROOT'):
183    print "    ERROR: The environment variable SAGE_ROOT must be defined."
184    sys.exit(1)
185else:
186    SAGE_ROOT  = os.environ['SAGE_ROOT']
187    SAGE_LOCAL = SAGE_ROOT + '/local/'
188
189extra_link_args =  ['-L' + SAGE_LOCAL + '/lib']
190extra_compile_args = ['-w']
191
192ext_modules = [Extension('%s', sources=['%s.%s', %s],
193                     libraries=%s,
194                     library_dirs=[SAGE_LOCAL + '/lib/'],
195                     extra_compile_args = extra_compile_args,
196                     extra_link_args = extra_link_args,
197                     language = '%s' )]
198                     
199setup(ext_modules = ext_modules,
200      include_dirs = %s)
201    """%(name, name, extension, additional_source_files, libs, language, includes)
202    open('%s/setup.py'%build_dir,'w').write(setup)
203
204    sagex_include = ' '.join(['-I %s'%x for x in includes if len(x.strip()) > 0 ])
205
206    cmd = 'cd %s && sagexc -p %s %s.pyx 1>log 2>err '%(build_dir, sagex_include, name)
207
208    if create_local_c_file:
209        target_c = '%s/_%s.c'%(os.path.abspath(os.curdir), base)
210        if language == 'c++':
211            target_c = target_c + "pp"
212        cmd += ' && cp %s.c %s'%(name, target_c)
213       
214    if verbose:
215        print cmd
216    if os.system(cmd):
217        log = open('%s/log'%build_dir).read()
218        err = subtract_from_line_numbers(open('%s/err'%build_dir).read(), offset)
219        raise RuntimeError, "Error converting %s to C:\n%s\n%s"%(filename, log, err)
220
221    if language=='c++':
222        os.system("cd %s && mv %s.c %s.cpp"%(build_dir,name,name))
223
224##     if make_c_file_nice and os.path.exists(target_c):
225##         R = open(target_c).read()
226##         R = "/* THIS IS A PARSED TO MAKE READABLE VERSION OF THE C FILE. */" + R
227       
228##         # 1. Get rid of the annoying __pyx_'s before variable names.
229##         # R = R.replace('__pyx_v_','').replace('__pyx','')
230##         # 2. Replace the line number references by the actual code from the file,
231##         #    since it is very painful to go back and forth, and the philosophy
232##         #    of SAGE is that everything that can be very easy *is*.
233
234##         pyx_file = os.path.abspath('%s/%s.pyx'%(build_dir,name))
235##         S = '/* "%s":'%pyx_file
236##         n = len(S)
237##         last_i = -1
238##         X = F.split('\n')
239##         stars = '*'*80
240##         while True:
241##             i = R.find(S)
242##             if i == -1 or i == last_i: break
243##             last_i = i
244##             j = R[i:].find('\n')
245##             if j == -1: break
246##             line_number = int(R[i+n: i+j])
247##             try:
248##                 line = X[line_number-1]
249##             except IndexError:
250##                 line = '(missing code)'
251##             R = R[:i+2] + '%s\n\n Line %s: %s\n\n%s'%(stars, line_number, line, stars) + R[i+j:]
252           
253##         open(target_c,'w').write(R)
254   
255
256    cmd = 'cd %s && python setup.py build 1>log 2>err'%build_dir
257    if verbose: print cmd
258    if os.system(cmd):
259        log = open('%s/log'%build_dir).read()
260        err = open('%s/err'%build_dir).read()
261        raise RuntimeError, "Error compiling %s:\n%s\n%s"%(filename, log, err)
262   
263    # Move from lib directory.
264    cmd = 'mv %s/build/lib.*/* %s'%(build_dir, build_dir)
265    if verbose: print cmd
266    if os.system(cmd):
267        raise RuntimeError, "Error copying extension module for %s"%filename
268
269
270    return name, build_dir
271
272
273
274def subtract_from_line_numbers(s, n):
275    ans = []
276    for X in s.split('\n'):
277        i = X.find(':')
278        j = i+1 + X[i+1:].find(':')
279        try:
280            ans.append('%s:%s:%s\n'%(X[:i], int(X[i+1:j]) - n, X[j+1:]))
281        except ValueError:
282            ans.append(X)
283    return '\n'.join(ans)
284
285
286################################################################
287# COMPILE
288################################################################
289def sagex_lambda(vars, expr,
290                 verbose=False,
291                 compile_message=False,
292                 use_cache=False):
293    """
294    Create a compiled function which evaluates expr assuming machine
295    values for vars.
296
297    WARNING: This implementation is not well tested.
298
299    INPUT:
300        vars -- list of pairs (variable name, c-data type), where
301                the variable names and data types are strings.
302            OR -- a string such as
303                         'double x, int y, int z'
304        expr -- an expression involving the vars and constants;
305                You can access objects defined in the current
306                module scope globals() using sagobject_name.
307                See the examples below.
308
309    EXAMPLES:
310    We create a Lambda function in pure Python (using the r to make sure the 3.2
311    is viewed as a Python float):
312        sage: f = lambda x,y: x*x + y*y + x + y + 17r*x + 3.2r
313
314    We make the same Lambda function, but in a compiled form.
315        sage: g = sagex_lambda('double x, double y', 'x*x + y*y + x + y + 17*x + 3.2')
316
317    We access a global function and variable.
318        sage: a = 25
319        sage: f = sagex_lambda('double x', 'sage.math.sin(x) + sage.a')
320        sage: f(10)
321        24.455978889110629
322        sage: a = 50
323        sage: f(10)
324        49.455978889110632       
325    """
326    if isinstance(vars, str):
327        v = vars
328    else:
329        v = ', '.join(['%s %s'%(typ,var) for typ, var in vars])
330
331    s = """
332class _s:
333   def __getattr__(self, x):
334       return globals()[x]
335
336sage = _s()
337
338def f(%s):
339 return %s
340    """%(v, expr)
341    if verbose:
342        print s
343    import sage.misc.misc
344    tmpfile = sage.misc.misc.tmp_filename() + ".spyx"
345    open(tmpfile,'w').write(s)
346
347    import sage.server.support
348    d = {}
349    sage.server.support.sagex_import_all(tmpfile, d,
350                                         verbose=verbose, compile_message=compile_message,
351                                         use_cache=use_cache,
352                                         create_local_c_file=False)
353    return d['f']
354   
355
356
357def sanitize(f):
358    """
359    Given a filename f, replace it by a filename that is a valid Python
360    module name.
361
362    This means that the characters are all alphanumeric or _'s and
363    doesn't begin with a numeral.
364    """
365    s = ''
366    if f[0].isdigit():
367        s += '_'
368    for a in f:
369        if a.isalnum():
370            s += a
371        else:
372            s += '_'
373    return s
374
375
Note: See TracBrowser for help on using the repository browser.