bxengine 0.1.0__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. bxengine-0.1.0/PKG-INFO +46 -0
  2. bxengine-0.1.0/README.md +30 -0
  3. bxengine-0.1.0/pyproject.toml +44 -0
  4. bxengine-0.1.0/setup.cfg +4 -0
  5. bxengine-0.1.0/src/bxengine/__about__.py +1 -0
  6. bxengine-0.1.0/src/bxengine/__init__.py +3 -0
  7. bxengine-0.1.0/src/bxengine/__main__.py +4 -0
  8. bxengine-0.1.0/src/bxengine/constants.py +2 -0
  9. bxengine-0.1.0/src/bxengine/exceptions.py +20 -0
  10. bxengine-0.1.0/src/bxengine/module.py +72 -0
  11. bxengine-0.1.0/src/bxengine/parsing/__init__.py +0 -0
  12. bxengine-0.1.0/src/bxengine/parsing/nodes.py +40 -0
  13. bxengine-0.1.0/src/bxengine/parsing/parser.py +204 -0
  14. bxengine-0.1.0/src/bxengine/runtime/__init__.py +4 -0
  15. bxengine-0.1.0/src/bxengine/runtime/context.py +15 -0
  16. bxengine-0.1.0/src/bxengine/runtime/executor.py +315 -0
  17. bxengine-0.1.0/src/bxengine/runtime/extensions/BxeExtension.py +72 -0
  18. bxengine-0.1.0/src/bxengine/runtime/extensions/__init__.py +15 -0
  19. bxengine-0.1.0/src/bxengine/runtime/extensions/builtin.py +586 -0
  20. bxengine-0.1.0/src/bxengine/runtime/extensions/discord_stub.py +25 -0
  21. bxengine-0.1.0/src/bxengine/spans.py +136 -0
  22. bxengine-0.1.0/src/bxengine/tokenizer/__init__.py +0 -0
  23. bxengine-0.1.0/src/bxengine/tokenizer/stringreader.py +163 -0
  24. bxengine-0.1.0/src/bxengine/tokenizer/tokenize.py +188 -0
  25. bxengine-0.1.0/src/bxengine/tokenizer/tokens.py +45 -0
  26. bxengine-0.1.0/src/bxengine.egg-info/PKG-INFO +46 -0
  27. bxengine-0.1.0/src/bxengine.egg-info/SOURCES.txt +31 -0
  28. bxengine-0.1.0/src/bxengine.egg-info/dependency_links.txt +1 -0
  29. bxengine-0.1.0/src/bxengine.egg-info/entry_points.txt +2 -0
  30. bxengine-0.1.0/src/bxengine.egg-info/top_level.txt +1 -0
  31. bxengine-0.1.0/tests/test_parser.py +87 -0
  32. bxengine-0.1.0/tests/test_runtime.py +461 -0
  33. bxengine-0.1.0/tests/test_tokenizer.py +96 -0
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: bxengine
3
+ Version: 0.1.0
4
+ Summary: B++ runtime engine
5
+ Classifier: Development Status :: 3 - Alpha
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Software Development :: Interpreters
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+
17
+ # bxengine
18
+
19
+ `bxengine` is a Python implementation of the B++ runtime engine.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install bxengine
25
+ ```
26
+
27
+ ## CLI
28
+
29
+ Run a file:
30
+
31
+ ```bash
32
+ bxengine path/to/program.bx
33
+ ```
34
+
35
+ Run inline code:
36
+
37
+ ```bash
38
+ bxengine -e "[CONCAT \"hello\" \" world\"]"
39
+ ```
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ uv sync --dev
45
+ pytest
46
+ ```
@@ -0,0 +1,30 @@
1
+ # bxengine
2
+
3
+ `bxengine` is a Python implementation of the B++ runtime engine.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install bxengine
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ Run a file:
14
+
15
+ ```bash
16
+ bxengine path/to/program.bx
17
+ ```
18
+
19
+ Run inline code:
20
+
21
+ ```bash
22
+ bxengine -e "[CONCAT \"hello\" \" world\"]"
23
+ ```
24
+
25
+ ## Development
26
+
27
+ ```bash
28
+ uv sync --dev
29
+ pytest
30
+ ```
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bxengine"
7
+ dynamic = ["version"]
8
+ description = "B++ runtime engine"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Interpreters",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [project.scripts]
26
+ bxengine = "bxengine.module:_module_main"
27
+
28
+ [tool.setuptools]
29
+ package-dir = {"" = "src"}
30
+ include-package-data = false
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+ include = ["bxengine*"]
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = {attr = "bxengine.__version__"}
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "build>=1.2.2",
42
+ "pytest>=9.0.3",
43
+ "twine>=6.1.0",
44
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .__about__ import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from bxengine.module import _module_main
2
+
3
+ if __name__ == "__main__":
4
+ _module_main()
@@ -0,0 +1,2 @@
1
+
2
+ ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from bxengine.spans import SpanData
4
+
5
+
6
+ class BxeRuntimeException(Exception):
7
+ def __init__(self, message: str = "", span: SpanData | None = None):
8
+ super().__init__(message)
9
+ self.span = span
10
+
11
+ class BxeSyntaxException(Exception):
12
+ pass
13
+
14
+ class BxeUnclosedStringException(BxeSyntaxException):
15
+ def __init__(self, position: int = -1):
16
+ super().__init__("Unclosed string")
17
+ self.position = position
18
+
19
+ class BxeRuntimeSyntaxException(BxeRuntimeException):
20
+ pass
@@ -0,0 +1,72 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from bxengine.runtime.extensions.BxeExtension import GlobalVariableBppExtension
5
+ from bxengine.runtime.extensions.discord_stub import DiscordStubExtension
6
+ from bxengine.tokenizer.tokenize import Tokenizer, TokenizationResult
7
+ from bxengine.parsing.parser import Parser, ParsingResult
8
+ from bxengine.runtime.executor import Executor, ExecutorResult
9
+ from bxengine.runtime.extensions.builtin import BuiltinExtension
10
+
11
+
12
+ DEFAULT_TEST = """
13
+ [GLOBAL DEFINE descentNumber 100]
14
+ [GLOBAL DEFINE descentAttempts 0]
15
+ [GLOBAL DEFINE descentHighscore 15]
16
+
17
+ [DEFINE number [GLOBAL VAR descentNumber]]
18
+ [DEFINE attempts [GLOBAL VAR descentAttempts]]
19
+ [DEFINE nextNumber [RANDINT 0 [MATH [VAR number] + 1]]]
20
+ [IF [COMPARE [VAR nextNumber] != 0] [CONCAT "The number has gone from **" [VAR number] "** to **" [VAR nextNumber] "**. The current number of tries in this run is **" [MATH [VAR attempts] + 1] "**!
21
+ " [IF [COMPARE [GLOBAL VAR descentHighscore] < [MATH [VAR attempts] + 1]] [CONCAT "You've successfully beaten the highscore of **" [GLOBAL VAR descentHighscore] "**, but you're still going! Good luck!"] [CONCAT "The highscore to beat is **" [GLOBAL VAR descentHighscore] "**."]]] [CONCAT "Uh oh, looks like this run has finally come to an end! You had **" [MATH [VAR attempts] + 1] "** attempts.
22
+ " [IF [COMPARE [GLOBAL VAR descentHighscore] < [MATH [VAR atoh
23
+ yetempts] + 1]] [CONCAT "But I'm pleased to say you beat the highscore of **" [GLOBAL VAR descentHighscore] "** with your new score of **" [MATH [VAR attempts] + 1] "**! Well done!"] [CONCAT "Unfortunately, you failed to beat the highscore of **" [GLOBAL VAR descentHighscore] "**. I'm sure next attempt will be more promising, though."]]]]
24
+ [IF [COMPARE [VAR nextNumber] != 0] [CONCAT [GLOBAL DEFINE descentAttempts [MATH [VAR attempts] + 1]] [GLOBAL DEFINE descentNumber [VAR nextNumber]]] [CONCAT [IF [COMPARE [GLOBAL VAR descentHighscore] < [MATH [VAR attempts] + 1]] [GLOBAL DEFINE descentHighscore [MATH [VAR attempts] + 1]]] [GLOBAL DEFINE descentAttempts 0] [GLOBAL DEFINE descentNumber 100]]
25
+ """
26
+
27
+
28
+ def run_code(code: str, program_args: list[str] | None = None) -> None:
29
+ tokenizer_res = Tokenizer.tokenize(code)
30
+ if isinstance(tokenizer_res, TokenizationResult.Error):
31
+ print(tokenizer_res.message, "\n\n", tokenizer_res.range.debug_info(), sep="")
32
+ return
33
+
34
+ parser_res = Parser.parse(code, tokenizer_res.tokens)
35
+ if isinstance(parser_res, ParsingResult.Error):
36
+ print(parser_res.message, "\n\n", parser_res.range.debug_info(), sep="")
37
+ return
38
+
39
+ executor = Executor(
40
+ extensions=[BuiltinExtension()],
41
+ stateful_extensions=[GlobalVariableBppExtension, DiscordStubExtension],
42
+ program_args=program_args or [],
43
+ )
44
+ result = executor.execute(parser_res.nodes)
45
+
46
+ if isinstance(result, ExecutorResult.Error):
47
+ exception = result.exception
48
+ print(type(exception).__name__ + ":", exception, file=sys.stderr)
49
+ span = getattr(exception, "span", None)
50
+ if span is not None:
51
+ print("", file=sys.stderr)
52
+ print(span.debug_info(), file=sys.stderr)
53
+ sys.exit(1)
54
+ else:
55
+ print(result.output.strip(), end="\n")
56
+
57
+
58
+ def _module_main():
59
+ parser = argparse.ArgumentParser(prog="bxengine", description="B++ runtime engine")
60
+ parser.add_argument("file", nargs="?", help="Path to a .bx script file")
61
+ parser.add_argument("-e", "--eval", metavar="CODE", help="Execute a string of B++ code")
62
+ parser.add_argument("args", nargs="*", help="Arguments passed to the script (accessible via ARGS)")
63
+
64
+ parsed = parser.parse_args()
65
+
66
+ if parsed.eval:
67
+ run_code(parsed.eval, parsed.args)
68
+ elif parsed.file:
69
+ with open(parsed.file) as f:
70
+ run_code(f.read(), parsed.args)
71
+ else:
72
+ run_code(DEFAULT_TEST, parsed.args)
File without changes
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+ from bxengine.spans import SpanData
4
+
5
+ class Node:
6
+ pass
7
+
8
+ class Nodes:
9
+ @dataclass(frozen=True)
10
+ class Error(Node):
11
+ message: str
12
+ range: SpanData
13
+
14
+ """
15
+ This is only used internally, package users do not need to handle _Complete.
16
+ """
17
+ @dataclass(frozen=True)
18
+ class _Complete(Node):
19
+ range: SpanData
20
+
21
+ @dataclass(frozen=True)
22
+ class OuterText(Node):
23
+ value: str
24
+ range: SpanData
25
+
26
+ @dataclass(frozen=True)
27
+ class Function(Node):
28
+ name: str
29
+ arguments: List[Node]
30
+ range: SpanData
31
+
32
+ @dataclass(frozen=True)
33
+ class Number(Node):
34
+ value: str
35
+ range: SpanData
36
+
37
+ @dataclass(frozen=True)
38
+ class StringNode(Node):
39
+ value: str
40
+ range: SpanData
@@ -0,0 +1,204 @@
1
+ from bxengine.parsing.nodes import Node, Nodes
2
+ from typing import List, Tuple, Sequence
3
+ from bxengine.tokenizer.tokens import Token, Tokens
4
+ from bxengine.spans import SpanData
5
+ from dataclasses import dataclass
6
+ import functools
7
+
8
+
9
+ class ParsingResult:
10
+ @dataclass(frozen=True)
11
+ class Error:
12
+ message: str
13
+ range: SpanData
14
+
15
+ @dataclass(frozen=True)
16
+ class Success:
17
+ nodes: List[Node]
18
+
19
+ def pretty(self) -> str:
20
+ string = f"{len(self.nodes)} nodes:"
21
+ for node in self.nodes:
22
+ string += "\n"
23
+ string += str(node)
24
+
25
+ return string
26
+
27
+
28
+ class Parser:
29
+ _identity_cache: dict[int, tuple[str, Sequence[Token], ParsingResult.Success | ParsingResult.Error]] = {}
30
+ _identity_cache_limit: int = 512
31
+
32
+ def __init__(self):
33
+ self.nesting_level: int = 0
34
+ self.nesting_token_indexes: List[int] = []
35
+ self.contents: str = ""
36
+ self.token_list: Sequence[Token] = ()
37
+ self.is_function_declaration: bool = False
38
+ self.index: int = 0
39
+
40
+ @staticmethod
41
+ def parse(contents: str, token_list: Sequence[Token]) -> ParsingResult.Success | ParsingResult.Error:
42
+ # Fast path for repeated runs with the same token list object (common when
43
+ # tokenization cache hits across multiple executions in one process).
44
+ cache_key = id(token_list)
45
+ cached = Parser._identity_cache.get(cache_key)
46
+ if cached is not None:
47
+ cached_contents, cached_tokens, cached_result = cached
48
+ if cached_contents == contents and cached_tokens is token_list:
49
+ return cached_result
50
+
51
+ result = Parser._parse_cache(contents, tuple(token_list))
52
+
53
+ if len(Parser._identity_cache) >= Parser._identity_cache_limit:
54
+ Parser._identity_cache.clear()
55
+ Parser._identity_cache[cache_key] = (contents, token_list, result)
56
+ return result
57
+
58
+ @staticmethod
59
+ @functools.lru_cache(maxsize=128, typed=False)
60
+ def _parse_cache(contents: str, token_list: Tuple[Token]) -> ParsingResult.Success | ParsingResult.Error:
61
+ parser = Parser()
62
+ parser.contents = contents
63
+ parser.token_list = token_list
64
+ parser.nesting_level = 0
65
+ parser.nesting_token_indexes = []
66
+ parser.index = 0
67
+ parser.is_function_declaration = False
68
+
69
+ return parser._parse_loop()
70
+
71
+ def _parse_loop(self) -> ParsingResult.Success | ParsingResult.Error:
72
+ node_list = []
73
+ while True:
74
+ node = self._parse_once()
75
+
76
+ if isinstance(node, Nodes.Error):
77
+ return ParsingResult.Error(node.message, node.range)
78
+ elif isinstance(node, Nodes._Complete):
79
+ return ParsingResult.Success(node_list)
80
+
81
+ node_list.append(node)
82
+
83
+ def _parse_once(self) -> Node:
84
+ return self._parse_inner()
85
+
86
+ def _parse_inner(self) -> Node:
87
+ token = self.token_list[self.index]
88
+
89
+ if isinstance(token, Tokens.EndOfFile):
90
+ if self.nesting_level > 0:
91
+ bracket_index = self.nesting_token_indexes[-1]
92
+ return Nodes.Error("Unclosed bracket.", self.token_list[bracket_index].range)
93
+ else:
94
+ return Nodes._Complete(token.range)
95
+
96
+ if isinstance(token, Tokens.Error):
97
+ self.index += 1
98
+ return Nodes.Error(token.message, token.range)
99
+
100
+ if self.nesting_level == 0:
101
+ match token:
102
+ case Tokens.OuterString():
103
+ self.index += 1
104
+ return Nodes.OuterText(token.value, token.range)
105
+ case Tokens.OpenBracket():
106
+ return self._parse_function()
107
+ case _:
108
+ self.index += 1
109
+ text = self.contents[token.range.cursor_start:token.range.cursor_end]
110
+ return Nodes.Error(
111
+ f"Unexpected token '{text}' at top level. Only text and function calls are allowed.",
112
+ token.range
113
+ )
114
+ else:
115
+ self.index += 1
116
+ return Nodes.Error("Parser bug: `_parse_inner` called while (somehow??) nested. Please report this!",
117
+ token.range)
118
+
119
+ def _parse_function(self) -> Node:
120
+ open_bracket = self.token_list[self.index] # this is always an OpenBracket
121
+ self.index += 1 # consume
122
+
123
+ self.nesting_level += 1
124
+ self.nesting_token_indexes.append(self.index - 1)
125
+
126
+ if self.index >= len(self.token_list):
127
+ self.nesting_level -= 1
128
+ self.nesting_token_indexes.pop()
129
+ # Equivalent to empty brackets `[]` due to auto-close.
130
+ return Nodes.Error("Function call cannot be empty `[]`.", self.create_span(open_bracket, open_bracket))
131
+
132
+ name_token = self.token_list[self.index]
133
+
134
+ # BPPCOMPAT: Close on EndOfFile just like a CloseBracket
135
+ if isinstance(name_token, (Tokens.CloseBracket, Tokens.EndOfFile)):
136
+ if isinstance(name_token, Tokens.CloseBracket):
137
+ self.index += 1
138
+ self.nesting_level -= 1
139
+ self.nesting_token_indexes.pop()
140
+ return Nodes.Error("Function call cannot be empty `[]`.", self.create_span(open_bracket, name_token))
141
+
142
+ if not isinstance(name_token, Tokens.UnquotedString):
143
+ self.nesting_level -= 1
144
+ self.nesting_token_indexes.pop()
145
+ return Nodes.Error("Function name must be an unquoted string.", name_token.range)
146
+
147
+ self.index += 1 # consume
148
+
149
+ function_name = name_token.value
150
+ arguments = []
151
+
152
+ while True:
153
+ if self.index >= len(self.token_list):
154
+ self.nesting_level -= 1
155
+ self.nesting_token_indexes.pop()
156
+ last_token = self.token_list[-1] if self.token_list else open_bracket
157
+ return Nodes.Function(function_name, arguments, self.create_span(open_bracket, last_token))
158
+
159
+ current = self.token_list[self.index]
160
+
161
+ # BPPCOMPAT: Close on EndOfFile just like a CloseBracket
162
+ if isinstance(current, (Tokens.CloseBracket, Tokens.EndOfFile)):
163
+ if isinstance(current, Tokens.CloseBracket):
164
+ self.index += 1
165
+ self.nesting_level -= 1
166
+ self.nesting_token_indexes.pop()
167
+ return Nodes.Function(function_name, arguments, self.create_span(open_bracket, current))
168
+
169
+ argument = self._parse_expression()
170
+ if isinstance(argument, Nodes.Error):
171
+ return argument
172
+
173
+ arguments.append(argument)
174
+
175
+ def _parse_expression(self) -> Node:
176
+ token = self.token_list[self.index]
177
+
178
+ if isinstance(token, Tokens.Error):
179
+ self.index += 1
180
+ return Nodes.Error(token.message, token.range)
181
+
182
+ match token:
183
+ case Tokens.OpenBracket():
184
+ return self._parse_function()
185
+ case Tokens.Number():
186
+ self.index += 1
187
+ return Nodes.Number(token.value, token.range)
188
+ case Tokens.QuotedString():
189
+ self.index += 1
190
+ return Nodes.StringNode(token.value, token.range)
191
+ case Tokens.UnquotedString():
192
+ self.index += 1
193
+ return Nodes.StringNode(token.value, token.range)
194
+ case Tokens.EndOfFile():
195
+ bracket_index = self.nesting_token_indexes[-1]
196
+ return Nodes.Error("Parser bug: `_parse_expression` called and hit end of file (should be impossible...!)", self.token_list[bracket_index].range)
197
+ case _:
198
+ self.index += 1
199
+ return Nodes.Error("Unexpected token while parsing arguments.", token.range)
200
+
201
+ def create_span(self, start: Token, end: Token) -> SpanData:
202
+ start_int = start.range.cursor_start
203
+ end_int = end.range.cursor_end
204
+ return SpanData(start_int, end_int, self.contents)
@@ -0,0 +1,4 @@
1
+ from bxengine.runtime.context import RuntimeContext
2
+ from bxengine.runtime.executor import Executor, ExecutorResult, FunctionEntry
3
+
4
+ __all__ = ["RuntimeContext", "Executor", "ExecutorResult", "FunctionEntry"]
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from bxengine.runtime.executor import Executor, FunctionEntry
8
+
9
+
10
+ @dataclass
11
+ class RuntimeContext:
12
+ local_variables: dict[str, Any] = field(default_factory=dict)
13
+ program_args: list[Any] = field(default_factory=list)
14
+ executor: Executor | None = None
15
+ functions: dict[str, FunctionEntry] = field(default_factory=dict)