koskript 1.4.0__tar.gz → 1.4.2__tar.gz
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.
- {koskript-1.4.0 → koskript-1.4.2}/PKG-INFO +16 -8
- {koskript-1.4.0 → koskript-1.4.2}/README.md +95 -87
- {koskript-1.4.0 → koskript-1.4.2}/pyproject.toml +4 -1
- koskript-1.4.2/src/koskript/__init__.py +328 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/grammar.lark +54 -3
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/lang/astgen.py +152 -5
- koskript-1.4.2/src/koskript/lang/compiler.py +1004 -0
- koskript-1.4.2/src/koskript/lang/emtypes.py +513 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/lang/errors.py +12 -1
- koskript-1.4.2/src/koskript/lang/interpreter.py +679 -0
- koskript-1.4.2/src/koskript/lang/lexer.py +352 -0
- koskript-1.4.2/src/koskript/lang/parser.py +676 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/__init__.py +2 -1
- koskript-1.4.2/src/koskript/stdlib/byteslib.py +115 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/core.py +18 -2
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/helpers.py +83 -73
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/PKG-INFO +16 -8
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/SOURCES.txt +8 -0
- koskript-1.4.2/tests/test_grammar.py +700 -0
- koskript-1.4.2/tests/test_language.py +980 -0
- koskript-1.4.2/tests/test_modules.py +421 -0
- koskript-1.4.2/tests/test_parser.py +667 -0
- {koskript-1.4.0 → koskript-1.4.2}/tests/test_stdlib.py +95 -0
- koskript-1.4.0/src/koskript/__init__.py +0 -117
- koskript-1.4.0/src/koskript/lang/emtypes.py +0 -215
- koskript-1.4.0/src/koskript/lang/interpreter.py +0 -742
- {koskript-1.4.0 → koskript-1.4.2}/LICENSE +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/setup.cfg +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/arrays.py +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/jsonlib.py +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/maps.py +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/mathlib.py +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/strings.py +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/dependency_links.txt +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/requires.txt +0 -0
- {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: koskript
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: A programming language fully embbeded on python.
|
|
5
5
|
Author: alesis.buzz
|
|
6
6
|
License: Mozilla Public License Version 2.0
|
|
@@ -378,7 +378,7 @@ License: Mozilla Public License Version 2.0
|
|
|
378
378
|
defined by the Mozilla Public License, v. 2.0.
|
|
379
379
|
|
|
380
380
|
Project-URL: Homepage, https://github.com/alesis-buzz/koskript
|
|
381
|
-
Keywords: scripting,programming-language,embedded,interpreter,lark
|
|
381
|
+
Keywords: scripting,programming-language,embedded,interpreter,parser,lark
|
|
382
382
|
Classifier: Development Status :: 3 - Alpha
|
|
383
383
|
Classifier: Programming Language :: Python :: 3
|
|
384
384
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -404,6 +404,8 @@ applications. It features dynamic typing, lexical scoping, closures, classes and
|
|
|
404
404
|
native Python interop — letting you expose any Python function or object
|
|
405
405
|
directly to your scripts.
|
|
406
406
|
|
|
407
|
+
See the documentation [here](https://koskript.alesis.buzz)
|
|
408
|
+
|
|
407
409
|
```python
|
|
408
410
|
from koskript import KoskriptRuntime
|
|
409
411
|
|
|
@@ -414,9 +416,8 @@ array.each(names, (name) { print("hello " + name) })
|
|
|
414
416
|
""")
|
|
415
417
|
```
|
|
416
418
|
|
|
417
|
-
> **NOTE:** Koskript is in early development
|
|
418
|
-
>
|
|
419
|
-
> between releases.
|
|
419
|
+
> **NOTE:** Koskript is in early development; expect the language and API to
|
|
420
|
+
> change between releases.
|
|
420
421
|
|
|
421
422
|
---
|
|
422
423
|
|
|
@@ -424,15 +425,20 @@ array.each(names, (name) { print("hello " + name) })
|
|
|
424
425
|
|
|
425
426
|
- Dynamic typing.
|
|
426
427
|
- Block-level lexical scoping with `local` declarations.
|
|
428
|
+
- Namespaces (`namespace Name { ... }`) to group declarations.
|
|
427
429
|
- Closures that capture their defining scope, including `this` inside methods.
|
|
428
430
|
- Constants with `const`.
|
|
429
431
|
- Classes with inheritance, visibility, static methods and constructors.
|
|
430
|
-
- Standard library: core builtins plus `map`, `array`, `string`, `math` and `json`.
|
|
432
|
+
- Standard library: core builtins plus `map`, `array`, `string`, `bytes`, `math` and `json`.
|
|
433
|
+
- Module imports with per-module resolution (`import "utils"`).
|
|
434
|
+
- Configurable import search paths (`runtime.add_import_path`).
|
|
431
435
|
- Native Python interop for functions, objects and classes.
|
|
432
436
|
- `if`, `elseif`, `else`.
|
|
433
437
|
- `while`, `for`, `foreach` loops with `break` / `continue`.
|
|
438
|
+
- Error handling with `error` declarations, `throw`, `try` / `catch` / `finally`.
|
|
434
439
|
- Member access and index access.
|
|
435
440
|
- First-class functions and lambda expressions.
|
|
441
|
+
- Wrappers with implicit return and Python-style decorators (`@wrapper`).
|
|
436
442
|
- Arithmetic, comparison and logical operators.
|
|
437
443
|
- Embeddable in any Python application.
|
|
438
444
|
|
|
@@ -451,6 +457,8 @@ git clone https://github.com/alesis-buzz/koskript.git
|
|
|
451
457
|
```
|
|
452
458
|
|
|
453
459
|
Koskript requires Python 3.10+ and [Lark](https://github.com/lark-parser/lark).
|
|
460
|
+
There is an experimental dependency-free parser behind
|
|
461
|
+
`KoskriptRuntime(native_parser_experiment=True)`.
|
|
454
462
|
|
|
455
463
|
---
|
|
456
464
|
|
|
@@ -459,9 +467,9 @@ Koskript requires Python 3.10+ and [Lark](https://github.com/lark-parser/lark).
|
|
|
459
467
|
| Document | What it covers |
|
|
460
468
|
|---|---|
|
|
461
469
|
| [Getting started](docs/getting-started.md) | Installation, first script, a complete example |
|
|
462
|
-
| [Language reference](docs/language.md) | Types, variables, operators, control flow, functions, closures, loops, strings |
|
|
470
|
+
| [Language reference](docs/language.md) | Types, variables, operators, control flow, errors, functions, wrappers, namespaces, closures, modules, loops, strings, bytes |
|
|
463
471
|
| [Classes](docs/classes.md) | Inheritance, visibility, static members, constructors, `super` |
|
|
464
|
-
| [Standard library](docs/standard-library.md) | Core builtins and the `map`, `array`, `string`, `math` and `json` namespaces |
|
|
472
|
+
| [Standard library](docs/standard-library.md) | Core builtins and the `map`, `array`, `string`, `bytes`, `math` and `json` namespaces |
|
|
465
473
|
| [Python interop](docs/python-interop.md) | Exposing Python values, callables and classes to scripts |
|
|
466
474
|
| [Embedding guide](docs/embedding.md) | `KoskriptRuntime`, `run()`, error handling and stdlib activation |
|
|
467
475
|
| [Roadmap](docs/roadmap.md) | What is done, what is planned, known limitations |
|
|
@@ -1,87 +1,95 @@
|
|
|
1
|
-

|
|
2
|
-
|
|
3
|
-
# Koskript
|
|
4
|
-
|
|
5
|
-
Koskript is a simple, embeddable, and lightweight scripting language for Python
|
|
6
|
-
applications. It features dynamic typing, lexical scoping, closures, classes and
|
|
7
|
-
native Python interop — letting you expose any Python function or object
|
|
8
|
-
directly to your scripts.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- `
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Koskript
|
|
4
|
+
|
|
5
|
+
Koskript is a simple, embeddable, and lightweight scripting language for Python
|
|
6
|
+
applications. It features dynamic typing, lexical scoping, closures, classes and
|
|
7
|
+
native Python interop — letting you expose any Python function or object
|
|
8
|
+
directly to your scripts.
|
|
9
|
+
|
|
10
|
+
See the documentation [here](https://koskript.alesis.buzz)
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from koskript import KoskriptRuntime
|
|
14
|
+
|
|
15
|
+
runtime = KoskriptRuntime()
|
|
16
|
+
runtime.execute("""
|
|
17
|
+
const names = ["Ana", "Luis"]
|
|
18
|
+
array.each(names, (name) { print("hello " + name) })
|
|
19
|
+
""")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **NOTE:** Koskript is in early development; expect the language and API to
|
|
23
|
+
> change between releases.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Dynamic typing.
|
|
30
|
+
- Block-level lexical scoping with `local` declarations.
|
|
31
|
+
- Namespaces (`namespace Name { ... }`) to group declarations.
|
|
32
|
+
- Closures that capture their defining scope, including `this` inside methods.
|
|
33
|
+
- Constants with `const`.
|
|
34
|
+
- Classes with inheritance, visibility, static methods and constructors.
|
|
35
|
+
- Standard library: core builtins plus `map`, `array`, `string`, `bytes`, `math` and `json`.
|
|
36
|
+
- Module imports with per-module resolution (`import "utils"`).
|
|
37
|
+
- Configurable import search paths (`runtime.add_import_path`).
|
|
38
|
+
- Native Python interop for functions, objects and classes.
|
|
39
|
+
- `if`, `elseif`, `else`.
|
|
40
|
+
- `while`, `for`, `foreach` loops with `break` / `continue`.
|
|
41
|
+
- Error handling with `error` declarations, `throw`, `try` / `catch` / `finally`.
|
|
42
|
+
- Member access and index access.
|
|
43
|
+
- First-class functions and lambda expressions.
|
|
44
|
+
- Wrappers with implicit return and Python-style decorators (`@wrapper`).
|
|
45
|
+
- Arithmetic, comparison and logical operators.
|
|
46
|
+
- Embeddable in any Python application.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -U koskript
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or clone the repository and import it directly:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/alesis-buzz/koskript.git
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Koskript requires Python 3.10+ and [Lark](https://github.com/lark-parser/lark).
|
|
63
|
+
There is an experimental dependency-free parser behind
|
|
64
|
+
`KoskriptRuntime(native_parser_experiment=True)`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
| Document | What it covers |
|
|
71
|
+
|---|---|
|
|
72
|
+
| [Getting started](docs/getting-started.md) | Installation, first script, a complete example |
|
|
73
|
+
| [Language reference](docs/language.md) | Types, variables, operators, control flow, errors, functions, wrappers, namespaces, closures, modules, loops, strings, bytes |
|
|
74
|
+
| [Classes](docs/classes.md) | Inheritance, visibility, static members, constructors, `super` |
|
|
75
|
+
| [Standard library](docs/standard-library.md) | Core builtins and the `map`, `array`, `string`, `bytes`, `math` and `json` namespaces |
|
|
76
|
+
| [Python interop](docs/python-interop.md) | Exposing Python values, callables and classes to scripts |
|
|
77
|
+
| [Embedding guide](docs/embedding.md) | `KoskriptRuntime`, `run()`, error handling and stdlib activation |
|
|
78
|
+
| [Roadmap](docs/roadmap.md) | What is done, what is planned, known limitations |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
Koskript is licensed under the Mozilla Public License 2.0 (MPL-2.0). See
|
|
85
|
+
[LICENSE](LICENSE) for the full text.
|
|
86
|
+
|
|
87
|
+
You are free to use, modify, and redistribute Koskript. However, you may not
|
|
88
|
+
redistribute this project under a different name or claim authorship of the
|
|
89
|
+
Koskript language.
|
|
90
|
+
|
|
91
|
+
**Koskript is a trademark of Alesis.**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
*Built with ❤️ by Alesis*
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "koskript"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.2"
|
|
8
8
|
description = "A programming language fully embbeded on python."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -19,6 +19,7 @@ keywords = [
|
|
|
19
19
|
"programming-language",
|
|
20
20
|
"embedded",
|
|
21
21
|
"interpreter",
|
|
22
|
+
"parser",
|
|
22
23
|
"lark"
|
|
23
24
|
]
|
|
24
25
|
|
|
@@ -37,6 +38,8 @@ classifiers = [
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
dependencies = [
|
|
41
|
+
# only the default parser needs it; `native_parser_experiment=True`
|
|
42
|
+
# runs without it
|
|
40
43
|
"lark>=1.3.1"
|
|
41
44
|
]
|
|
42
45
|
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from .lang.emtypes import (KoskriptObject, Module, Namespace, Scope, ScopeInfo,
|
|
4
|
+
ThrownSignal)
|
|
5
|
+
from .lang.interpreter import KoskriptInterpreter
|
|
6
|
+
from .lang.parser import parse
|
|
7
|
+
from .lang.errors import Errors
|
|
8
|
+
from .stdlib import build_globals
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
_grammar_path = os.path.join(pathlib.Path(__file__).resolve().parent,
|
|
12
|
+
"grammar.lark")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _wrap(value):
|
|
16
|
+
return value if isinstance(value, KoskriptObject) else KoskriptObject(value=value)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _require_lark():
|
|
20
|
+
"""Import Lark, with a message that points at the experimental parser."""
|
|
21
|
+
try:
|
|
22
|
+
from lark import Lark
|
|
23
|
+
except ImportError:
|
|
24
|
+
raise Errors.RuntimeError(
|
|
25
|
+
"the default parser needs the 'lark' package: install it, or pass "
|
|
26
|
+
"native_parser_experiment=True to use the native parser") from None
|
|
27
|
+
return Lark
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _Grammar(object):
|
|
31
|
+
"""The Lark grammar, with the LALR tables built on first use.
|
|
32
|
+
|
|
33
|
+
Building them takes about 200 ms, so it is deferred until a script is
|
|
34
|
+
actually parsed: runtimes created with ``native_parser_experiment=True``
|
|
35
|
+
never pay for them.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("path", "parser")
|
|
39
|
+
|
|
40
|
+
def __init__(self, path):
|
|
41
|
+
self.path = path
|
|
42
|
+
self.parser = None
|
|
43
|
+
|
|
44
|
+
def parse(self, code: str):
|
|
45
|
+
"""Parse ``code`` into a Lark tree."""
|
|
46
|
+
if self.parser is None:
|
|
47
|
+
with open(self.path, "r") as grammar_file:
|
|
48
|
+
self.parser = _require_lark()(
|
|
49
|
+
grammar_file.read(), parser="lalr", maybe_placeholders=False)
|
|
50
|
+
return self.parser.parse(code)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
grammar = _Grammar(_grammar_path)
|
|
54
|
+
|
|
55
|
+
# How many compiled sources a runtime keeps. Running the same script again is
|
|
56
|
+
# then free of parsing and compiling; the oldest entry is dropped past this.
|
|
57
|
+
_CHUNK_CACHE = 32
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _format_syntax_error(code: str, error) -> str:
|
|
61
|
+
from lark.exceptions import UnexpectedInput
|
|
62
|
+
|
|
63
|
+
if not isinstance(error, UnexpectedInput) or error.line is None:
|
|
64
|
+
return str(error)
|
|
65
|
+
|
|
66
|
+
lines = [f"Syntax error at line {error.line}, column {error.column}:", error.get_context(code).rstrip()]
|
|
67
|
+
|
|
68
|
+
token = getattr(error, "token", None)
|
|
69
|
+
if token is not None and token.type != "$END":
|
|
70
|
+
lines.append(f"Unexpected token '{token}'.")
|
|
71
|
+
|
|
72
|
+
expected = getattr(error, "expected", None)
|
|
73
|
+
if expected:
|
|
74
|
+
lines.append("Expected one of: " + ", ".join(sorted(expected)))
|
|
75
|
+
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class KoskriptRuntime(object):
|
|
80
|
+
"""Embeddable Koskript runtime.
|
|
81
|
+
|
|
82
|
+
The standard library is registered by default. Host globals are registered
|
|
83
|
+
after it, so any of them can override a standard library name. Pass
|
|
84
|
+
``stdlib=False`` to skip the standard library entirely.
|
|
85
|
+
|
|
86
|
+
Any Python value or callable exposed to scripts is wrapped automatically,
|
|
87
|
+
so you can pass plain functions without building ``KoskriptObject`` by hand.
|
|
88
|
+
|
|
89
|
+
Scripts are parsed with Lark, the default and stable front end, which
|
|
90
|
+
needs the ``lark`` package. ``native_parser_experiment=True`` switches to
|
|
91
|
+
the experimental pure Python scanner and parser instead: it needs no
|
|
92
|
+
dependency, starts faster and parses about 6x quicker, and it is what
|
|
93
|
+
will become the default once it has been validated in production.
|
|
94
|
+
|
|
95
|
+
>>> rt = KoskriptRuntime({"print": print})
|
|
96
|
+
>>> rt.execute("local x = 10\\nx + 26")
|
|
97
|
+
36
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, globals_map=None, _globals_=None, stdlib=True,
|
|
101
|
+
native_parser_experiment=False):
|
|
102
|
+
if _globals_ is not None:
|
|
103
|
+
globals_map = {**(globals_map or {}), **_globals_}
|
|
104
|
+
|
|
105
|
+
self.globals = {}
|
|
106
|
+
self.native_parser_experiment = bool(native_parser_experiment)
|
|
107
|
+
self.__interpreter__ = KoskriptInterpreter()
|
|
108
|
+
self._ast = None
|
|
109
|
+
self.__interpreter__.module_loader = self._load_module
|
|
110
|
+
self._modules = {}
|
|
111
|
+
self._loading = []
|
|
112
|
+
self._import_paths = []
|
|
113
|
+
self._chunks = {}
|
|
114
|
+
|
|
115
|
+
if not self.native_parser_experiment:
|
|
116
|
+
# fail here instead of on the first script
|
|
117
|
+
_require_lark()
|
|
118
|
+
|
|
119
|
+
if stdlib:
|
|
120
|
+
self.register_many(build_globals(self.__interpreter__.call_value))
|
|
121
|
+
|
|
122
|
+
if globals_map:
|
|
123
|
+
self.register_many(globals_map)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def __ast__(self):
|
|
127
|
+
"""The Lark AST transformer, created the first time it is needed.
|
|
128
|
+
|
|
129
|
+
It lives in :mod:`koskript.lang.astgen`, which imports Lark, so
|
|
130
|
+
runtimes using the native parser never load it.
|
|
131
|
+
"""
|
|
132
|
+
transformer = self._ast
|
|
133
|
+
if transformer is None:
|
|
134
|
+
from .lang.astgen import KoskriptTransformer
|
|
135
|
+
transformer = self._ast = KoskriptTransformer()
|
|
136
|
+
return transformer
|
|
137
|
+
|
|
138
|
+
@__ast__.setter
|
|
139
|
+
def __ast__(self, transformer):
|
|
140
|
+
self._ast = transformer
|
|
141
|
+
|
|
142
|
+
def register(self, name: str, value):
|
|
143
|
+
"""Expose a Python value or callable to scripts.
|
|
144
|
+
|
|
145
|
+
Returns the runtime so calls can be chained.
|
|
146
|
+
"""
|
|
147
|
+
obj = _wrap(value)
|
|
148
|
+
self.globals[name] = obj
|
|
149
|
+
self.__interpreter__.set_global(name, obj)
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def register_many(self, mapping: dict):
|
|
153
|
+
for name, value in mapping.items():
|
|
154
|
+
self.register(name, value)
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def __setitem__(self, name: str, value):
|
|
158
|
+
self.register(name, value)
|
|
159
|
+
|
|
160
|
+
def __getitem__(self, name: str):
|
|
161
|
+
return self.__interpreter__.get_global(name).value
|
|
162
|
+
|
|
163
|
+
def add_import_path(self, path: str):
|
|
164
|
+
"""Add a directory that ``import`` searches after the base directory.
|
|
165
|
+
|
|
166
|
+
The base directory is the one of the file that contains the ``import``
|
|
167
|
+
(or the current working directory for ``execute``). Added paths are
|
|
168
|
+
searched in the order they were registered, so a module next to the
|
|
169
|
+
importer always wins. Returns the runtime so calls can be chained.
|
|
170
|
+
"""
|
|
171
|
+
real = os.path.realpath(path)
|
|
172
|
+
if not os.path.isdir(real):
|
|
173
|
+
raise Errors.RuntimeError(
|
|
174
|
+
f"import path '{path}' is not a directory")
|
|
175
|
+
if real not in self._import_paths:
|
|
176
|
+
self._import_paths.append(real)
|
|
177
|
+
return self
|
|
178
|
+
|
|
179
|
+
def execute(self, code: str):
|
|
180
|
+
"""Parse and run ``code``.
|
|
181
|
+
|
|
182
|
+
Returns the value of the last evaluated expression, or the value of a
|
|
183
|
+
top-level ``return`` if the script uses one.
|
|
184
|
+
|
|
185
|
+
``import "name"`` inside the script resolves against the current
|
|
186
|
+
working directory of the Python process, then against the directories
|
|
187
|
+
added with :meth:`add_import_path`.
|
|
188
|
+
|
|
189
|
+
The compiled form of a source is cached, so running the same source
|
|
190
|
+
again skips parsing and compiling it.
|
|
191
|
+
|
|
192
|
+
Raises ``Errors.SyntaxError`` if the code cannot be parsed.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
return self.__interpreter__.run_chunk(self._chunk(code))
|
|
196
|
+
except ThrownSignal as signal:
|
|
197
|
+
raise self.__interpreter__._uncaught(signal) from None
|
|
198
|
+
|
|
199
|
+
def _chunk(self, code: str):
|
|
200
|
+
"""The compiled chunk of ``code``, compiled once per source."""
|
|
201
|
+
chunk = self._chunks.get(code)
|
|
202
|
+
if chunk is not None:
|
|
203
|
+
return chunk
|
|
204
|
+
|
|
205
|
+
chunk = self.__interpreter__.compiler.compile_chunk(self._compile(code))
|
|
206
|
+
if len(self._chunks) >= _CHUNK_CACHE:
|
|
207
|
+
# bounded, so a host that runs many different scripts does not
|
|
208
|
+
# grow the cache without limit
|
|
209
|
+
del self._chunks[next(iter(self._chunks))]
|
|
210
|
+
self._chunks[code] = chunk
|
|
211
|
+
return chunk
|
|
212
|
+
|
|
213
|
+
def execute_module(self, path: str):
|
|
214
|
+
"""Execute ``path`` as a Koskript module and return the module.
|
|
215
|
+
|
|
216
|
+
The file is resolved against the current working directory, and the
|
|
217
|
+
imports inside it resolve against the directory of the module file.
|
|
218
|
+
Modules are loaded once per runtime and cached; circular imports raise
|
|
219
|
+
``Errors.RuntimeError``.
|
|
220
|
+
"""
|
|
221
|
+
base_dir = os.path.dirname(self._loading[-1]) if self._loading else None
|
|
222
|
+
try:
|
|
223
|
+
return self._load_module(path, base_dir)
|
|
224
|
+
except ThrownSignal as signal:
|
|
225
|
+
raise self.__interpreter__._uncaught(signal) from None
|
|
226
|
+
|
|
227
|
+
def _transform(self, tree):
|
|
228
|
+
"""Run the AST transformer, unwrapping clean syntax errors."""
|
|
229
|
+
from lark.exceptions import VisitError
|
|
230
|
+
|
|
231
|
+
transformer = self.__ast__
|
|
232
|
+
try:
|
|
233
|
+
return transformer.transform(tree)
|
|
234
|
+
except VisitError as e:
|
|
235
|
+
if isinstance(e.orig_exc, Errors.SyntaxError):
|
|
236
|
+
raise e.orig_exc from None
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
def _compile(self, code: str):
|
|
240
|
+
"""Parse ``code`` into the list of statements the compiler consumes."""
|
|
241
|
+
if self.native_parser_experiment:
|
|
242
|
+
return parse(code)
|
|
243
|
+
|
|
244
|
+
from lark.exceptions import LarkError
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
tree = grammar.parse(code)
|
|
248
|
+
except LarkError as e:
|
|
249
|
+
raise Errors.SyntaxError(_format_syntax_error(code, e)) from e
|
|
250
|
+
ast = self._transform(tree)
|
|
251
|
+
|
|
252
|
+
if not isinstance(ast, list):
|
|
253
|
+
ast = [ast]
|
|
254
|
+
return ast
|
|
255
|
+
|
|
256
|
+
def _module_candidates(self, path: str, base_dir: str) -> list:
|
|
257
|
+
"""Absolute paths tried for ``import "path"``, in search order."""
|
|
258
|
+
bases = [None] if os.path.isabs(path) else [base_dir, *self._import_paths]
|
|
259
|
+
candidates = []
|
|
260
|
+
for base in bases:
|
|
261
|
+
candidate = path if base is None else os.path.join(base, path)
|
|
262
|
+
if not candidate.lower().endswith(".kos") \
|
|
263
|
+
and not os.path.isfile(candidate):
|
|
264
|
+
candidate += ".kos"
|
|
265
|
+
candidates.append(os.path.realpath(candidate))
|
|
266
|
+
return candidates
|
|
267
|
+
|
|
268
|
+
def _load_module(self, path: str, base_dir: str = None):
|
|
269
|
+
if base_dir is None:
|
|
270
|
+
base_dir = os.getcwd()
|
|
271
|
+
|
|
272
|
+
candidates = self._module_candidates(path, base_dir)
|
|
273
|
+
real = next((item for item in candidates if os.path.isfile(item)), None)
|
|
274
|
+
if real is None:
|
|
275
|
+
looked = ", ".join(f"'{item}'" for item in candidates)
|
|
276
|
+
raise Errors.RuntimeError(
|
|
277
|
+
f"module '{path}' not found (looked for {looked})")
|
|
278
|
+
|
|
279
|
+
cached = self._modules.get(real)
|
|
280
|
+
if cached is not None:
|
|
281
|
+
return cached
|
|
282
|
+
|
|
283
|
+
if real in self._loading:
|
|
284
|
+
raise Errors.RuntimeError(
|
|
285
|
+
f"circular import of module '{path}'")
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
with open(real, "r", encoding="utf-8-sig") as handle:
|
|
289
|
+
code = handle.read()
|
|
290
|
+
except OSError as e:
|
|
291
|
+
raise Errors.RuntimeError(
|
|
292
|
+
f"cannot read module '{path}': {e}") from e
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
ast = self._compile(code)
|
|
296
|
+
except Errors.SyntaxError as e:
|
|
297
|
+
raise Errors.SyntaxError(f"module '{path}':\n{e}") from None
|
|
298
|
+
|
|
299
|
+
interpreter = self.__interpreter__
|
|
300
|
+
info = ScopeInfo(interpreter.root_info)
|
|
301
|
+
scope = Scope(interpreter.root, info)
|
|
302
|
+
name = os.path.splitext(os.path.basename(real))[0]
|
|
303
|
+
|
|
304
|
+
self._loading.append(real)
|
|
305
|
+
try:
|
|
306
|
+
interpreter.run_in(ast, scope, info,
|
|
307
|
+
base_dir=os.path.dirname(real))
|
|
308
|
+
finally:
|
|
309
|
+
self._loading.pop()
|
|
310
|
+
|
|
311
|
+
module = Module(name, scope)
|
|
312
|
+
self._modules[real] = module
|
|
313
|
+
return module
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def run(code: str, globals_map: dict = None, stdlib: bool = True, **kwargs):
|
|
317
|
+
"""One-shot convenience helper.
|
|
318
|
+
|
|
319
|
+
>>> run("return 1 + 2")
|
|
320
|
+
3
|
|
321
|
+
"""
|
|
322
|
+
merged = dict(globals_map or {})
|
|
323
|
+
merged.update(kwargs)
|
|
324
|
+
return KoskriptRuntime(merged, stdlib=stdlib).execute(code)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
__all__ = ["KoskriptRuntime", "KoskriptObject", "Module", "Namespace",
|
|
328
|
+
"Errors", "run", "build_globals"]
|