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.
- pyrust_engine/__init__.py +5 -0
- pyrust_engine/__main__.py +60 -0
- pyrust_engine/cache.py +116 -0
- pyrust_engine/compiler.py +812 -0
- pyrust_engine/profiling.py +45 -0
- pyrust_engine/py.typed +0 -0
- pyrust_engine/runtime.py +400 -0
- pyrust_engine-0.6.0.dist-info/METADATA +473 -0
- pyrust_engine-0.6.0.dist-info/RECORD +12 -0
- pyrust_engine-0.6.0.dist-info/WHEEL +5 -0
- pyrust_engine-0.6.0.dist-info/licenses/LICENSE +21 -0
- pyrust_engine-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -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}
|