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.
- bxengine-0.1.0/PKG-INFO +46 -0
- bxengine-0.1.0/README.md +30 -0
- bxengine-0.1.0/pyproject.toml +44 -0
- bxengine-0.1.0/setup.cfg +4 -0
- bxengine-0.1.0/src/bxengine/__about__.py +1 -0
- bxengine-0.1.0/src/bxengine/__init__.py +3 -0
- bxengine-0.1.0/src/bxengine/__main__.py +4 -0
- bxengine-0.1.0/src/bxengine/constants.py +2 -0
- bxengine-0.1.0/src/bxengine/exceptions.py +20 -0
- bxengine-0.1.0/src/bxengine/module.py +72 -0
- bxengine-0.1.0/src/bxengine/parsing/__init__.py +0 -0
- bxengine-0.1.0/src/bxengine/parsing/nodes.py +40 -0
- bxengine-0.1.0/src/bxengine/parsing/parser.py +204 -0
- bxengine-0.1.0/src/bxengine/runtime/__init__.py +4 -0
- bxengine-0.1.0/src/bxengine/runtime/context.py +15 -0
- bxengine-0.1.0/src/bxengine/runtime/executor.py +315 -0
- bxengine-0.1.0/src/bxengine/runtime/extensions/BxeExtension.py +72 -0
- bxengine-0.1.0/src/bxengine/runtime/extensions/__init__.py +15 -0
- bxengine-0.1.0/src/bxengine/runtime/extensions/builtin.py +586 -0
- bxengine-0.1.0/src/bxengine/runtime/extensions/discord_stub.py +25 -0
- bxengine-0.1.0/src/bxengine/spans.py +136 -0
- bxengine-0.1.0/src/bxengine/tokenizer/__init__.py +0 -0
- bxengine-0.1.0/src/bxengine/tokenizer/stringreader.py +163 -0
- bxengine-0.1.0/src/bxengine/tokenizer/tokenize.py +188 -0
- bxengine-0.1.0/src/bxengine/tokenizer/tokens.py +45 -0
- bxengine-0.1.0/src/bxengine.egg-info/PKG-INFO +46 -0
- bxengine-0.1.0/src/bxengine.egg-info/SOURCES.txt +31 -0
- bxengine-0.1.0/src/bxengine.egg-info/dependency_links.txt +1 -0
- bxengine-0.1.0/src/bxengine.egg-info/entry_points.txt +2 -0
- bxengine-0.1.0/src/bxengine.egg-info/top_level.txt +1 -0
- bxengine-0.1.0/tests/test_parser.py +87 -0
- bxengine-0.1.0/tests/test_runtime.py +461 -0
- bxengine-0.1.0/tests/test_tokenizer.py +96 -0
bxengine-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
bxengine-0.1.0/README.md
ADDED
|
@@ -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
|
+
]
|
bxengine-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -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,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)
|