| 1 | """ |
|---|
| 2 | SageX Compiled SAGE Code |
|---|
| 3 | |
|---|
| 4 | AUTHORS: |
|---|
| 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 | |
|---|
| 17 | import os, sys |
|---|
| 18 | |
|---|
| 19 | from misc import SPYX_TMP, SAGE_ROOT |
|---|
| 20 | |
|---|
| 21 | def 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 | |
|---|
| 34 | include_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 | |
|---|
| 41 | standard_libs = ['mpfr', 'gmp', 'gmpxx', 'stdc++', 'pari', 'm', \ |
|---|
| 42 | 'mwrank', 'gsl', cblas(), 'ntl', 'csage'] |
|---|
| 43 | |
|---|
| 44 | offset = 0 |
|---|
| 45 | |
|---|
| 46 | def 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 | |
|---|
| 64 | def 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 | |
|---|
| 80 | def 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 = """ |
|---|
| 95 | include "cdefs.pxi" |
|---|
| 96 | """ + s |
|---|
| 97 | if lang != "c++": # has issues with init_csage() |
|---|
| 98 | s = """ |
|---|
| 99 | include "interrupt.pxi" # ctrl-c interrupt block support |
|---|
| 100 | include "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 | |
|---|
| 117 | sequence_number = {} |
|---|
| 118 | |
|---|
| 119 | def 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' |
|---|
| 179 | import distutils.sysconfig, os, sys |
|---|
| 180 | from distutils.core import setup, Extension |
|---|
| 181 | |
|---|
| 182 | if not os.environ.has_key('SAGE_ROOT'): |
|---|
| 183 | print " ERROR: The environment variable SAGE_ROOT must be defined." |
|---|
| 184 | sys.exit(1) |
|---|
| 185 | else: |
|---|
| 186 | SAGE_ROOT = os.environ['SAGE_ROOT'] |
|---|
| 187 | SAGE_LOCAL = SAGE_ROOT + '/local/' |
|---|
| 188 | |
|---|
| 189 | extra_link_args = ['-L' + SAGE_LOCAL + '/lib'] |
|---|
| 190 | extra_compile_args = ['-w'] |
|---|
| 191 | |
|---|
| 192 | ext_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 | |
|---|
| 199 | setup(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 | |
|---|
| 274 | def 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 | ################################################################ |
|---|
| 289 | def 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 = """ |
|---|
| 332 | class _s: |
|---|
| 333 | def __getattr__(self, x): |
|---|
| 334 | return globals()[x] |
|---|
| 335 | |
|---|
| 336 | sage = _s() |
|---|
| 337 | |
|---|
| 338 | def 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 | |
|---|
| 357 | def 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 | |
|---|