koskript 1.4.1__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.1 → koskript-1.4.2}/PKG-INFO +7 -5
- {koskript-1.4.1 → koskript-1.4.2}/README.md +5 -3
- {koskript-1.4.1 → koskript-1.4.2}/pyproject.toml +4 -1
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/__init__.py +125 -20
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/grammar.lark +4 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/astgen.py +45 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/compiler.py +143 -38
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/emtypes.py +12 -8
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/interpreter.py +107 -65
- 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.1 → 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.1 → koskript-1.4.2}/src/koskript/stdlib/core.py +15 -2
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/helpers.py +2 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/PKG-INFO +7 -5
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/SOURCES.txt +5 -0
- koskript-1.4.2/tests/test_grammar.py +700 -0
- {koskript-1.4.1 → koskript-1.4.2}/tests/test_language.py +51 -2
- koskript-1.4.2/tests/test_parser.py +667 -0
- {koskript-1.4.1 → koskript-1.4.2}/tests/test_stdlib.py +95 -0
- {koskript-1.4.1 → koskript-1.4.2}/LICENSE +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/setup.cfg +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/errors.py +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/arrays.py +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/jsonlib.py +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/maps.py +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/mathlib.py +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/strings.py +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/dependency_links.txt +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/requires.txt +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/top_level.txt +0 -0
- {koskript-1.4.1 → koskript-1.4.2}/tests/test_modules.py +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
|
|
@@ -429,7 +429,7 @@ array.each(names, (name) { print("hello " + name) })
|
|
|
429
429
|
- Closures that capture their defining scope, including `this` inside methods.
|
|
430
430
|
- Constants with `const`.
|
|
431
431
|
- Classes with inheritance, visibility, static methods and constructors.
|
|
432
|
-
- 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
433
|
- Module imports with per-module resolution (`import "utils"`).
|
|
434
434
|
- Configurable import search paths (`runtime.add_import_path`).
|
|
435
435
|
- Native Python interop for functions, objects and classes.
|
|
@@ -457,6 +457,8 @@ git clone https://github.com/alesis-buzz/koskript.git
|
|
|
457
457
|
```
|
|
458
458
|
|
|
459
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)`.
|
|
460
462
|
|
|
461
463
|
---
|
|
462
464
|
|
|
@@ -465,9 +467,9 @@ Koskript requires Python 3.10+ and [Lark](https://github.com/lark-parser/lark).
|
|
|
465
467
|
| Document | What it covers |
|
|
466
468
|
|---|---|
|
|
467
469
|
| [Getting started](docs/getting-started.md) | Installation, first script, a complete example |
|
|
468
|
-
| [Language reference](docs/language.md) | Types, variables, operators, control flow, errors, functions, wrappers, namespaces, closures, modules, loops, strings |
|
|
470
|
+
| [Language reference](docs/language.md) | Types, variables, operators, control flow, errors, functions, wrappers, namespaces, closures, modules, loops, strings, bytes |
|
|
469
471
|
| [Classes](docs/classes.md) | Inheritance, visibility, static members, constructors, `super` |
|
|
470
|
-
| [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 |
|
|
471
473
|
| [Python interop](docs/python-interop.md) | Exposing Python values, callables and classes to scripts |
|
|
472
474
|
| [Embedding guide](docs/embedding.md) | `KoskriptRuntime`, `run()`, error handling and stdlib activation |
|
|
473
475
|
| [Roadmap](docs/roadmap.md) | What is done, what is planned, known limitations |
|
|
@@ -32,7 +32,7 @@ array.each(names, (name) { print("hello " + name) })
|
|
|
32
32
|
- Closures that capture their defining scope, including `this` inside methods.
|
|
33
33
|
- Constants with `const`.
|
|
34
34
|
- Classes with inheritance, visibility, static methods and constructors.
|
|
35
|
-
- Standard library: core builtins plus `map`, `array`, `string`, `math` and `json`.
|
|
35
|
+
- Standard library: core builtins plus `map`, `array`, `string`, `bytes`, `math` and `json`.
|
|
36
36
|
- Module imports with per-module resolution (`import "utils"`).
|
|
37
37
|
- Configurable import search paths (`runtime.add_import_path`).
|
|
38
38
|
- Native Python interop for functions, objects and classes.
|
|
@@ -60,6 +60,8 @@ git clone https://github.com/alesis-buzz/koskript.git
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
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)`.
|
|
63
65
|
|
|
64
66
|
---
|
|
65
67
|
|
|
@@ -68,9 +70,9 @@ Koskript requires Python 3.10+ and [Lark](https://github.com/lark-parser/lark).
|
|
|
68
70
|
| Document | What it covers |
|
|
69
71
|
|---|---|
|
|
70
72
|
| [Getting started](docs/getting-started.md) | Installation, first script, a complete example |
|
|
71
|
-
| [Language reference](docs/language.md) | Types, variables, operators, control flow, errors, functions, wrappers, namespaces, closures, modules, loops, strings |
|
|
73
|
+
| [Language reference](docs/language.md) | Types, variables, operators, control flow, errors, functions, wrappers, namespaces, closures, modules, loops, strings, bytes |
|
|
72
74
|
| [Classes](docs/classes.md) | Inheritance, visibility, static members, constructors, `super` |
|
|
73
|
-
| [Standard library](docs/standard-library.md) | Core builtins and the `map`, `array`, `string`, `math` and `json` namespaces |
|
|
75
|
+
| [Standard library](docs/standard-library.md) | Core builtins and the `map`, `array`, `string`, `bytes`, `math` and `json` namespaces |
|
|
74
76
|
| [Python interop](docs/python-interop.md) | Exposing Python values, callables and classes to scripts |
|
|
75
77
|
| [Embedding guide](docs/embedding.md) | `KoskriptRuntime`, `run()`, error handling and stdlib activation |
|
|
76
78
|
| [Roadmap](docs/roadmap.md) | What is done, what is planned, known limitations |
|
|
@@ -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
|
|
|
@@ -1,23 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
3
|
from .lang.emtypes import (KoskriptObject, Module, Namespace, Scope, ScopeInfo,
|
|
4
4
|
ThrownSignal)
|
|
5
5
|
from .lang.interpreter import KoskriptInterpreter
|
|
6
|
-
from .lang.
|
|
6
|
+
from .lang.parser import parse
|
|
7
7
|
from .lang.errors import Errors
|
|
8
8
|
from .stdlib import build_globals
|
|
9
|
-
import
|
|
9
|
+
import os
|
|
10
10
|
|
|
11
|
-
_grammar_path = os.path.join(pathlib.Path(__file__).resolve().parent,
|
|
12
|
-
|
|
13
|
-
grammar = Lark(_grammar_file, parser="lalr", maybe_placeholders=False)
|
|
11
|
+
_grammar_path = os.path.join(pathlib.Path(__file__).resolve().parent,
|
|
12
|
+
"grammar.lark")
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def _wrap(value):
|
|
17
16
|
return value if isinstance(value, KoskriptObject) else KoskriptObject(value=value)
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
def
|
|
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
|
+
|
|
21
63
|
if not isinstance(error, UnexpectedInput) or error.line is None:
|
|
22
64
|
return str(error)
|
|
23
65
|
|
|
@@ -44,22 +86,35 @@ class KoskriptRuntime(object):
|
|
|
44
86
|
Any Python value or callable exposed to scripts is wrapped automatically,
|
|
45
87
|
so you can pass plain functions without building ``KoskriptObject`` by hand.
|
|
46
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
|
+
|
|
47
95
|
>>> rt = KoskriptRuntime({"print": print})
|
|
48
96
|
>>> rt.execute("local x = 10\\nx + 26")
|
|
49
97
|
36
|
|
50
98
|
"""
|
|
51
99
|
|
|
52
|
-
def __init__(self, globals_map=None, _globals_=None, stdlib=True
|
|
100
|
+
def __init__(self, globals_map=None, _globals_=None, stdlib=True,
|
|
101
|
+
native_parser_experiment=False):
|
|
53
102
|
if _globals_ is not None:
|
|
54
103
|
globals_map = {**(globals_map or {}), **_globals_}
|
|
55
104
|
|
|
56
105
|
self.globals = {}
|
|
106
|
+
self.native_parser_experiment = bool(native_parser_experiment)
|
|
57
107
|
self.__interpreter__ = KoskriptInterpreter()
|
|
58
|
-
self.
|
|
108
|
+
self._ast = None
|
|
59
109
|
self.__interpreter__.module_loader = self._load_module
|
|
60
110
|
self._modules = {}
|
|
61
111
|
self._loading = []
|
|
62
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()
|
|
63
118
|
|
|
64
119
|
if stdlib:
|
|
65
120
|
self.register_many(build_globals(self.__interpreter__.call_value))
|
|
@@ -67,6 +122,23 @@ class KoskriptRuntime(object):
|
|
|
67
122
|
if globals_map:
|
|
68
123
|
self.register_many(globals_map)
|
|
69
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
|
+
|
|
70
142
|
def register(self, name: str, value):
|
|
71
143
|
"""Expose a Python value or callable to scripts.
|
|
72
144
|
|
|
@@ -114,9 +186,29 @@ class KoskriptRuntime(object):
|
|
|
114
186
|
working directory of the Python process, then against the directories
|
|
115
187
|
added with :meth:`add_import_path`.
|
|
116
188
|
|
|
189
|
+
The compiled form of a source is cached, so running the same source
|
|
190
|
+
again skips parsing and compiling it.
|
|
191
|
+
|
|
117
192
|
Raises ``Errors.SyntaxError`` if the code cannot be parsed.
|
|
118
193
|
"""
|
|
119
|
-
|
|
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
|
|
120
212
|
|
|
121
213
|
def execute_module(self, path: str):
|
|
122
214
|
"""Execute ``path`` as a Koskript module and return the module.
|
|
@@ -132,12 +224,30 @@ class KoskriptRuntime(object):
|
|
|
132
224
|
except ThrownSignal as signal:
|
|
133
225
|
raise self.__interpreter__._uncaught(signal) from None
|
|
134
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
|
+
|
|
135
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
|
+
|
|
136
246
|
try:
|
|
137
247
|
tree = grammar.parse(code)
|
|
138
248
|
except LarkError as e:
|
|
139
249
|
raise Errors.SyntaxError(_format_syntax_error(code, e)) from e
|
|
140
|
-
ast = self.
|
|
250
|
+
ast = self._transform(tree)
|
|
141
251
|
|
|
142
252
|
if not isinstance(ast, list):
|
|
143
253
|
ast = [ast]
|
|
@@ -182,14 +292,9 @@ class KoskriptRuntime(object):
|
|
|
182
292
|
f"cannot read module '{path}': {e}") from e
|
|
183
293
|
|
|
184
294
|
try:
|
|
185
|
-
|
|
186
|
-
except
|
|
187
|
-
raise Errors.SyntaxError(
|
|
188
|
-
f"module '{path}':\n{_format_syntax_error(code, e)}") from e
|
|
189
|
-
|
|
190
|
-
ast = self.__ast__.transform(tree)
|
|
191
|
-
if not isinstance(ast, list):
|
|
192
|
-
ast = [ast]
|
|
295
|
+
ast = self._compile(code)
|
|
296
|
+
except Errors.SyntaxError as e:
|
|
297
|
+
raise Errors.SyntaxError(f"module '{path}':\n{e}") from None
|
|
193
298
|
|
|
194
299
|
interpreter = self.__interpreter__
|
|
195
300
|
info = ScopeInfo(interpreter.root_info)
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
NAME : /(?!(?:if|elseif|else|while|for|foreach|fn|wrapper|return|local|const|true|false|null|and|or|not|in|break|continue|class|extends|new|static|public|private|this|super|constructor|import|as|error|throw|try|catch|finally|namespace)\b)[a-zA-Z_][a-zA-Z0-9_]*/
|
|
4
4
|
NUMBER : /[0-9]+(\.[0-9]+)?/
|
|
5
5
|
STRING : /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/
|
|
6
|
+
// Higher priority than NAME so `b"..."` lexes as a single token instead
|
|
7
|
+
// of NAME + STRING.
|
|
8
|
+
BYTES.2 : /[bB]"(?:\\.|[^"\\])*"|[bB]'(?:\\.|[^'\\])*'/
|
|
6
9
|
COMMENT : /\/\/[^\n]*/
|
|
7
10
|
// Matches a bare `return` at the end of a line (or before a comment/EOF).
|
|
8
11
|
// Higher priority so it wins over the `return` keyword on equal length.
|
|
@@ -188,6 +191,7 @@ block : "{" statement* "}"
|
|
|
188
191
|
|
|
189
192
|
?atom : NUMBER
|
|
190
193
|
| STRING
|
|
194
|
+
| BYTES
|
|
191
195
|
| array
|
|
192
196
|
| map
|
|
193
197
|
| bool
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from lark import Transformer
|
|
2
2
|
from .emtypes import *
|
|
3
|
+
from .errors import Errors
|
|
3
4
|
|
|
4
5
|
_ESCAPES = {
|
|
5
6
|
"n": "\n",
|
|
@@ -11,6 +12,8 @@ _ESCAPES = {
|
|
|
11
12
|
"'": "'",
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
_HEX_DIGITS = "0123456789abcdefABCDEF"
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
def _unescape(raw: str) -> str:
|
|
16
19
|
out = []
|
|
@@ -27,6 +30,37 @@ def _unescape(raw: str) -> str:
|
|
|
27
30
|
return "".join(out)
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
def _unescape_bytes(raw: str) -> bytes:
|
|
34
|
+
"""Turn the body of a ``b"..."`` literal into bytes.
|
|
35
|
+
|
|
36
|
+
Supports the same escapes as strings plus ``\\xNN`` for arbitrary byte
|
|
37
|
+
values. Characters outside ASCII are encoded as UTF-8.
|
|
38
|
+
"""
|
|
39
|
+
out = bytearray()
|
|
40
|
+
i = 0
|
|
41
|
+
length = len(raw)
|
|
42
|
+
while i < length:
|
|
43
|
+
ch = raw[i]
|
|
44
|
+
if ch == "\\" and i + 1 < length:
|
|
45
|
+
nxt = raw[i + 1]
|
|
46
|
+
if nxt == "x":
|
|
47
|
+
digits = raw[i + 2:i + 4]
|
|
48
|
+
if len(digits) != 2 \
|
|
49
|
+
or digits[0] not in _HEX_DIGITS \
|
|
50
|
+
or digits[1] not in _HEX_DIGITS:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"invalid bytes escape '\\x{digits}', expected \\xNN")
|
|
53
|
+
out.append(int(digits, 16))
|
|
54
|
+
i += 4
|
|
55
|
+
continue
|
|
56
|
+
out.extend(_ESCAPES.get(nxt, nxt).encode("utf-8"))
|
|
57
|
+
i += 2
|
|
58
|
+
else:
|
|
59
|
+
out.extend(ch.encode("utf-8"))
|
|
60
|
+
i += 1
|
|
61
|
+
return bytes(out)
|
|
62
|
+
|
|
63
|
+
|
|
30
64
|
def _qualified_name(node) -> str:
|
|
31
65
|
"""Display name for a simple or dotted reference node."""
|
|
32
66
|
if type(node) is NameRef:
|
|
@@ -47,6 +81,17 @@ class KoskriptTransformer(Transformer):
|
|
|
47
81
|
|
|
48
82
|
def NAME(self, token): return NameRef(name=str(token))
|
|
49
83
|
def STRING(self, token): return StrLit(value=_unescape(str(token)[1:-1]))
|
|
84
|
+
|
|
85
|
+
def BYTES(self, token):
|
|
86
|
+
text = str(token)
|
|
87
|
+
try:
|
|
88
|
+
value = _unescape_bytes(text[2:-1])
|
|
89
|
+
except ValueError as exc:
|
|
90
|
+
raise Errors.SyntaxError(
|
|
91
|
+
f"Syntax error at line {token.line}, "
|
|
92
|
+
f"column {token.column}: {exc}") from None
|
|
93
|
+
return BytesLit(value=value)
|
|
94
|
+
|
|
50
95
|
def bool_true(self, tree): return BoolLit(value=True)
|
|
51
96
|
def bool_false(self, tree): return BoolLit(value=False)
|
|
52
97
|
def null_lit(self, tree): return NullLit(value=None)
|