Phasor 3.3.0
Stack VM based Programming Language
Loading...
Searching...
No Matches
Runtime.py
Go to the documentation of this file.
1"""
2phasor.Runtime
3==============
4cffi bindings for the PhasorRT shared library.
5
6State is an opaque handle returned by :func:`new_state` and passed into
7execution and evaluation functions. This mirrors the C API directly
8"""
9
10from __future__ import annotations
11
12import os
13import sys
14from pathlib import Path
15from typing import Optional, Sequence, Union
16
17from .Bytecode import Bytecode
18
19from cffi import FFI
20
21def _to_bytes(bytecode: "Union[bytes, bytearray, Bytecode]") -> bytes:
22 """Accept raw bytes/bytearray *or* a Bytecode object; always return bytes."""
23 if isinstance(bytecode, Bytecode):
24 return bytecode.to_bytes()
25 return bytes(bytecode)
26
27_ffi = FFI()
28_ffi.cdef(
29 """
30 int exec(void *state, const unsigned char *bytecode, size_t bytecodeSize,
31 const char *moduleName, int argc, const char **argv);
32
33 int evaluatePHS(void *state, const char *script, const char *moduleName,
34 const char *modulePath, bool verbose);
35
36 int evaluatePUL(void *state, const char *script, const char *moduleName);
37
38 bool compilePHS(const char *script, const char *moduleName,
39 const char *modulePath,
40 unsigned char *buffer, size_t bufferSize, size_t *outSize);
41
42 bool compilePUL(const char *script, const char *moduleName,
43 unsigned char *buffer, size_t bufferSize, size_t *outSize);
44
45 void *createState(void);
46
47 void initStdLib(void *state);
48
49 bool freeState(void *state);
50
51 bool resetState(void *state, bool resetFunctions, bool resetVariables);
52 """
53)
54
55_CANDIDATES: dict[str, list[str]] = {
56 "linux": ["libphasorrt.so", "libphasorrt.so.1"],
57 "darwin": ["libphasorrt.dylib", "libphasorrt.1.dylib"],
58 "win32": ["phasorrt.dll"],
59}
60
61_LOAD_HINT: dict[str, str] = {
62 "linux": "Set LD_LIBRARY_PATH or run ldconfig after installing Phasor.",
63 "darwin": "Set DYLD_LIBRARY_PATH or install Phasor.",
64 "win32": "Add phasorrt.dll to your system PATH",
65}
66
67_lib = None # loaded lazily on first use
68
69
71 global _lib
72 if _lib is not None:
73 return _lib
74
75 key = "linux" if sys.platform.startswith("linux") else sys.platform
76 names = _CANDIDATES.get(key, _CANDIDATES["linux"])
77 hint = _LOAD_HINT.get(key, "Ensure the library is on your system library search path.")
78
79 errors: list[str] = []
80 for name in names:
81 try:
82 _lib = _ffi.dlopen(name)
83 return _lib
84 except OSError as exc:
85 errors.append(f" {name}: {exc}")
86
87 raise OSError(
88 f"Could not load PhasorRT shared library on {sys.platform}.\n"
89 "Tried:\n" + "\n".join(errors) + f"\n{hint}"
90 )
91
92_DEFAULT_MODULE_NAME = "Python"
93
94StateHandle = object # cffi cdata void *
95
96
97def _cwd_bytes() -> bytes:
98 return os.getcwd().encode("utf-8")
99
100
101def _ptr(state: Optional[StateHandle]):
102 """Return the raw cffi pointer for *state*, or NULL if *state* is None."""
103 return state if state is not None else _ffi.NULL
104
105
106def _build_argv(args: Sequence[str]):
107 """Convert a sequence of strings to ``(argc, argv_p)`` for cffi."""
108 if not args:
109 return 0, _ffi.NULL
110 encoded = [_ffi.new("char[]", a.encode("utf-8")) for a in args]
111 return len(encoded), _ffi.new("const char *[]", encoded)
112
113
114def _two_pass_compile(compile_fn, *leading_args: bytes) -> Bytecode:
115 """Size-probe then compile as required by the C API.
116
117 Returns:
118 A :class:`~phasor.Bytecode.Bytecode` object built from the compiled output.
119 """
120 out_size = _ffi.new("size_t *")
121
122 if not compile_fn(*leading_args, _ffi.NULL, 0, out_size):
123 raise RuntimeError("Compilation failed, check your source for errors.")
124
125 size = out_size[0]
126 buf = _ffi.new(f"unsigned char[{size}]")
127
128 if not compile_fn(*leading_args, buf, size, out_size):
129 raise RuntimeError("Compilation failed during buffer write; this is likely a PhasorRT bug.")
130
131 return Bytecode.from_bytes(bytes(_ffi.buffer(buf, size)))
132
133def new_state() -> StateHandle:
134 """Allocate a new Phasor VM state and return its opaque handle.
135
136 Pass the handle to any execution or evaluation function via the
137 ``state`` parameter to reuse the same VM across multiple calls,
138 preserving globals and registered functions between them.
139
140 Always pair with :func:`free_state` when the state is no longer needed.
141
142 Returns:
143 An opaque cffi handle representing the new VM state.
144
145 Raises:
146 RuntimeError: If ``createState()`` returns NULL.
147 OSError: If the PhasorRT library cannot be loaded.
148
149 Example::
150
151 vm = new_state()
152 free_state(vm)
153 """
154 lib = _get_lib()
155 ptr = lib.createState()
156 if ptr == _ffi.NULL:
157 raise RuntimeError("createState() returned NULL; PhasorRT may be uninitialised.")
158 return ptr
159
160def init_stdlib(state: StateHandle) -> None:
161 """Register standard library functions into a given state.
162
163 Args:
164 state: The handle returned by :func:`new_state`.
165
166 Raises:
167 RuntimeError: If ``initStdLib()`` reports failure.
168 """
169 if not _get_lib().initStdLib(state):
170 raise RuntimeError("initStdLib() failed.")
171
172def free_state(state: StateHandle) -> None:
173 """Release a VM state created by :func:`new_state`.
174
175 The handle must not be used after this call.
176
177 Args:
178 state: The handle returned by :func:`new_state`.
179
180 Raises:
181 RuntimeError: If ``freeState()`` reports failure (double-free or corrupt pointer).
182 OSError: If the PhasorRT library cannot be loaded.
183 """
184 if not _get_lib().freeState(state):
185 raise RuntimeError("freeState() failed; the state may already have been freed.")
186
187
189 state: StateHandle,
190 *,
191 reset_functions: bool = False,
192 reset_variables: bool = False,
193) -> None:
194 """Reset a VM state (stack, PC, and bytecode are always cleared).
195
196 Args:
197 state: The handle returned by :func:`new_state`.
198 reset_functions: Also clear all registered functions when ``True``.
199 reset_variables: Also clear all global variables when ``True``.
200
201 Raises:
202 RuntimeError: If ``resetState()`` reports failure.
203 OSError: If the PhasorRT library cannot be loaded.
204
205 Example::
206
207 # Soft reset: keep globals and functions, just reset execution state.
208 reset_state(vm)
209
210 # Full wipe.
211 reset_state(vm, reset_functions=True, reset_variables=True)
212 """
213 if not _get_lib().resetState(state, reset_functions, reset_variables):
214 raise RuntimeError("resetState() failed; state may be corrupt.")
215
217 script: str,
218 *,
219 module_name: str = _DEFAULT_MODULE_NAME,
220 module_path: Optional[str] = None,
221) -> bytes:
222 """Compile a Phasor (``.phs``) source string to ``.phsb`` bytecode.
223
224 Args:
225 script: Phasor source code to compile.
226 module_name: Name reported in error messages. Defaults to ``"Python"``.
227 module_path: Directory for resolving compile-time imports.
228 Defaults to the current working directory.
229
230 Returns:
231 A :class:`~phasor.Bytecode.Bytecode` object, ready to inspect, modify,
232 pass to :func:`run`, or serialise with
233 :meth:`~phasor.Bytecode.Bytecode.save` / :meth:`~phasor.Bytecode.Bytecode.to_bytes`.
234
235 Raises:
236 RuntimeError: If compilation fails.
237 OSError: If the PhasorRT library cannot be loaded.
238 """
239 mod_path = module_path.encode("utf-8") if module_path else _cwd_bytes()
240 return _two_pass_compile(
241 _get_lib().compilePHS,
242 script.encode("utf-8"),
243 module_name.encode("utf-8"),
244 mod_path,
245 )
246
247
249 path: str | Path,
250 *,
251 module_name: Optional[str] = None,
252 module_path: Optional[str] = None,
253) -> bytes:
254 """Read a ``.phs`` file and compile it to ``.phsb`` bytecode.
255
256 Args:
257 path: Path to the ``.phs`` source file.
258 module_name: Name reported in error messages.
259 Defaults to the file's stem (e.g. ``"hello"`` for ``hello.phs``).
260 module_path: Directory for resolving compile-time imports.
261 Defaults to the parent directory of *path*.
262
263 Returns:
264 A :class:`~phasor.Bytecode.Bytecode` object.
265
266 Raises:
267 FileNotFoundError: If *path* does not exist.
268 RuntimeError: If compilation fails.
269 OSError: If the PhasorRT library cannot be loaded.
270 """
271 p = Path(path)
272 if not p.is_file():
273 raise FileNotFoundError(f"Source file not found: {p}")
274 return compile_phs(
275 p.read_text(encoding="utf-8"),
276 module_name=module_name or p.stem,
277 module_path=module_path or str(p.resolve().parent),
278 )
279
280
282 script: str,
283 *,
284 module_name: str = _DEFAULT_MODULE_NAME,
285) -> bytes:
286 """Compile a Pulsar (``.pul``) source string to ``.phsb`` bytecode.
287
288 Args:
289 script: Pulsar source code to compile.
290 module_name: Name reported in error messages. Defaults to ``"Python"``.
291
292 Returns:
293 A :class:`~phasor.Bytecode.Bytecode` object.
294
295 Raises:
296 RuntimeError: If compilation fails.
297 OSError: If the PhasorRT library cannot be loaded.
298 """
299 return _two_pass_compile(
300 _get_lib().compilePUL,
301 script.encode("utf-8"),
302 module_name.encode("utf-8"),
303 )
304
305
307 path: str | Path,
308 *,
309 module_name: Optional[str] = None,
310) -> bytes:
311 """Read a ``.pul`` file and compile it to ``.phsb`` bytecode.
312
313 Args:
314 path: Path to the ``.pul`` source file.
315 module_name: Name reported in error messages.
316 Defaults to the file's stem.
317
318 Returns:
319 A :class:`~phasor.Bytecode.Bytecode` object.
320
321 Raises:
322 FileNotFoundError: If *path* does not exist.
323 RuntimeError: If compilation fails.
324 OSError: If the PhasorRT library cannot be loaded.
325 """
326 p = Path(path)
327 if not p.is_file():
328 raise FileNotFoundError(f"Source file not found: {p}")
329 return compile_pul(
330 p.read_text(encoding="utf-8"),
331 module_name=module_name or p.stem,
332 )
333
334def run(
335 bytecode: Union[bytes, bytearray, Bytecode],
336 *,
337 state: Optional[StateHandle] = None,
338 module_name: str = _DEFAULT_MODULE_NAME,
339 args: Sequence[str] = (),
340) -> int:
341 """Execute pre-compiled ``.phsb`` bytecode.
342
343 Args:
344 bytecode: Raw ``.phsb`` bytes **or** a :class:`~phasor.Bytecode.Bytecode`
345 object (e.g. from :func:`compile_phs` or
346 :meth:`~phasor.Bytecode.Bytecode.load`).
347 state: An optional handle from :func:`new_state`. When ``None``,
348 PhasorRT creates and manages a transient state internally.
349 module_name: Name reported in error messages. Defaults to ``"Python"``.
350 args: Command-line arguments forwarded to the script as ``argv``.
351
352 Returns:
353 The script's exit code (``-1`` may indicate an unhandled VM exception).
354
355 Raises:
356 OSError: If the PhasorRT library cannot be loaded.
357 """
358 raw = _to_bytes(bytecode)
359 data = _ffi.from_buffer("unsigned char[]", raw)
360 argc, argv = _build_argv(args)
361 return _get_lib().exec(
362 _ptr(state), data, len(raw),
363 module_name.encode("utf-8"), argc, argv,
364 )
365
366
368 path: str | Path,
369 *,
370 state: Optional[StateHandle] = None,
371 module_name: Optional[str] = None,
372 args: Sequence[str] = (),
373) -> int:
374 """Load a ``.phsb`` file and execute it.
375
376 Args:
377 path: Path to the ``.phsb`` bytecode file.
378 state: An optional handle from :func:`new_state`.
379 module_name: Name reported in error messages.
380 Defaults to the file's stem.
381 args: Command-line arguments forwarded to the script.
382
383 Returns:
384 The script's exit code.
385
386 Raises:
387 FileNotFoundError: If *path* does not exist.
388 OSError: If the PhasorRT library cannot be loaded.
389 """
390 p = Path(path)
391 if not p.is_file():
392 raise FileNotFoundError(f"Bytecode file not found: {p}")
393 return run(
394 p.read_bytes(),
395 state=state,
396 module_name=module_name or p.stem,
397 args=args,
398 )
399
400
402 script: str,
403 *,
404 state: Optional[StateHandle] = None,
405 module_name: str = _DEFAULT_MODULE_NAME,
406 module_path: Optional[str] = None,
407 verbose: bool = False,
408) -> int:
409 """Compile and execute a Phasor source string.
410
411 Args:
412 script: Phasor source code.
413 state: An optional handle from :func:`new_state`. When ``None``,
414 PhasorRT creates and manages a transient state internally.
415 module_name: Name reported in error messages. Defaults to ``"Python"``.
416 module_path: Directory for resolving compile-time imports.
417 Defaults to the current working directory.
418 verbose: Print the AST to stdout when ``True``.
419
420 Returns:
421 The script's exit code.
422
423 Raises:
424 OSError: If the PhasorRT library cannot be loaded.
425 """
426 mod_path = module_path.encode("utf-8") if module_path else _cwd_bytes()
427 return _get_lib().evaluatePHS(
428 _ptr(state),
429 script.encode("utf-8"),
430 module_name.encode("utf-8"),
431 mod_path,
432 verbose,
433 )
434
435
437 path: str | Path,
438 *,
439 state: Optional[StateHandle] = None,
440 module_name: Optional[str] = None,
441 verbose: bool = False,
442) -> int:
443 """Read and evaluate a ``.phs`` source file.
444
445 The file's parent directory is automatically used for resolving
446 compile-time imports.
447
448 Args:
449 path: Path to the ``.phs`` source file.
450 state: An optional handle from :func:`new_state`.
451 module_name: Name reported in error messages.
452 Defaults to the file's stem.
453 verbose: Print the AST to stdout when ``True``.
454
455 Returns:
456 The script's exit code.
457
458 Raises:
459 FileNotFoundError: If *path* does not exist.
460 OSError: If the PhasorRT library cannot be loaded.
461 """
462 p = Path(path)
463 if not p.is_file():
464 raise FileNotFoundError(f"Source file not found: {p}")
465 return evaluate_phs(
466 p.read_text(encoding="utf-8"),
467 state=state,
468 module_name=module_name or p.stem,
469 module_path=str(p.resolve().parent),
470 verbose=verbose,
471 )
472
473
475 script: str,
476 *,
477 state: Optional[StateHandle] = None,
478 module_name: str = _DEFAULT_MODULE_NAME,
479) -> int:
480 """Compile and execute a Pulsar source string.
481
482 Args:
483 script: Pulsar source code.
484 state: An optional handle from :func:`new_state`. When ``None``,
485 PhasorRT creates and manages a transient state internally.
486 module_name: Name reported in error messages. Defaults to ``"Python"``.
487
488 Returns:
489 The script's exit code.
490
491 Raises:
492 OSError: If the PhasorRT library cannot be loaded.
493 """
494 return _get_lib().evaluatePUL(
495 _ptr(state),
496 script.encode("utf-8"),
497 module_name.encode("utf-8"),
498 )
499
500
502 path: str | Path,
503 *,
504 state: Optional[StateHandle] = None,
505 module_name: Optional[str] = None,
506) -> int:
507 """Read and evaluate a ``.pul`` source file.
508
509 Args:
510 path: Path to the ``.pul`` source file.
511 state: An optional handle from :func:`new_state`.
512 module_name: Name reported in error messages.
513 Defaults to the file's stem.
514
515 Returns:
516 The script's exit code.
517
518 Raises:
519 FileNotFoundError: If *path* does not exist.
520 OSError: If the PhasorRT library cannot be loaded.
521 """
522 p = Path(path)
523 if not p.is_file():
524 raise FileNotFoundError(f"Source file not found: {p}")
525 return evaluate_pul(
526 p.read_text(encoding="utf-8"),
527 state=state,
528 module_name=module_name or p.stem,
529 )
PHASOR_API int evaluatePUL(void *vmPtr, const char *script, const char *moduleName)
Executes a Pulsar Scripting Language script.
PHASOR_API int evaluatePHS(void *vmPtr, const char *script, const char *moduleName, const char *modulePath, bool verbose)
Executes a Phasor Programming Language script.
PHASOR_API bool freeState(void *vmPtr)
Frees an existing state instance.
PHASOR_API void initStdLib(void *vmPtr)
Register standard library to state instance.
PHASOR_API bool resetState(void *vmPtr, bool resetFunctions, bool resetVariables)
Resets the state.
bytes compile_phs_file(str|Path path, *, Optional[str] module_name=None, Optional[str] module_path=None)
Definition Runtime.py:253
None init_stdlib(StateHandle state)
Definition Runtime.py:160
int evaluate_pul(str script, *, Optional[StateHandle] state=None, str module_name=_DEFAULT_MODULE_NAME)
Definition Runtime.py:479
_ptr(Optional[StateHandle] state)
Definition Runtime.py:101
int evaluate_pul_file(str|Path path, *, Optional[StateHandle] state=None, Optional[str] module_name=None)
Definition Runtime.py:506
None reset_state(StateHandle state, *, bool reset_functions=False, bool reset_variables=False)
Definition Runtime.py:193
None free_state(StateHandle state)
Definition Runtime.py:172
Bytecode _two_pass_compile(compile_fn, *bytes leading_args)
Definition Runtime.py:114
bytes compile_phs(str script, *, str module_name=_DEFAULT_MODULE_NAME, Optional[str] module_path=None)
Definition Runtime.py:221
int evaluate_phs_file(str|Path path, *, Optional[StateHandle] state=None, Optional[str] module_name=None, bool verbose=False)
Definition Runtime.py:442
bytes _cwd_bytes()
Definition Runtime.py:97
_build_argv(Sequence[str] args)
Definition Runtime.py:106
int run(Union[bytes, bytearray, Bytecode] bytecode, *, Optional[StateHandle] state=None, str module_name=_DEFAULT_MODULE_NAME, Sequence[str] args=())
Definition Runtime.py:340
bytes compile_pul_file(str|Path path, *, Optional[str] module_name=None)
Definition Runtime.py:310
StateHandle new_state()
Definition Runtime.py:133
bytes _to_bytes("Union[bytes, bytearray, Bytecode]" bytecode)
Definition Runtime.py:21
bytes compile_pul(str script, *, str module_name=_DEFAULT_MODULE_NAME)
Definition Runtime.py:285
int evaluate_phs(str script, *, Optional[StateHandle] state=None, str module_name=_DEFAULT_MODULE_NAME, Optional[str] module_path=None, bool verbose=False)
Definition Runtime.py:408
int run_file(str|Path path, *, Optional[StateHandle] state=None, Optional[str] module_name=None, Sequence[str] args=())
Definition Runtime.py:373
int exec(void *state, const unsigned char embeddedBytecode[], size_t embeddedBytecodeSize, const char *moduleName, int argc, const char *argv[])