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.
Files changed (36) hide show
  1. {koskript-1.4.0 → koskript-1.4.2}/PKG-INFO +16 -8
  2. {koskript-1.4.0 → koskript-1.4.2}/README.md +95 -87
  3. {koskript-1.4.0 → koskript-1.4.2}/pyproject.toml +4 -1
  4. koskript-1.4.2/src/koskript/__init__.py +328 -0
  5. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/grammar.lark +54 -3
  6. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/lang/astgen.py +152 -5
  7. koskript-1.4.2/src/koskript/lang/compiler.py +1004 -0
  8. koskript-1.4.2/src/koskript/lang/emtypes.py +513 -0
  9. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/lang/errors.py +12 -1
  10. koskript-1.4.2/src/koskript/lang/interpreter.py +679 -0
  11. koskript-1.4.2/src/koskript/lang/lexer.py +352 -0
  12. koskript-1.4.2/src/koskript/lang/parser.py +676 -0
  13. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/__init__.py +2 -1
  14. koskript-1.4.2/src/koskript/stdlib/byteslib.py +115 -0
  15. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/core.py +18 -2
  16. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/helpers.py +83 -73
  17. {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/PKG-INFO +16 -8
  18. {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/SOURCES.txt +8 -0
  19. koskript-1.4.2/tests/test_grammar.py +700 -0
  20. koskript-1.4.2/tests/test_language.py +980 -0
  21. koskript-1.4.2/tests/test_modules.py +421 -0
  22. koskript-1.4.2/tests/test_parser.py +667 -0
  23. {koskript-1.4.0 → koskript-1.4.2}/tests/test_stdlib.py +95 -0
  24. koskript-1.4.0/src/koskript/__init__.py +0 -117
  25. koskript-1.4.0/src/koskript/lang/emtypes.py +0 -215
  26. koskript-1.4.0/src/koskript/lang/interpreter.py +0 -742
  27. {koskript-1.4.0 → koskript-1.4.2}/LICENSE +0 -0
  28. {koskript-1.4.0 → koskript-1.4.2}/setup.cfg +0 -0
  29. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/arrays.py +0 -0
  30. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/jsonlib.py +0 -0
  31. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/maps.py +0 -0
  32. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/mathlib.py +0 -0
  33. {koskript-1.4.0 → koskript-1.4.2}/src/koskript/stdlib/strings.py +0 -0
  34. {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/dependency_links.txt +0 -0
  35. {koskript-1.4.0 → koskript-1.4.2}/src/koskript.egg-info/requires.txt +0 -0
  36. {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.0
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. Module imports, error handling and
418
- > performance improvements are on the way; expect the language and API to change
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
- ![Koskript](https://raw.githubusercontent.com/alesisce/koskript/refs/heads/main/banner.png)
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
- ```python
11
- from koskript import KoskriptRuntime
12
-
13
- runtime = KoskriptRuntime()
14
- runtime.execute("""
15
- const names = ["Ana", "Luis"]
16
- array.each(names, (name) { print("hello " + name) })
17
- """)
18
- ```
19
-
20
- > **NOTE:** Koskript is in early development. Module imports, error handling and
21
- > performance improvements are on the way; expect the language and API to change
22
- > between releases.
23
-
24
- ---
25
-
26
- ## Features
27
-
28
- - Dynamic typing.
29
- - Block-level lexical scoping with `local` declarations.
30
- - Closures that capture their defining scope, including `this` inside methods.
31
- - Constants with `const`.
32
- - Classes with inheritance, visibility, static methods and constructors.
33
- - Standard library: core builtins plus `map`, `array`, `string`, `math` and `json`.
34
- - Native Python interop for functions, objects and classes.
35
- - `if`, `elseif`, `else`.
36
- - `while`, `for`, `foreach` loops with `break` / `continue`.
37
- - Member access and index access.
38
- - First-class functions and lambda expressions.
39
- - Arithmetic, comparison and logical operators.
40
- - Embeddable in any Python application.
41
-
42
- ---
43
-
44
- ## Installation
45
-
46
- ```bash
47
- pip install -U koskript
48
- ```
49
-
50
- Or clone the repository and import it directly:
51
-
52
- ```bash
53
- git clone https://github.com/alesis-buzz/koskript.git
54
- ```
55
-
56
- Koskript requires Python 3.10+ and [Lark](https://github.com/lark-parser/lark).
57
-
58
- ---
59
-
60
- ## Documentation
61
-
62
- | Document | What it covers |
63
- |---|---|
64
- | [Getting started](docs/getting-started.md) | Installation, first script, a complete example |
65
- | [Language reference](docs/language.md) | Types, variables, operators, control flow, functions, closures, loops, strings |
66
- | [Classes](docs/classes.md) | Inheritance, visibility, static members, constructors, `super` |
67
- | [Standard library](docs/standard-library.md) | Core builtins and the `map`, `array`, `string`, `math` and `json` namespaces |
68
- | [Python interop](docs/python-interop.md) | Exposing Python values, callables and classes to scripts |
69
- | [Embedding guide](docs/embedding.md) | `KoskriptRuntime`, `run()`, error handling and stdlib activation |
70
- | [Roadmap](docs/roadmap.md) | What is done, what is planned, known limitations |
71
-
72
- ---
73
-
74
- ## License
75
-
76
- Koskript is licensed under the Mozilla Public License 2.0 (MPL-2.0). See
77
- [LICENSE](LICENSE) for the full text.
78
-
79
- You are free to use, modify, and redistribute Koskript. However, you may not
80
- redistribute this project under a different name or claim authorship of the
81
- Koskript language.
82
-
83
- **Koskript is a trademark of Alesis.**
84
-
85
- ---
86
-
87
- *Built with ❤️ by Alesis*
1
+ ![Koskript](https://raw.githubusercontent.com/alesisce/koskript/refs/heads/main/banner.png)
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.0"
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"]