spacelanguage 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.
- spacelanguage-0.1.0/.github/workflows/tests.yml +24 -0
- spacelanguage-0.1.0/.gitignore +8 -0
- spacelanguage-0.1.0/CMakeLists.txt +13 -0
- spacelanguage-0.1.0/PKG-INFO +187 -0
- spacelanguage-0.1.0/README.md +180 -0
- spacelanguage-0.1.0/benchmarks/__init__.py +1 -0
- spacelanguage-0.1.0/benchmarks/benchmark_vm.py +117 -0
- spacelanguage-0.1.0/csrc/binding.cpp +11 -0
- spacelanguage-0.1.0/csrc/ir.hpp +14 -0
- spacelanguage-0.1.0/csrc/value.hpp +126 -0
- spacelanguage-0.1.0/csrc/vm.cpp +929 -0
- spacelanguage-0.1.0/csrc/vm.hpp +67 -0
- spacelanguage-0.1.0/pyproject.toml +20 -0
- spacelanguage-0.1.0/spacelang/__init__.py +59 -0
- spacelanguage-0.1.0/spacelang/asm.py +255 -0
- spacelanguage-0.1.0/spacelang/builtins_py.py +61 -0
- spacelanguage-0.1.0/spacelang/compress.py +37 -0
- spacelanguage-0.1.0/spacelang/decoder.py +12 -0
- spacelanguage-0.1.0/spacelang/disasm.py +67 -0
- spacelanguage-0.1.0/spacelang/encoder.py +108 -0
- spacelanguage-0.1.0/spacelang/errors.py +63 -0
- spacelanguage-0.1.0/spacelang/ir.py +58 -0
- spacelanguage-0.1.0/spacelang/lexer.py +38 -0
- spacelanguage-0.1.0/spacelang/main.py +131 -0
- spacelanguage-0.1.0/spacelang/modules.py +30 -0
- spacelanguage-0.1.0/spacelang/names.py +38 -0
- spacelanguage-0.1.0/spacelang/parser.py +173 -0
- spacelanguage-0.1.0/spacelang/peephole.py +345 -0
- spacelanguage-0.1.0/spacelang/pyfront.py +678 -0
- spacelanguage-0.1.0/spacelang/quine.py +393 -0
- spacelanguage-0.1.0/spacelang/vm_cpp.py +23 -0
- spacelanguage-0.1.0/spacelang/vm_py.py +532 -0
- spacelanguage-0.1.0/tests/test_0/demo.names.json +11 -0
- spacelanguage-0.1.0/tests/test_0/demo.py +16 -0
- spacelanguage-0.1.0/tests/test_0/demo.sp +277 -0
- spacelanguage-0.1.0/tests/test_0/demo.spasm +133 -0
- spacelanguage-0.1.0/tests/test_0/demo_from_spasm.names.json +11 -0
- spacelanguage-0.1.0/tests/test_0/demo_from_spasm.sp +277 -0
- spacelanguage-0.1.0/tests/test_0/test.py +50 -0
- spacelanguage-0.1.0/tests/test_compress.py +24 -0
- spacelanguage-0.1.0/tests/test_differential.py +46 -0
- spacelanguage-0.1.0/tests/test_m0.py +45 -0
- spacelanguage-0.1.0/tests/test_m1.py +118 -0
- spacelanguage-0.1.0/tests/test_m2.py +101 -0
- spacelanguage-0.1.0/tests/test_main.py +104 -0
- spacelanguage-0.1.0/tests/test_peephole.py +99 -0
- spacelanguage-0.1.0/tests/test_pyfront.py +144 -0
- spacelanguage-0.1.0/tests/test_quine/quine.copy.py +2 -0
- spacelanguage-0.1.0/tests/test_quine/quine.names.json +4 -0
- spacelanguage-0.1.0/tests/test_quine/quine.py +2 -0
- spacelanguage-0.1.0/tests/test_quine/quine.sp +59 -0
- spacelanguage-0.1.0/tests/test_quine/quine.spasm +10 -0
- spacelanguage-0.1.0/tests/test_quine/quine_from_sp.copy.py +2 -0
- spacelanguage-0.1.0/tests/test_quine/quine_from_spasm.copy.names.json +18 -0
- spacelanguage-0.1.0/tests/test_quine/quine_from_spasm.copy.py +2 -0
- spacelanguage-0.1.0/tests/test_quine/quine_from_spasm.copy.sp +237 -0
- spacelanguage-0.1.0/tests/test_quine/quine_from_spasm.names.json +4 -0
- spacelanguage-0.1.0/tests/test_quine/quine_from_spasm.sp +59 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.copy.names.json +18 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.copy.py +11 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.copy.sp +237 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.names.json +18 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.py +11 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.sp +237 -0
- spacelanguage-0.1.0/tests/test_quine/quine_self.spasm +90 -0
- spacelanguage-0.1.0/tests/test_quine/run_sp.py +62 -0
- spacelanguage-0.1.0/tests/test_quine/to_sp.py +29 -0
- spacelanguage-0.1.0/tests/test_quine.py +201 -0
- spacelanguage-0.1.0/tests/test_vm_cpp.py +451 -0
- spacelanguage-0.1.0/tests/test_vm_py.py +204 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
name: Python ${{ matrix.python-version }} / ${{ matrix.os }}
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- run: python -m pip install --upgrade pip
|
|
23
|
+
- run: python -m pip install -e . pytest
|
|
24
|
+
- run: python -m pytest -q
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18)
|
|
2
|
+
project(spacelang_cpp LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
|
|
5
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
6
|
+
|
|
7
|
+
pybind11_add_module(_spacelang_cpp MODULE
|
|
8
|
+
csrc/binding.cpp
|
|
9
|
+
csrc/vm.cpp
|
|
10
|
+
)
|
|
11
|
+
target_compile_features(_spacelang_cpp PRIVATE cxx_std_17)
|
|
12
|
+
target_include_directories(_spacelang_cpp PRIVATE csrc)
|
|
13
|
+
install(TARGETS _spacelang_cpp LIBRARY DESTINATION spacelang)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: spacelanguage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SpaceLang core package
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# SpaceLang
|
|
9
|
+
|
|
10
|
+
SpaceLang is an experimental stack-based language toolkit. It currently
|
|
11
|
+
contains a Python reference VM, an experimental C++ VM, a small Python-subset
|
|
12
|
+
frontend, and tools for assembling, disassembling, and encoding SpaceLang
|
|
13
|
+
programs.
|
|
14
|
+
|
|
15
|
+
> **Status:** Early development. The frontend and VM implement a subset of
|
|
16
|
+
> Python and the project is not yet a drop-in Python runtime. See the source and
|
|
17
|
+
> tests for the behavior currently supported.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3.10 or newer
|
|
22
|
+
- A C++17 compiler and CMake 3.18 or newer to build the native extension
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
Clone the repository and install it in editable mode:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/DwwLSM/spacelang.git
|
|
30
|
+
cd spacelang
|
|
31
|
+
python -m pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For development, install pytest as well:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pip install pytest
|
|
38
|
+
python -m pytest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Run Python-subset source
|
|
42
|
+
|
|
43
|
+
Compile supported Python syntax to a SpaceLang program, then execute it with
|
|
44
|
+
the reference VM:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from spacelang.pyfront import compile_py
|
|
48
|
+
from spacelang.vm_py import VM
|
|
49
|
+
|
|
50
|
+
source = """
|
|
51
|
+
def add(a, b):
|
|
52
|
+
return a + b
|
|
53
|
+
|
|
54
|
+
i = 0
|
|
55
|
+
while i < 3:
|
|
56
|
+
print(add(i, 2))
|
|
57
|
+
i = i + 1
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
program = compile_py(source)
|
|
61
|
+
VM(program).run()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Output:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
2
|
|
68
|
+
3
|
|
69
|
+
4
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The current frontend handles basic integer, boolean, and string constants,
|
|
73
|
+
arithmetic, comparisons, boolean expressions, calls, assignments, supported
|
|
74
|
+
imports, `if`/`else`, `while`, simple positional functions, and `for` loops
|
|
75
|
+
over `range(...)`. Sequence indexing is supported for indexable values.
|
|
76
|
+
|
|
77
|
+
This is still a Python subset, not full Python; features such as classes,
|
|
78
|
+
exceptions in source code, generators, nested or filtered comprehensions,
|
|
79
|
+
keyword arguments to user functions, and general iterator protocols are not implemented.
|
|
80
|
+
Unsupported syntax raises an error rather than being silently compiled.
|
|
81
|
+
|
|
82
|
+
## Create a self-copying program
|
|
83
|
+
|
|
84
|
+
For Python files, `quine` writes a new file with a small hook that copies that
|
|
85
|
+
generated file to a `.copy.py` sibling when run. The input file is left
|
|
86
|
+
unchanged:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from spacelang.quine import quine
|
|
90
|
+
|
|
91
|
+
quine("example.py", "example_quine.py")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Then run `python example_quine.py`. The CLI equivalent is:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m spacelang.quine example.py example_quine.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The CLI also supports `--in-place --yes`; it creates a `.bak` backup before
|
|
101
|
+
replacing the source. The Python API itself rejects in-place operation.
|
|
102
|
+
SpaceLang `.sp`/`.spz` quines produce a companion `.names.json` file because
|
|
103
|
+
the instruction stream stores name IDs, not their textual names.
|
|
104
|
+
|
|
105
|
+
## Command-line tools
|
|
106
|
+
|
|
107
|
+
The command-line interface supports Python compilation, assembly,
|
|
108
|
+
disassembly, execution with the reference VM, and `.sp`/`.spz` compression:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python -m spacelang.main compile-py example.py example.sp
|
|
112
|
+
python -m spacelang.main run example.sp
|
|
113
|
+
python -m spacelang.main run example.sp --engine cpp
|
|
114
|
+
python -m spacelang.main disasm example.sp
|
|
115
|
+
python -m spacelang.main pack example.sp
|
|
116
|
+
python -m spacelang.main run example.spz
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Compilation enables the safe peephole optimizations and integer constant pool
|
|
120
|
+
by default. Pass `--no-optimize` to `compile-py` to preserve the frontend IR,
|
|
121
|
+
or `--compress-source` to factorize large integer constants before encoding.
|
|
122
|
+
The C++ engine is experimental and is not the default. GCC and Clang builds use
|
|
123
|
+
computed-goto instruction dispatch; other compilers use a portable switch
|
|
124
|
+
dispatcher. Use the Python engine as the reference for behavior that has not
|
|
125
|
+
yet been verified for native parity.
|
|
126
|
+
|
|
127
|
+
To compare both engines on the same recursive workload after installation:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m benchmarks.benchmark_vm --n 25 --repeat 3
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The benchmark checks that both engines return the same result and reports the
|
|
134
|
+
median runtime and speedup. Timings depend on the host and are not a performance
|
|
135
|
+
guarantee.
|
|
136
|
+
|
|
137
|
+
## Assemble and run instructions
|
|
138
|
+
|
|
139
|
+
The assembler accepts one mnemonic instruction per line. For example:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from spacelang.asm import assemble
|
|
143
|
+
from spacelang.vm_py import VM
|
|
144
|
+
|
|
145
|
+
program = assemble("""
|
|
146
|
+
PUSH_INT 2
|
|
147
|
+
PUSH_INT 3
|
|
148
|
+
OP 1
|
|
149
|
+
HALT
|
|
150
|
+
""")
|
|
151
|
+
|
|
152
|
+
print(VM(program).run())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`OP 1` adds the two values on top of the stack. This example prints `5`.
|
|
156
|
+
|
|
157
|
+
## Inspect and encode programs
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from spacelang.asm import assemble
|
|
161
|
+
from spacelang.disasm import disassemble
|
|
162
|
+
from spacelang.encoder import encode
|
|
163
|
+
from spacelang.lexer import tokenize
|
|
164
|
+
from spacelang.parser import parse
|
|
165
|
+
|
|
166
|
+
program = assemble("PUSH_INT 2\nPUSH_INT 3\nOP 1\nHALT\n")
|
|
167
|
+
|
|
168
|
+
print(disassemble(program)) # Human-readable instructions
|
|
169
|
+
encoded = encode(program) # SpaceLang whitespace encoding
|
|
170
|
+
tokens = tokenize(encoded)
|
|
171
|
+
decoded_program = parse(tokens)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The package also exports the core `Program`, `Instr`, and `OP` types, along
|
|
175
|
+
with `assemble`, `disassemble`, `encode`, `parse`, and `tokenize` from
|
|
176
|
+
`spacelang`.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
Run the test suite from the repository root:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python -m pytest
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The tests cover the current IR, encoding/parser, assembler/disassembler, VM,
|
|
187
|
+
Python-subset frontend, and native/reference differential behavior.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# SpaceLang
|
|
2
|
+
|
|
3
|
+
SpaceLang is an experimental stack-based language toolkit. It currently
|
|
4
|
+
contains a Python reference VM, an experimental C++ VM, a small Python-subset
|
|
5
|
+
frontend, and tools for assembling, disassembling, and encoding SpaceLang
|
|
6
|
+
programs.
|
|
7
|
+
|
|
8
|
+
> **Status:** Early development. The frontend and VM implement a subset of
|
|
9
|
+
> Python and the project is not yet a drop-in Python runtime. See the source and
|
|
10
|
+
> tests for the behavior currently supported.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python 3.10 or newer
|
|
15
|
+
- A C++17 compiler and CMake 3.18 or newer to build the native extension
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Clone the repository and install it in editable mode:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/DwwLSM/spacelang.git
|
|
23
|
+
cd spacelang
|
|
24
|
+
python -m pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For development, install pytest as well:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m pip install pytest
|
|
31
|
+
python -m pytest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Run Python-subset source
|
|
35
|
+
|
|
36
|
+
Compile supported Python syntax to a SpaceLang program, then execute it with
|
|
37
|
+
the reference VM:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from spacelang.pyfront import compile_py
|
|
41
|
+
from spacelang.vm_py import VM
|
|
42
|
+
|
|
43
|
+
source = """
|
|
44
|
+
def add(a, b):
|
|
45
|
+
return a + b
|
|
46
|
+
|
|
47
|
+
i = 0
|
|
48
|
+
while i < 3:
|
|
49
|
+
print(add(i, 2))
|
|
50
|
+
i = i + 1
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
program = compile_py(source)
|
|
54
|
+
VM(program).run()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Output:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
2
|
|
61
|
+
3
|
|
62
|
+
4
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The current frontend handles basic integer, boolean, and string constants,
|
|
66
|
+
arithmetic, comparisons, boolean expressions, calls, assignments, supported
|
|
67
|
+
imports, `if`/`else`, `while`, simple positional functions, and `for` loops
|
|
68
|
+
over `range(...)`. Sequence indexing is supported for indexable values.
|
|
69
|
+
|
|
70
|
+
This is still a Python subset, not full Python; features such as classes,
|
|
71
|
+
exceptions in source code, generators, nested or filtered comprehensions,
|
|
72
|
+
keyword arguments to user functions, and general iterator protocols are not implemented.
|
|
73
|
+
Unsupported syntax raises an error rather than being silently compiled.
|
|
74
|
+
|
|
75
|
+
## Create a self-copying program
|
|
76
|
+
|
|
77
|
+
For Python files, `quine` writes a new file with a small hook that copies that
|
|
78
|
+
generated file to a `.copy.py` sibling when run. The input file is left
|
|
79
|
+
unchanged:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from spacelang.quine import quine
|
|
83
|
+
|
|
84
|
+
quine("example.py", "example_quine.py")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then run `python example_quine.py`. The CLI equivalent is:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python -m spacelang.quine example.py example_quine.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The CLI also supports `--in-place --yes`; it creates a `.bak` backup before
|
|
94
|
+
replacing the source. The Python API itself rejects in-place operation.
|
|
95
|
+
SpaceLang `.sp`/`.spz` quines produce a companion `.names.json` file because
|
|
96
|
+
the instruction stream stores name IDs, not their textual names.
|
|
97
|
+
|
|
98
|
+
## Command-line tools
|
|
99
|
+
|
|
100
|
+
The command-line interface supports Python compilation, assembly,
|
|
101
|
+
disassembly, execution with the reference VM, and `.sp`/`.spz` compression:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python -m spacelang.main compile-py example.py example.sp
|
|
105
|
+
python -m spacelang.main run example.sp
|
|
106
|
+
python -m spacelang.main run example.sp --engine cpp
|
|
107
|
+
python -m spacelang.main disasm example.sp
|
|
108
|
+
python -m spacelang.main pack example.sp
|
|
109
|
+
python -m spacelang.main run example.spz
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Compilation enables the safe peephole optimizations and integer constant pool
|
|
113
|
+
by default. Pass `--no-optimize` to `compile-py` to preserve the frontend IR,
|
|
114
|
+
or `--compress-source` to factorize large integer constants before encoding.
|
|
115
|
+
The C++ engine is experimental and is not the default. GCC and Clang builds use
|
|
116
|
+
computed-goto instruction dispatch; other compilers use a portable switch
|
|
117
|
+
dispatcher. Use the Python engine as the reference for behavior that has not
|
|
118
|
+
yet been verified for native parity.
|
|
119
|
+
|
|
120
|
+
To compare both engines on the same recursive workload after installation:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python -m benchmarks.benchmark_vm --n 25 --repeat 3
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The benchmark checks that both engines return the same result and reports the
|
|
127
|
+
median runtime and speedup. Timings depend on the host and are not a performance
|
|
128
|
+
guarantee.
|
|
129
|
+
|
|
130
|
+
## Assemble and run instructions
|
|
131
|
+
|
|
132
|
+
The assembler accepts one mnemonic instruction per line. For example:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from spacelang.asm import assemble
|
|
136
|
+
from spacelang.vm_py import VM
|
|
137
|
+
|
|
138
|
+
program = assemble("""
|
|
139
|
+
PUSH_INT 2
|
|
140
|
+
PUSH_INT 3
|
|
141
|
+
OP 1
|
|
142
|
+
HALT
|
|
143
|
+
""")
|
|
144
|
+
|
|
145
|
+
print(VM(program).run())
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`OP 1` adds the two values on top of the stack. This example prints `5`.
|
|
149
|
+
|
|
150
|
+
## Inspect and encode programs
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from spacelang.asm import assemble
|
|
154
|
+
from spacelang.disasm import disassemble
|
|
155
|
+
from spacelang.encoder import encode
|
|
156
|
+
from spacelang.lexer import tokenize
|
|
157
|
+
from spacelang.parser import parse
|
|
158
|
+
|
|
159
|
+
program = assemble("PUSH_INT 2\nPUSH_INT 3\nOP 1\nHALT\n")
|
|
160
|
+
|
|
161
|
+
print(disassemble(program)) # Human-readable instructions
|
|
162
|
+
encoded = encode(program) # SpaceLang whitespace encoding
|
|
163
|
+
tokens = tokenize(encoded)
|
|
164
|
+
decoded_program = parse(tokens)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The package also exports the core `Program`, `Instr`, and `OP` types, along
|
|
168
|
+
with `assemble`, `disassemble`, `encode`, `parse`, and `tokenize` from
|
|
169
|
+
`spacelang`.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
Run the test suite from the repository root:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
python -m pytest
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The tests cover the current IR, encoding/parser, assembler/disassembler, VM,
|
|
180
|
+
Python-subset frontend, and native/reference differential behavior.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SpaceLang benchmark utilities."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from contextlib import redirect_stdout
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from statistics import median
|
|
7
|
+
from time import perf_counter
|
|
8
|
+
|
|
9
|
+
from spacelang.pyfront import compile_py
|
|
10
|
+
from spacelang.vm_cpp import VM as CppVM
|
|
11
|
+
from spacelang.vm_py import VM as PythonVM
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def benchmark_engines(
|
|
15
|
+
source: str,
|
|
16
|
+
*,
|
|
17
|
+
repeats: int = 7,
|
|
18
|
+
warmup: int = 2,
|
|
19
|
+
result_name: str | None = "result",
|
|
20
|
+
) -> dict[str, float]:
|
|
21
|
+
"""Return median execution times for CPython and both SpaceLang VMs."""
|
|
22
|
+
if repeats < 1 or warmup < 0:
|
|
23
|
+
raise ValueError("repeats must be >= 1 and warmup must be non-negative")
|
|
24
|
+
|
|
25
|
+
python_code = compile(source, "<benchmark>", "exec")
|
|
26
|
+
program = compile_py(source)
|
|
27
|
+
engine_names = ("CPython", "SP Python", "SP C++")
|
|
28
|
+
samples = {name: [] for name in engine_names}
|
|
29
|
+
|
|
30
|
+
def run_once(engine: str) -> tuple[float, str, object]:
|
|
31
|
+
output = StringIO()
|
|
32
|
+
namespace = {"__name__": "__main__"}
|
|
33
|
+
machine = None
|
|
34
|
+
if engine == "SP Python":
|
|
35
|
+
machine = PythonVM(program)
|
|
36
|
+
elif engine == "SP C++":
|
|
37
|
+
machine = CppVM(program)
|
|
38
|
+
|
|
39
|
+
with redirect_stdout(output):
|
|
40
|
+
started = perf_counter()
|
|
41
|
+
if engine == "CPython":
|
|
42
|
+
exec(python_code, namespace, namespace)
|
|
43
|
+
else:
|
|
44
|
+
machine.run()
|
|
45
|
+
elapsed = perf_counter() - started
|
|
46
|
+
|
|
47
|
+
state = namespace if machine is None else machine.globals_
|
|
48
|
+
if result_name is None:
|
|
49
|
+
result = None
|
|
50
|
+
else:
|
|
51
|
+
if result_name not in state:
|
|
52
|
+
raise AssertionError(f"{engine} did not set {result_name!r}")
|
|
53
|
+
result = state[result_name]
|
|
54
|
+
return elapsed, output.getvalue(), result
|
|
55
|
+
|
|
56
|
+
for iteration in range(warmup + repeats):
|
|
57
|
+
offset = iteration % len(engine_names)
|
|
58
|
+
order = engine_names[offset:] + engine_names[:offset]
|
|
59
|
+
measured = {engine: run_once(engine) for engine in order}
|
|
60
|
+
reference_output = measured["CPython"][1]
|
|
61
|
+
reference_result = measured["CPython"][2]
|
|
62
|
+
|
|
63
|
+
for engine, (_, output, result) in measured.items():
|
|
64
|
+
if output != reference_output or type(result) is not type(reference_result) or result != reference_result:
|
|
65
|
+
raise AssertionError(f"{engine} differs from CPython")
|
|
66
|
+
|
|
67
|
+
if iteration >= warmup:
|
|
68
|
+
for engine, (elapsed, _, _) in measured.items():
|
|
69
|
+
samples[engine].append(elapsed)
|
|
70
|
+
|
|
71
|
+
return {engine: median(durations) for engine, durations in samples.items()}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> None:
|
|
75
|
+
parser = argparse.ArgumentParser(description="Compare CPython and both SpaceLang VM runtimes.")
|
|
76
|
+
parser.add_argument("--n", type=int, default=27, help="Fibonacci input (default: 27)")
|
|
77
|
+
parser.add_argument("--repeat", type=int, default=7, help="Measured runs per engine (default: 7)")
|
|
78
|
+
parser.add_argument("--warmup", type=int, default=2, help="Unmeasured runs per engine (default: 2)")
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
if args.n < 0 or args.repeat < 1 or args.warmup < 0:
|
|
81
|
+
parser.error("--n must be non-negative, --repeat >= 1, and --warmup >= 0")
|
|
82
|
+
|
|
83
|
+
source = f"""
|
|
84
|
+
def fib(n):
|
|
85
|
+
if n < 2:
|
|
86
|
+
return n
|
|
87
|
+
return fib(n - 1) + fib(n - 2)
|
|
88
|
+
|
|
89
|
+
result = fib({args.n})
|
|
90
|
+
"""
|
|
91
|
+
source = f"""
|
|
92
|
+
|
|
93
|
+
from math import sqrt
|
|
94
|
+
sum = 0
|
|
95
|
+
for i in range(100000):
|
|
96
|
+
sum = sqrt(sum) if sum > 20000000 else sqrt(i) * sqrt(3) * 2 + sum
|
|
97
|
+
|
|
98
|
+
result = sum
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
durations = benchmark_engines(source, repeats=args.repeat, warmup=args.warmup)
|
|
104
|
+
baseline = durations["CPython"]
|
|
105
|
+
for name, duration in durations.items():
|
|
106
|
+
comparison = "baseline" if name == "CPython" else (
|
|
107
|
+
f"{baseline / duration:.2f}x faster than CPython"
|
|
108
|
+
if duration < baseline
|
|
109
|
+
else f"{duration / baseline:.2f}x slower than CPython"
|
|
110
|
+
)
|
|
111
|
+
print(f"{name} median: {duration:.6f}s ({comparison})")
|
|
112
|
+
print(f"SP C++ vs SP Python: {durations['SP Python'] / durations['SP C++']:.2f}x faster")
|
|
113
|
+
print("All engines produced matching output and results.")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include <pybind11/pybind11.h>
|
|
2
|
+
|
|
3
|
+
#include "vm.hpp"
|
|
4
|
+
|
|
5
|
+
PYBIND11_MODULE(_spacelang_cpp, module) {
|
|
6
|
+
pybind11::class_<spacelang::VM>(module, "VM")
|
|
7
|
+
.def(pybind11::init<pybind11::object>())
|
|
8
|
+
.def("run", &spacelang::VM::run)
|
|
9
|
+
.def_property_readonly("stack", &spacelang::VM::stack)
|
|
10
|
+
.def_property_readonly("globals", &spacelang::VM::globals);
|
|
11
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <utility>
|
|
5
|
+
|
|
6
|
+
#include <Python.h>
|
|
7
|
+
#include <pybind11/pybind11.h>
|
|
8
|
+
|
|
9
|
+
namespace spacelang {
|
|
10
|
+
|
|
11
|
+
namespace py = pybind11;
|
|
12
|
+
|
|
13
|
+
struct Value {
|
|
14
|
+
enum class Tag : std::uint8_t { INT, FLOAT, BOOL, NONE, PYOBJ };
|
|
15
|
+
|
|
16
|
+
union Payload {
|
|
17
|
+
std::int64_t i;
|
|
18
|
+
double f;
|
|
19
|
+
bool b;
|
|
20
|
+
PyObject* obj;
|
|
21
|
+
|
|
22
|
+
Payload() : i(0) {}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
Tag tag = Tag::NONE;
|
|
26
|
+
Payload data;
|
|
27
|
+
|
|
28
|
+
Value() = default;
|
|
29
|
+
explicit Value(std::int64_t value) : tag(Tag::INT) { data.i = value; }
|
|
30
|
+
explicit Value(double value) : tag(Tag::FLOAT) { data.f = value; }
|
|
31
|
+
explicit Value(bool value) : tag(Tag::BOOL) { data.b = value; }
|
|
32
|
+
|
|
33
|
+
static Value from_python(py::handle value) {
|
|
34
|
+
if (value.is_none()) {
|
|
35
|
+
return Value();
|
|
36
|
+
}
|
|
37
|
+
if (PyBool_Check(value.ptr())) {
|
|
38
|
+
return Value(value.ptr() == Py_True);
|
|
39
|
+
}
|
|
40
|
+
if (PyLong_Check(value.ptr())) {
|
|
41
|
+
const auto integer = PyLong_AsLongLong(value.ptr());
|
|
42
|
+
if (PyErr_Occurred()) {
|
|
43
|
+
PyErr_Clear();
|
|
44
|
+
} else {
|
|
45
|
+
return Value(static_cast<std::int64_t>(integer));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (PyFloat_Check(value.ptr())) {
|
|
49
|
+
return Value(PyFloat_AS_DOUBLE(value.ptr()));
|
|
50
|
+
}
|
|
51
|
+
Value result;
|
|
52
|
+
result.tag = Tag::PYOBJ;
|
|
53
|
+
result.data.obj = value.ptr();
|
|
54
|
+
Py_INCREF(result.data.obj);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Value(const Value& other) : tag(other.tag), data(other.data) {
|
|
59
|
+
if (tag == Tag::PYOBJ) {
|
|
60
|
+
Py_INCREF(data.obj);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Value(Value&& other) noexcept : tag(other.tag), data(other.data) {
|
|
65
|
+
other.tag = Tag::NONE;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Value& operator=(Value other) noexcept {
|
|
69
|
+
swap(other);
|
|
70
|
+
return *this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
~Value() {
|
|
74
|
+
if (tag == Tag::PYOBJ) {
|
|
75
|
+
Py_DECREF(data.obj);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void swap(Value& other) noexcept {
|
|
80
|
+
const auto temporary = data;
|
|
81
|
+
data = other.data;
|
|
82
|
+
other.data = temporary;
|
|
83
|
+
const auto previous_tag = tag;
|
|
84
|
+
tag = other.tag;
|
|
85
|
+
other.tag = previous_tag;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
py::object to_python() const {
|
|
89
|
+
switch (tag) {
|
|
90
|
+
case Tag::INT:
|
|
91
|
+
return py::int_(data.i);
|
|
92
|
+
case Tag::FLOAT:
|
|
93
|
+
return py::float_(data.f);
|
|
94
|
+
case Tag::BOOL:
|
|
95
|
+
return py::bool_(data.b);
|
|
96
|
+
case Tag::NONE:
|
|
97
|
+
return py::none();
|
|
98
|
+
case Tag::PYOBJ:
|
|
99
|
+
return py::reinterpret_borrow<py::object>(data.obj);
|
|
100
|
+
}
|
|
101
|
+
throw py::type_error("invalid SpaceLang Value tag");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bool truthy() const {
|
|
105
|
+
switch (tag) {
|
|
106
|
+
case Tag::INT:
|
|
107
|
+
return data.i != 0;
|
|
108
|
+
case Tag::FLOAT:
|
|
109
|
+
return data.f != 0.0;
|
|
110
|
+
case Tag::BOOL:
|
|
111
|
+
return data.b;
|
|
112
|
+
case Tag::NONE:
|
|
113
|
+
return false;
|
|
114
|
+
case Tag::PYOBJ: {
|
|
115
|
+
const int result = PyObject_IsTrue(data.obj);
|
|
116
|
+
if (result < 0) {
|
|
117
|
+
throw py::error_already_set();
|
|
118
|
+
}
|
|
119
|
+
return result != 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
} // namespace spacelang
|