pyrust-engine 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ from .runtime import optimize, OptimizationError, OptimizedFunction, PyRustFallbackWarning
2
+ from .compiler import Unsupported, F64Array, I64Array
3
+ from .cache import prune_cache
4
+ __all__ = ['optimize', 'OptimizationError', 'OptimizedFunction', 'PyRustFallbackWarning', 'Unsupported', 'F64Array', 'I64Array', 'prune_cache']
5
+ __version__ = '0.6.0'
@@ -0,0 +1,60 @@
1
+ import argparse
2
+ import inspect
3
+ import json
4
+ from pathlib import Path
5
+ import runpy
6
+ import sys
7
+ from .profiling import profile_script
8
+ from .cache import prune_cache
9
+
10
+
11
+ def check_file(path, max_iterations=10_000_000):
12
+ """Load a trusted file (without running its `if __name__ == '__main__'` block) and
13
+ explain, for every function defined in it, whether PyRust can compile it."""
14
+ from .runtime import optimize
15
+ path = Path(path).resolve()
16
+ old_path = sys.path[:]
17
+ try:
18
+ sys.path.insert(0, str(path.parent))
19
+ namespace = runpy.run_path(str(path), run_name='__pyrust_check__')
20
+ finally:
21
+ sys.path[:] = old_path
22
+ results = []
23
+ for value in namespace.values():
24
+ original = getattr(value, 'python', value)
25
+ if not inspect.isfunction(original) or Path(original.__code__.co_filename).resolve() != path:
26
+ continue
27
+ text = optimize(original, max_iterations=max_iterations, warn_on_fallback=False).explain()
28
+ results.append({'function': original.__qualname__, 'line': original.__code__.co_firstlineno,
29
+ 'native': ' compiles to Rust.' in text.splitlines()[0], 'explanation': text})
30
+ return sorted(results, key=lambda r: r['line'])
31
+
32
+
33
+ def main():
34
+ parser=argparse.ArgumentParser(prog='python -m pyrust_engine',description='PyRust Engine developer tools')
35
+ commands=parser.add_subparsers(dest='command',required=True)
36
+ p=commands.add_parser('check',help='Explain which functions in a trusted file compile to Rust, and why not')
37
+ p.add_argument('script');p.add_argument('--json',action='store_true')
38
+ p=commands.add_parser('profile',help='Execute a trusted script once; rank native candidates')
39
+ p.add_argument('script');p.add_argument('arguments',nargs=argparse.REMAINDER)
40
+ p=commands.add_parser('cache',help='List old cache entries; delete only with --delete')
41
+ p.add_argument('--directory');p.add_argument('--days',type=float,default=30)
42
+ p.add_argument('--delete',action='store_true')
43
+ args=parser.parse_args()
44
+ if args.command=='check':
45
+ results=check_file(args.script)
46
+ if args.json:
47
+ print(json.dumps(results,indent=2))
48
+ else:
49
+ for row in results:
50
+ print(row['explanation'])
51
+ print()
52
+ native=sum(r['native'] for r in results)
53
+ print(f'{native} of {len(results)} functions compile to Rust.')
54
+ return
55
+ result=(profile_script(args.script,args.arguments) if args.command=='profile' else
56
+ prune_cache(args.directory,older_than_days=args.days,dry_run=not args.delete))
57
+ print(json.dumps(result,indent=2))
58
+
59
+ if __name__=='__main__':
60
+ main()
pyrust_engine/cache.py ADDED
@@ -0,0 +1,116 @@
1
+ """Private native cache with OS process locks, hashes and atomic publication."""
2
+ from contextlib import contextmanager
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import time
12
+
13
+ SUFFIX = '.dll' if sys.platform=='win32' else '.dylib' if sys.platform=='darwin' else '.so'
14
+
15
+ def cache_root(path=None):
16
+ root = Path(path) if path else Path.home()/'.cache'/'pyrust_engine'
17
+ root.mkdir(mode=0o700, parents=True, exist_ok=True)
18
+ root=root.resolve()
19
+ if os.name=='posix':
20
+ info=root.stat()
21
+ if info.st_uid!=os.getuid() or info.st_mode & 0o022:
22
+ raise PermissionError('Native cache must be owned by this user and not group/world writable')
23
+ return root
24
+
25
+ def digest(path):
26
+ h=hashlib.sha256()
27
+ with open(path,'rb') as f:
28
+ for chunk in iter(lambda:f.read(1024*1024),b''):
29
+ h.update(chunk)
30
+ return h.hexdigest()
31
+
32
+ @contextmanager
33
+ def process_lock(path, timeout=150):
34
+ # Advisory OS locks are released automatically if the process dies.
35
+ f=open(path,'a+b')
36
+ try:
37
+ f.seek(0,2)
38
+ if f.tell()==0:
39
+ f.write(b'0');f.flush()
40
+ start=time.monotonic()
41
+ while True:
42
+ try:
43
+ f.seek(0)
44
+ if sys.platform=='win32':
45
+ import msvcrt
46
+ msvcrt.locking(f.fileno(),msvcrt.LK_NBLCK,1)
47
+ else:
48
+ import fcntl
49
+ fcntl.flock(f.fileno(),fcntl.LOCK_EX|fcntl.LOCK_NB)
50
+ break
51
+ except (BlockingIOError,OSError):
52
+ if time.monotonic()-start >= timeout:
53
+ raise TimeoutError('Timed out waiting for native cache lock')
54
+ time.sleep(0.02)
55
+ try:
56
+ yield
57
+ finally:
58
+ if sys.platform=='win32':
59
+ import msvcrt
60
+ f.seek(0);msvcrt.locking(f.fileno(),msvcrt.LK_UNLCK,1)
61
+ else:
62
+ import fcntl
63
+ fcntl.flock(f.fileno(),fcntl.LOCK_UN)
64
+ finally:
65
+ f.close()
66
+
67
+ def cached_path(root,key):
68
+ try:
69
+ meta=json.loads((root/(key+'.json')).read_text())
70
+ sha=meta['sha256']
71
+ if not isinstance(sha,str) or not re.fullmatch('[0-9a-f]{64}',sha):
72
+ return None
73
+ path=root/(key+'-'+sha+SUFFIX)
74
+ return path if path.is_file() and digest(path)==sha else None
75
+ except (OSError,ValueError,KeyError,TypeError):
76
+ return None
77
+
78
+ def compile_library(root,key,compiler,source):
79
+ with tempfile.TemporaryDirectory(dir=root,prefix='build-') as work:
80
+ rs=Path(work)/'kernel.rs';binary=Path(work)/('kernel'+SUFFIX)
81
+ rs.write_text(source,encoding='utf-8')
82
+ result=subprocess.run([compiler,str(rs),'--crate-type','cdylib','--edition','2021',
83
+ '-C','opt-level=3','-C','panic=abort','-o',str(binary)],
84
+ capture_output=True,text=True,timeout=120)
85
+ if result.returncode:
86
+ raise RuntimeError('Rust compilation failed: '+result.stderr[-3000:])
87
+ sha=digest(binary)
88
+ path=root/(key+'-'+sha+SUFFIX)
89
+ # Never truncate a library another process could have mapped.
90
+ if not path.exists() or digest(path)!=sha:
91
+ os.replace(binary,path)
92
+ meta=Path(work)/'metadata.json'
93
+ meta.write_text(json.dumps({'sha256':sha,'created':time.time()}))
94
+ os.replace(meta,root/(key+'.json'))
95
+ return path
96
+
97
+ def prune_cache(path=None, *, older_than_days=30, dry_run=True):
98
+ """List/remove old cache entries. Defaults to a dry run. Lock files are retained."""
99
+ if older_than_days < 0:
100
+ raise ValueError('older_than_days must be nonnegative')
101
+ root=cache_root(path);entries=[];errors=[]
102
+ for meta in root.glob('*.json'):
103
+ key=meta.stem
104
+ if not re.fullmatch('[0-9a-f]{64}',key):
105
+ continue
106
+ with process_lock(root/(key+'.lock')):
107
+ if not meta.exists() or time.time()-meta.stat().st_mtime < older_than_days*86400:
108
+ continue
109
+ for file in [*root.glob(key+'-*'+SUFFIX),meta]:
110
+ entries.append(str(file))
111
+ if not dry_run:
112
+ try:
113
+ file.unlink(missing_ok=True)
114
+ except OSError as exc:
115
+ errors.append(str(exc))
116
+ return {'dry_run':dry_run,'entries':entries,'errors':errors}