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.
Files changed (33) hide show
  1. {koskript-1.4.1 → koskript-1.4.2}/PKG-INFO +7 -5
  2. {koskript-1.4.1 → koskript-1.4.2}/README.md +5 -3
  3. {koskript-1.4.1 → koskript-1.4.2}/pyproject.toml +4 -1
  4. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/__init__.py +125 -20
  5. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/grammar.lark +4 -0
  6. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/astgen.py +45 -0
  7. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/compiler.py +143 -38
  8. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/emtypes.py +12 -8
  9. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/interpreter.py +107 -65
  10. koskript-1.4.2/src/koskript/lang/lexer.py +352 -0
  11. koskript-1.4.2/src/koskript/lang/parser.py +676 -0
  12. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/__init__.py +2 -1
  13. koskript-1.4.2/src/koskript/stdlib/byteslib.py +115 -0
  14. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/core.py +15 -2
  15. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/helpers.py +2 -0
  16. {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/PKG-INFO +7 -5
  17. {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/SOURCES.txt +5 -0
  18. koskript-1.4.2/tests/test_grammar.py +700 -0
  19. {koskript-1.4.1 → koskript-1.4.2}/tests/test_language.py +51 -2
  20. koskript-1.4.2/tests/test_parser.py +667 -0
  21. {koskript-1.4.1 → koskript-1.4.2}/tests/test_stdlib.py +95 -0
  22. {koskript-1.4.1 → koskript-1.4.2}/LICENSE +0 -0
  23. {koskript-1.4.1 → koskript-1.4.2}/setup.cfg +0 -0
  24. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/lang/errors.py +0 -0
  25. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/arrays.py +0 -0
  26. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/jsonlib.py +0 -0
  27. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/maps.py +0 -0
  28. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/mathlib.py +0 -0
  29. {koskript-1.4.1 → koskript-1.4.2}/src/koskript/stdlib/strings.py +0 -0
  30. {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/dependency_links.txt +0 -0
  31. {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/requires.txt +0 -0
  32. {koskript-1.4.1 → koskript-1.4.2}/src/koskript.egg-info/top_level.txt +0 -0
  33. {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.1
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.1"
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
- from lark import Lark
2
- from lark.exceptions import LarkError, UnexpectedInput
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.astgen import KoskriptTransformer
6
+ from .lang.parser import parse
7
7
  from .lang.errors import Errors
8
8
  from .stdlib import build_globals
9
- import pathlib, os
9
+ import os
10
10
 
11
- _grammar_path = os.path.join(pathlib.Path(__file__).resolve().parent, "grammar.lark")
12
- with open(_grammar_path, "r") as _grammar_file:
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 _format_syntax_error(code: str, error: LarkError) -> str:
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.__ast__ = KoskriptTransformer()
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
- return self.__interpreter__.run(self._compile(code))
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.__ast__.transform(tree)
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
- tree = grammar.parse(code)
186
- except LarkError as e:
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)