toyc 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.
- toyc-0.1.0/PKG-INFO +39 -0
- toyc-0.1.0/README.md +0 -0
- toyc-0.1.0/pyproject.toml +39 -0
- toyc-0.1.0/setup.cfg +4 -0
- toyc-0.1.0/toyc/LICENSE +18 -0
- toyc-0.1.0/toyc/__init__.py +10 -0
- toyc-0.1.0/toyc/ast_nodes.py +68 -0
- toyc-0.1.0/toyc/compiler.py +199 -0
- toyc-0.1.0/toyc/evaluator.py +36 -0
- toyc-0.1.0/toyc/gpu/__init__.py +6 -0
- toyc-0.1.0/toyc/gpu/flattener.py +56 -0
- toyc-0.1.0/toyc/gpu/instructions.py +27 -0
- toyc-0.1.0/toyc/gpu/opengl/__init__.py +9 -0
- toyc-0.1.0/toyc/gpu/opengl/backend.py +136 -0
- toyc-0.1.0/toyc/gpu/vulkan/__init__.py +9 -0
- toyc-0.1.0/toyc/gpu/vulkan/backend.py +136 -0
- toyc-0.1.0/toyc/lexer.py +98 -0
- toyc-0.1.0/toyc/parser.py +78 -0
- toyc-0.1.0/toyc/vm.py +182 -0
- toyc-0.1.0/toyc.egg-info/PKG-INFO +39 -0
- toyc-0.1.0/toyc.egg-info/SOURCES.txt +22 -0
- toyc-0.1.0/toyc.egg-info/dependency_links.txt +1 -0
- toyc-0.1.0/toyc.egg-info/requires.txt +8 -0
- toyc-0.1.0/toyc.egg-info/top_level.txt +1 -0
toyc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toyc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A toy compiler with GPU backends (Vulkan + OpenGL)
|
|
5
|
+
Author: spy1345a
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 spy1345a
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
11
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
12
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
14
|
+
following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
17
|
+
portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
20
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
21
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
22
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
23
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
Project-URL: Homepage, https://github.com/spy1345a/toyc-repo
|
|
26
|
+
Project-URL: Repository, https://github.com/spy1345a/toyc-repo
|
|
27
|
+
Keywords: compiler,gpu,vulkan,opengl,toy
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
32
|
+
Requires-Python: >=3.10
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
Requires-Dist: pyopengl>=3.1
|
|
35
|
+
Requires-Dist: vulkan>=1.3.275.1
|
|
36
|
+
Provides-Extra: vulkan
|
|
37
|
+
Requires-Dist: vulkan; extra == "vulkan"
|
|
38
|
+
Provides-Extra: opengl
|
|
39
|
+
Requires-Dist: PyOpenGL; extra == "opengl"
|
toyc-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "toyc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A toy compiler with GPU backends (Vulkan + OpenGL)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "toyc/LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "spy1345a"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["compiler", "gpu", "vulkan", "opengl", "toy"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Software Development :: Compilers",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# list any actual pip deps your code imports at runtime:
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pyopengl>=3.1",
|
|
26
|
+
"vulkan>=1.3.275.1"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/spy1345a/toyc-repo"
|
|
31
|
+
Repository = "https://github.com/spy1345a/toyc-repo"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
vulkan = ["vulkan"]
|
|
35
|
+
opengl = ["PyOpenGL"]
|
|
36
|
+
|
|
37
|
+
# If you want a CLI entrypoint (e.g. `toyc compile foo.tc`): comming soon
|
|
38
|
+
# [project.scripts]
|
|
39
|
+
# toyc = "toyc.compiler:main"
|
toyc-0.1.0/setup.cfg
ADDED
toyc-0.1.0/toyc/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 spy1345a
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# compiler/__init__.py
|
|
2
|
+
|
|
3
|
+
from .lexer import Lexer
|
|
4
|
+
from .parser import Parser
|
|
5
|
+
from .gpu import Flattener
|
|
6
|
+
from .vm import Cpu , GpuVulkan , GpuOpengl
|
|
7
|
+
from .compiler import Compiler
|
|
8
|
+
|
|
9
|
+
__all__ = ["Lexer", "Parser", "Evaluator", "Flattener",
|
|
10
|
+
"Instruction", "compile_to_file", "load_from_file"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# compiler/ast_nodes.py
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Number:
|
|
8
|
+
value: float | int
|
|
9
|
+
|
|
10
|
+
def __repr__(self):
|
|
11
|
+
return f"Number({self.value})"
|
|
12
|
+
|
|
13
|
+
def to_dict(self):
|
|
14
|
+
return {"type": "Number", "value": self.value}
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def from_dict(d):
|
|
18
|
+
return Number(d["value"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Var:
|
|
23
|
+
name: str
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"Var({self.name!r})"
|
|
27
|
+
|
|
28
|
+
def to_dict(self):
|
|
29
|
+
return {"type": "Var", "name": self.name}
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_dict(d):
|
|
33
|
+
return Var(d["name"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class BinOp:
|
|
38
|
+
op: str
|
|
39
|
+
left: Any
|
|
40
|
+
right: Any
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return f"BinOp({self.op!r}, {self.left}, {self.right})"
|
|
44
|
+
|
|
45
|
+
def to_dict(self):
|
|
46
|
+
return {
|
|
47
|
+
"type": "BinOp",
|
|
48
|
+
"op": self.op,
|
|
49
|
+
"left": self.left.to_dict(),
|
|
50
|
+
"right": self.right.to_dict(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def from_dict(d):
|
|
55
|
+
return BinOp(
|
|
56
|
+
op = d["op"],
|
|
57
|
+
left = node_from_dict(d["left"]),
|
|
58
|
+
right = node_from_dict(d["right"]),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def node_from_dict(d):
|
|
63
|
+
"""Reconstruct any AST node from a dict."""
|
|
64
|
+
kind = d["type"]
|
|
65
|
+
if kind == "Number": return Number.from_dict(d)
|
|
66
|
+
if kind == "Var": return Var.from_dict(d)
|
|
67
|
+
if kind == "BinOp": return BinOp.from_dict(d)
|
|
68
|
+
raise ValueError(f"Unknown node type: {kind!r}")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# compiler/compiler.py
|
|
2
|
+
#
|
|
3
|
+
# Responsible for:
|
|
4
|
+
# • Defining the Instr dataclass (the instruction set)
|
|
5
|
+
# • .toyc binary serialisation / deserialisation helpers
|
|
6
|
+
# • The Compiler class (AST → list[Instr], with disk write)
|
|
7
|
+
#
|
|
8
|
+
# It knows nothing about execution — import vm.py for that.
|
|
9
|
+
#
|
|
10
|
+
# Two calling styles are supported:
|
|
11
|
+
#
|
|
12
|
+
# A) AST-first (you already have an AST node):
|
|
13
|
+
# Compiler.compile(ast) → writes out.toyc
|
|
14
|
+
# Compiler.compile(ast, path="foo.toyc") → writes foo.toyc
|
|
15
|
+
# Compiler.compile(ast, path=None) → memory only
|
|
16
|
+
#
|
|
17
|
+
# B) File-first (point straight at a .toy source file):
|
|
18
|
+
# Compiler.compile("script.toy") → writes script.toyc
|
|
19
|
+
# Compiler.compile("script.toy", path=None) → memory only
|
|
20
|
+
#
|
|
21
|
+
# The Lexer and Parser are invoked internally so the caller doesn't
|
|
22
|
+
# need to tokenise or build an AST manually.
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import pickle
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from .ast_nodes import Number, Var, BinOp
|
|
30
|
+
from .lexer import Lexer
|
|
31
|
+
from .parser import Parser
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── instruction set ───────────────────────────────────────────────────────────
|
|
35
|
+
# These map 1-to-1 to SPIR-V ops later:
|
|
36
|
+
# PUSH → OpConstant
|
|
37
|
+
# LOAD → OpLoad
|
|
38
|
+
# ADD → OpFAdd
|
|
39
|
+
# SUB → OpFSub
|
|
40
|
+
# MUL → OpFMul
|
|
41
|
+
# DIV → OpFDiv
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Instr:
|
|
45
|
+
op: str
|
|
46
|
+
arg: Any = None
|
|
47
|
+
|
|
48
|
+
def __repr__(self):
|
|
49
|
+
return f"{self.op} {self.arg!r}" if self.arg is not None else self.op
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── .toyc binary format ───────────────────────────────────────────────────────
|
|
53
|
+
# Layout:
|
|
54
|
+
# bytes 0-3 : MAGIC b'\x54\x4F\x59\x43' ("TOYC")
|
|
55
|
+
# bytes 4+ : pickle.dumps(list[Instr])
|
|
56
|
+
#
|
|
57
|
+
# The magic header lets Cpu.run() detect compiled files by peeking at raw
|
|
58
|
+
# bytes, without relying on the file extension alone.
|
|
59
|
+
|
|
60
|
+
MAGIC = b'\x54\x4F\x59\x43' # "TOYC"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_bytecode(path: str, instructions: list) -> None:
|
|
64
|
+
"""Serialise *instructions* to a .toyc file at *path*."""
|
|
65
|
+
with open(path, "wb") as f:
|
|
66
|
+
f.write(MAGIC)
|
|
67
|
+
pickle.dump(instructions, f)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_bytecode(path: str) -> list:
|
|
71
|
+
"""Deserialise a .toyc file and return the list[Instr]."""
|
|
72
|
+
with open(path, "rb") as f:
|
|
73
|
+
magic = f.read(4)
|
|
74
|
+
if magic != MAGIC:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"{path!r} is not a valid .toyc file (bad magic bytes)"
|
|
77
|
+
)
|
|
78
|
+
return pickle.load(f)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_compiled_bytecode(path: str) -> bool:
|
|
82
|
+
"""Return True if *path* starts with the TOYC magic number."""
|
|
83
|
+
try:
|
|
84
|
+
with open(path, "rb") as f:
|
|
85
|
+
return f.read(4) == MAGIC
|
|
86
|
+
except OSError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
def _source_to_ast(toy_path: str):
|
|
93
|
+
"""Read a .toy file and return its parsed AST using Lexer + Parser."""
|
|
94
|
+
with open(toy_path, "r", encoding="utf-8") as f:
|
|
95
|
+
source = f.read()
|
|
96
|
+
tokens = Lexer.tokenize(source)
|
|
97
|
+
return Parser.parse(tokens)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── Compiler: AST → bytecode ──────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
class Compiler:
|
|
103
|
+
"""Walk an AST and emit a flat list[Instr].
|
|
104
|
+
|
|
105
|
+
Calling styles
|
|
106
|
+
--------------
|
|
107
|
+
# A) You already have tokens / an AST (original workflow):
|
|
108
|
+
tokens = Lexer.tokenize(code)
|
|
109
|
+
ast = Parser.parse(tokens)
|
|
110
|
+
Compiler.compile(ast) # writes out.toyc
|
|
111
|
+
Compiler.compile(ast, path="expr.toyc") # writes expr.toyc
|
|
112
|
+
Compiler.compile(ast, path=None) # memory only
|
|
113
|
+
|
|
114
|
+
# B) Point straight at a .toy source file:
|
|
115
|
+
Compiler.compile("script.toy") # reads script.toy,
|
|
116
|
+
# writes script.toyc next to it
|
|
117
|
+
Compiler.compile("script.toy", path=None) # reads script.toy,
|
|
118
|
+
# memory only (no .toyc written)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def compile(node_or_path, path: str = "out.toyc") -> list:
|
|
123
|
+
"""
|
|
124
|
+
Compile source or an AST node into bytecode.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
node_or_path : AST node OR str
|
|
129
|
+
• An AST node → compiled directly (original behaviour).
|
|
130
|
+
• A str ending in ``.toy`` → the file is read, lexed, and parsed
|
|
131
|
+
first; the default output path becomes a sibling .toyc with the
|
|
132
|
+
same stem (overrides the ``path`` default of ``"out.toyc"``).
|
|
133
|
+
|
|
134
|
+
path : str | None, optional
|
|
135
|
+
Where to write the .toyc file.
|
|
136
|
+
• ``"out.toyc"`` (default) when *node_or_path* is an AST node.
|
|
137
|
+
• Auto-derived sibling path when *node_or_path* is a .toy file.
|
|
138
|
+
• ``None`` → compile but do not write anything to disk.
|
|
139
|
+
The .toyc extension is enforced regardless of what is passed.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
list[Instr]
|
|
144
|
+
Flat bytecode list (always returned).
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
# ── resolve the input ────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
if isinstance(node_or_path, str):
|
|
150
|
+
# File-first mode
|
|
151
|
+
toy_path = node_or_path
|
|
152
|
+
if not toy_path.endswith(".toy"):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Expected a .toy source file, got: {toy_path!r}"
|
|
155
|
+
)
|
|
156
|
+
if not os.path.isfile(toy_path):
|
|
157
|
+
raise FileNotFoundError(f"Source file not found: {toy_path!r}")
|
|
158
|
+
|
|
159
|
+
node = _source_to_ast(toy_path)
|
|
160
|
+
|
|
161
|
+
# Derive sibling .toyc path unless the caller overrode it
|
|
162
|
+
if path == "out.toyc": # still the default → replace with sibling
|
|
163
|
+
path = os.path.splitext(os.path.abspath(toy_path))[0] + ".toyc"
|
|
164
|
+
|
|
165
|
+
else:
|
|
166
|
+
# AST-first mode — node_or_path is an AST node
|
|
167
|
+
node = node_or_path
|
|
168
|
+
# path stays as whatever the caller passed ("out.toyc" or explicit)
|
|
169
|
+
|
|
170
|
+
# ── emit bytecode ────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
instructions: list[Instr] = []
|
|
173
|
+
Compiler._emit(node, instructions)
|
|
174
|
+
|
|
175
|
+
# ── write to disk ────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
if path is not None:
|
|
178
|
+
if not path.endswith(".toyc"):
|
|
179
|
+
path = os.path.splitext(path)[0] + ".toyc"
|
|
180
|
+
write_bytecode(path, instructions)
|
|
181
|
+
|
|
182
|
+
return instructions
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _emit(node, out: list) -> None:
|
|
186
|
+
if isinstance(node, Number):
|
|
187
|
+
out.append(Instr("PUSH", node.value))
|
|
188
|
+
|
|
189
|
+
elif isinstance(node, Var):
|
|
190
|
+
out.append(Instr("LOAD", node.name))
|
|
191
|
+
|
|
192
|
+
elif isinstance(node, BinOp):
|
|
193
|
+
Compiler._emit(node.left, out) # push left operand
|
|
194
|
+
Compiler._emit(node.right, out) # push right operand
|
|
195
|
+
op_map = {"+": "ADD", "-": "SUB", "*": "MUL", "/": "DIV"}
|
|
196
|
+
out.append(Instr(op_map[node.op]))
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
raise TypeError(f"Unknown AST node: {type(node).__name__}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# compiler/evaluator.py
|
|
2
|
+
|
|
3
|
+
from .ast_nodes import Number, Var, BinOp
|
|
4
|
+
|
|
5
|
+
class Evaluator:
|
|
6
|
+
def __init__(self, env=None):
|
|
7
|
+
# env holds variable values e.g {"a": 10, "b": 5}
|
|
8
|
+
self.env = env or {}
|
|
9
|
+
|
|
10
|
+
def eval(self, node):
|
|
11
|
+
# base case — just return the number
|
|
12
|
+
if isinstance(node, Number):
|
|
13
|
+
return node.value
|
|
14
|
+
|
|
15
|
+
# variable lookup
|
|
16
|
+
if isinstance(node, Var):
|
|
17
|
+
if node.name not in self.env:
|
|
18
|
+
raise NameError(f"Undefined variable: {node.name!r}")
|
|
19
|
+
return self.env[node.name]
|
|
20
|
+
|
|
21
|
+
# recursive case — evaluate both sides then apply operator
|
|
22
|
+
if isinstance(node, BinOp):
|
|
23
|
+
left = self.eval(node.left)
|
|
24
|
+
right = self.eval(node.right)
|
|
25
|
+
|
|
26
|
+
if node.op == "+": return left + right
|
|
27
|
+
if node.op == "-": return left - right
|
|
28
|
+
if node.op == "*": return left * right
|
|
29
|
+
if node.op == "/":
|
|
30
|
+
if right == 0:
|
|
31
|
+
raise ZeroDivisionError("Division by zero in expression")
|
|
32
|
+
return left / right
|
|
33
|
+
|
|
34
|
+
raise ValueError(f"Unknown operator: {node.op!r}")
|
|
35
|
+
|
|
36
|
+
raise TypeError(f"Unknown AST node: {type(node)}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# compiler/flattener.py
|
|
2
|
+
|
|
3
|
+
from ..ast_nodes import Number, Var, BinOp
|
|
4
|
+
from .instructions import Instruction, ADD, SUB, MUL, DIV, LOAD, VAR
|
|
5
|
+
|
|
6
|
+
class Flattener:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.instructions = []
|
|
9
|
+
self.reg_counter = 0 # next free register
|
|
10
|
+
self.var_map = {} # var name → register index
|
|
11
|
+
self.const_values = [] # constant pool [1.0, 2.5, ...]
|
|
12
|
+
|
|
13
|
+
def new_reg(self):
|
|
14
|
+
"""Allocate a new register, return its index."""
|
|
15
|
+
r = self.reg_counter
|
|
16
|
+
self.reg_counter += 1
|
|
17
|
+
return r
|
|
18
|
+
|
|
19
|
+
def flatten(self, node):
|
|
20
|
+
"""Recursively flatten an AST node, return the register holding result."""
|
|
21
|
+
|
|
22
|
+
if isinstance(node, Number):
|
|
23
|
+
dest = self.new_reg()
|
|
24
|
+
const_idx = len(self.const_values)
|
|
25
|
+
self.const_values.append(float(node.value))
|
|
26
|
+
self.instructions.append(Instruction(LOAD, dest, const_idx))
|
|
27
|
+
return dest
|
|
28
|
+
|
|
29
|
+
if isinstance(node, Var):
|
|
30
|
+
# each unique variable gets one register
|
|
31
|
+
if node.name not in self.var_map:
|
|
32
|
+
dest = self.new_reg()
|
|
33
|
+
var_idx = len(self.var_map)
|
|
34
|
+
self.var_map[node.name] = (dest, var_idx)
|
|
35
|
+
self.instructions.append(Instruction(VAR, dest, var_idx))
|
|
36
|
+
return self.var_map[node.name][0]
|
|
37
|
+
|
|
38
|
+
if isinstance(node, BinOp):
|
|
39
|
+
left_reg = self.flatten(node.left)
|
|
40
|
+
right_reg = self.flatten(node.right)
|
|
41
|
+
dest = self.new_reg()
|
|
42
|
+
|
|
43
|
+
op = {"+": ADD, "-": SUB, "*": MUL, "/": DIV}[node.op]
|
|
44
|
+
self.instructions.append(Instruction(op, dest, left_reg, right_reg))
|
|
45
|
+
return dest
|
|
46
|
+
|
|
47
|
+
raise TypeError(f"Unknown node: {type(node)}")
|
|
48
|
+
|
|
49
|
+
def get_flat(self):
|
|
50
|
+
"""Return flat int list ready for GPU upload.
|
|
51
|
+
Layout: [op, dest, src1, src2, op, dest, src1, src2, ...]
|
|
52
|
+
"""
|
|
53
|
+
flat = []
|
|
54
|
+
for instr in self.instructions:
|
|
55
|
+
flat.extend(instr.to_list())
|
|
56
|
+
return flat
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# compiler/instructions.py
|
|
2
|
+
|
|
3
|
+
# opcodes
|
|
4
|
+
ADD = 0
|
|
5
|
+
SUB = 1
|
|
6
|
+
MUL = 2
|
|
7
|
+
DIV = 3
|
|
8
|
+
LOAD = 4 # load a constant value into a register
|
|
9
|
+
VAR = 5 # load a variable into a register
|
|
10
|
+
|
|
11
|
+
OP_NAMES = {ADD: "ADD", SUB: "SUB", MUL: "MUL",
|
|
12
|
+
DIV: "DIV", LOAD: "LOAD", VAR: "VAR"}
|
|
13
|
+
|
|
14
|
+
class Instruction:
|
|
15
|
+
def __init__(self, op, dest, src1=0, src2=0):
|
|
16
|
+
self.op = op # opcode int
|
|
17
|
+
self.dest = dest # destination register index
|
|
18
|
+
self.src1 = src1 # source register 1 (or value for LOAD)
|
|
19
|
+
self.src2 = src2 # source register 2
|
|
20
|
+
|
|
21
|
+
def __repr__(self):
|
|
22
|
+
name = OP_NAMES.get(self.op, "???")
|
|
23
|
+
return f"{name:4} r{self.dest} r{self.src1} r{self.src2}"
|
|
24
|
+
|
|
25
|
+
def to_list(self):
|
|
26
|
+
"""Serialize to flat list of 4 ints for GPU upload."""
|
|
27
|
+
return [self.op, self.dest, self.src1, self.src2]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# compiler/gpu/vulkan/backend.py
|
|
2
|
+
#
|
|
3
|
+
# GpuVulkan — Vulkan compute backend for the toy VM.
|
|
4
|
+
#
|
|
5
|
+
# Translates list[Instr] → SPIR-V opcodes → dispatches a Vulkan compute
|
|
6
|
+
# pipeline and reads back the result.
|
|
7
|
+
#
|
|
8
|
+
# Right now the SPIR-V emission and Vulkan dispatch are stubbed with clear
|
|
9
|
+
# TODO markers so the structure is in place and wires up cleanly to vm.py.
|
|
10
|
+
# Fill each stub in as you port your existing vulkan-llm Vulkan engine across.
|
|
11
|
+
#
|
|
12
|
+
# Instruction → SPIR-V opcode mapping (matches compiler.py comments):
|
|
13
|
+
# PUSH → OpConstant
|
|
14
|
+
# LOAD → OpLoad
|
|
15
|
+
# ADD → OpFAdd
|
|
16
|
+
# SUB → OpFSub
|
|
17
|
+
# MUL → OpFMul
|
|
18
|
+
# DIV → OpFDiv
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GpuOpengl:
|
|
25
|
+
"""Vulkan compute backend.
|
|
26
|
+
|
|
27
|
+
Accepts the same *program* forms as ``Cpu.run()``:
|
|
28
|
+
|
|
29
|
+
* ``list[Instr]`` — already-compiled bytecode, dispatch directly.
|
|
30
|
+
* ``str`` ending in ``.toyc`` — load compiled file, dispatch.
|
|
31
|
+
* ``str`` ending in ``.toy`` — lex + parse + compile in memory, dispatch.
|
|
32
|
+
|
|
33
|
+
The ``silent`` flag mirrors ``Cpu.run()`` — False by default so the result
|
|
34
|
+
is printed automatically, True to suppress printing when you capture the
|
|
35
|
+
return value yourself.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def run(program, env: dict = None, silent: bool = False) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Compile (if needed), emit SPIR-V, dispatch on GPU, return result.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
program : list[Instr] | str
|
|
46
|
+
Bytecode list, .toyc path, or .toy source path.
|
|
47
|
+
env : dict, optional
|
|
48
|
+
Variable bindings for LOAD instructions {name: value}.
|
|
49
|
+
silent : bool, optional
|
|
50
|
+
False (default) → result is printed before returning.
|
|
51
|
+
True → result returned quietly, no stdout output.
|
|
52
|
+
"""
|
|
53
|
+
# Resolve whatever the caller passed into a flat list[Instr].
|
|
54
|
+
# We reuse Cpu._resolve() so file loading / on-the-fly compilation
|
|
55
|
+
# lives in exactly one place.
|
|
56
|
+
#from compiler.vm import Cpu # local import to avoid circular dependency
|
|
57
|
+
#instructions = Cpu._resolve(program)
|
|
58
|
+
|
|
59
|
+
#spirv = GpuVulkan._emit_spirv(instructions, env or {})
|
|
60
|
+
#result = GpuVulkan._dispatch(spirv)
|
|
61
|
+
|
|
62
|
+
#if not silent:
|
|
63
|
+
#print(result)
|
|
64
|
+
|
|
65
|
+
#return result
|
|
66
|
+
|
|
67
|
+
# ── SPIR-V emission ───────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _emit_spirv(instructions: list, env: dict) -> bytes:
|
|
71
|
+
"""
|
|
72
|
+
Translate list[Instr] into a SPIR-V compute shader binary.
|
|
73
|
+
|
|
74
|
+
Each toy opcode maps to one SPIR-V op:
|
|
75
|
+
PUSH value → OpConstant (f32 constant)
|
|
76
|
+
LOAD name → OpLoad (load from uniform/push-constant)
|
|
77
|
+
ADD → OpFAdd
|
|
78
|
+
SUB → OpFSub
|
|
79
|
+
MUL → OpFMul
|
|
80
|
+
DIV → OpFDiv
|
|
81
|
+
|
|
82
|
+
Returns raw SPIR-V bytes ready to pass to vkCreateShaderModule.
|
|
83
|
+
"""
|
|
84
|
+
# TODO: build the SPIR-V word stream here.
|
|
85
|
+
#
|
|
86
|
+
# Suggested approach:
|
|
87
|
+
# 1. Walk `instructions` and track a virtual register stack.
|
|
88
|
+
# 2. Emit OpConstant / OpLoad for PUSH / LOAD.
|
|
89
|
+
# 3. Emit OpFAdd / OpFSub / OpFMul / OpFDiv for arithmetic ops,
|
|
90
|
+
# consuming the top two registers and producing a new one.
|
|
91
|
+
# 4. Emit OpStore to write the final register to the output buffer.
|
|
92
|
+
# 5. Assemble the header (magic, version, bound, schema) and
|
|
93
|
+
# return the whole thing as bytes.
|
|
94
|
+
#
|
|
95
|
+
# Libraries that help:
|
|
96
|
+
# • pyspirv — pure-Python SPIR-V assembler
|
|
97
|
+
# • spirv-cross (via ctypes) — if you prefer C bindings
|
|
98
|
+
# • hand-roll with struct.pack('<I', word) — minimal dependency
|
|
99
|
+
|
|
100
|
+
raise NotImplementedError(
|
|
101
|
+
"GpuVulkan._emit_spirv() is not yet implemented.\n"
|
|
102
|
+
"Fill in the SPIR-V word stream in compiler/gpu/vulkan/backend.py."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ── Vulkan dispatch ───────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _dispatch(spirv: bytes) -> Any:
|
|
109
|
+
"""
|
|
110
|
+
Create a Vulkan compute pipeline from *spirv*, dispatch it, and
|
|
111
|
+
read back the scalar result from the output buffer.
|
|
112
|
+
|
|
113
|
+
Steps (mirrors a typical compute dispatch):
|
|
114
|
+
1. vkCreateShaderModule(spirv)
|
|
115
|
+
2. vkCreateComputePipeline(shader)
|
|
116
|
+
3. Allocate input / output VkBuffers, upload constants / env vars.
|
|
117
|
+
4. vkCmdDispatch(1, 1, 1) ← single workgroup for scalar ops
|
|
118
|
+
5. vkMapMemory → read f32 result → vkUnmapMemory
|
|
119
|
+
6. Teardown (pipeline, shader module, buffers).
|
|
120
|
+
|
|
121
|
+
Returns the scalar result as a Python float (or int).
|
|
122
|
+
"""
|
|
123
|
+
# TODO: wire up your existing Vulkan engine from spy1345a/vulkan-llm.
|
|
124
|
+
#
|
|
125
|
+
# If you're using ctypes bindings to your C++ engine, the call will
|
|
126
|
+
# look roughly like:
|
|
127
|
+
#
|
|
128
|
+
# from compiler.gpu.vulkan._bindings import vulkan_lib
|
|
129
|
+
# result_buf = (ctypes.c_float * 1)()
|
|
130
|
+
# vulkan_lib.dispatch_spirv(spirv, len(spirv), result_buf)
|
|
131
|
+
# return result_buf[0]
|
|
132
|
+
|
|
133
|
+
raise NotImplementedError(
|
|
134
|
+
"GpuVulkan._dispatch() is not yet implemented.\n"
|
|
135
|
+
"Wire up the Vulkan compute dispatch in compiler/gpu/vulkan/backend.py."
|
|
136
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# compiler/gpu/vulkan/backend.py
|
|
2
|
+
#
|
|
3
|
+
# GpuVulkan — Vulkan compute backend for the toy VM.
|
|
4
|
+
#
|
|
5
|
+
# Translates list[Instr] → SPIR-V opcodes → dispatches a Vulkan compute
|
|
6
|
+
# pipeline and reads back the result.
|
|
7
|
+
#
|
|
8
|
+
# Right now the SPIR-V emission and Vulkan dispatch are stubbed with clear
|
|
9
|
+
# TODO markers so the structure is in place and wires up cleanly to vm.py.
|
|
10
|
+
# Fill each stub in as you port your existing vulkan-llm Vulkan engine across.
|
|
11
|
+
#
|
|
12
|
+
# Instruction → SPIR-V opcode mapping (matches compiler.py comments):
|
|
13
|
+
# PUSH → OpConstant
|
|
14
|
+
# LOAD → OpLoad
|
|
15
|
+
# ADD → OpFAdd
|
|
16
|
+
# SUB → OpFSub
|
|
17
|
+
# MUL → OpFMul
|
|
18
|
+
# DIV → OpFDiv
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GpuVulkan:
|
|
25
|
+
"""Vulkan compute backend.
|
|
26
|
+
|
|
27
|
+
Accepts the same *program* forms as ``Cpu.run()``:
|
|
28
|
+
|
|
29
|
+
* ``list[Instr]`` — already-compiled bytecode, dispatch directly.
|
|
30
|
+
* ``str`` ending in ``.toyc`` — load compiled file, dispatch.
|
|
31
|
+
* ``str`` ending in ``.toy`` — lex + parse + compile in memory, dispatch.
|
|
32
|
+
|
|
33
|
+
The ``silent`` flag mirrors ``Cpu.run()`` — False by default so the result
|
|
34
|
+
is printed automatically, True to suppress printing when you capture the
|
|
35
|
+
return value yourself.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def run(program, env: dict = None, silent: bool = False) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Compile (if needed), emit SPIR-V, dispatch on GPU, return result.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
program : list[Instr] | str
|
|
46
|
+
Bytecode list, .toyc path, or .toy source path.
|
|
47
|
+
env : dict, optional
|
|
48
|
+
Variable bindings for LOAD instructions {name: value}.
|
|
49
|
+
silent : bool, optional
|
|
50
|
+
False (default) → result is printed before returning.
|
|
51
|
+
True → result returned quietly, no stdout output.
|
|
52
|
+
"""
|
|
53
|
+
# Resolve whatever the caller passed into a flat list[Instr].
|
|
54
|
+
# We reuse Cpu._resolve() so file loading / on-the-fly compilation
|
|
55
|
+
# lives in exactly one place.
|
|
56
|
+
from compiler.vm import Cpu # local import to avoid circular dependency
|
|
57
|
+
instructions = Cpu._resolve(program)
|
|
58
|
+
|
|
59
|
+
spirv = GpuVulkan._emit_spirv(instructions, env or {})
|
|
60
|
+
result = GpuVulkan._dispatch(spirv)
|
|
61
|
+
|
|
62
|
+
if not silent:
|
|
63
|
+
print(result)
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
# ── SPIR-V emission ───────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _emit_spirv(instructions: list, env: dict) -> bytes:
|
|
71
|
+
"""
|
|
72
|
+
Translate list[Instr] into a SPIR-V compute shader binary.
|
|
73
|
+
|
|
74
|
+
Each toy opcode maps to one SPIR-V op:
|
|
75
|
+
PUSH value → OpConstant (f32 constant)
|
|
76
|
+
LOAD name → OpLoad (load from uniform/push-constant)
|
|
77
|
+
ADD → OpFAdd
|
|
78
|
+
SUB → OpFSub
|
|
79
|
+
MUL → OpFMul
|
|
80
|
+
DIV → OpFDiv
|
|
81
|
+
|
|
82
|
+
Returns raw SPIR-V bytes ready to pass to vkCreateShaderModule.
|
|
83
|
+
"""
|
|
84
|
+
# TODO: build the SPIR-V word stream here.
|
|
85
|
+
#
|
|
86
|
+
# Suggested approach:
|
|
87
|
+
# 1. Walk `instructions` and track a virtual register stack.
|
|
88
|
+
# 2. Emit OpConstant / OpLoad for PUSH / LOAD.
|
|
89
|
+
# 3. Emit OpFAdd / OpFSub / OpFMul / OpFDiv for arithmetic ops,
|
|
90
|
+
# consuming the top two registers and producing a new one.
|
|
91
|
+
# 4. Emit OpStore to write the final register to the output buffer.
|
|
92
|
+
# 5. Assemble the header (magic, version, bound, schema) and
|
|
93
|
+
# return the whole thing as bytes.
|
|
94
|
+
#
|
|
95
|
+
# Libraries that help:
|
|
96
|
+
# • pyspirv — pure-Python SPIR-V assembler
|
|
97
|
+
# • spirv-cross (via ctypes) — if you prefer C bindings
|
|
98
|
+
# • hand-roll with struct.pack('<I', word) — minimal dependency
|
|
99
|
+
|
|
100
|
+
raise NotImplementedError(
|
|
101
|
+
"GpuVulkan._emit_spirv() is not yet implemented.\n"
|
|
102
|
+
"Fill in the SPIR-V word stream in compiler/gpu/vulkan/backend.py."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ── Vulkan dispatch ───────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _dispatch(spirv: bytes) -> Any:
|
|
109
|
+
"""
|
|
110
|
+
Create a Vulkan compute pipeline from *spirv*, dispatch it, and
|
|
111
|
+
read back the scalar result from the output buffer.
|
|
112
|
+
|
|
113
|
+
Steps (mirrors a typical compute dispatch):
|
|
114
|
+
1. vkCreateShaderModule(spirv)
|
|
115
|
+
2. vkCreateComputePipeline(shader)
|
|
116
|
+
3. Allocate input / output VkBuffers, upload constants / env vars.
|
|
117
|
+
4. vkCmdDispatch(1, 1, 1) ← single workgroup for scalar ops
|
|
118
|
+
5. vkMapMemory → read f32 result → vkUnmapMemory
|
|
119
|
+
6. Teardown (pipeline, shader module, buffers).
|
|
120
|
+
|
|
121
|
+
Returns the scalar result as a Python float (or int).
|
|
122
|
+
"""
|
|
123
|
+
# TODO: wire up your existing Vulkan engine from spy1345a/vulkan-llm.
|
|
124
|
+
#
|
|
125
|
+
# If you're using ctypes bindings to your C++ engine, the call will
|
|
126
|
+
# look roughly like:
|
|
127
|
+
#
|
|
128
|
+
# from compiler.gpu.vulkan._bindings import vulkan_lib
|
|
129
|
+
# result_buf = (ctypes.c_float * 1)()
|
|
130
|
+
# vulkan_lib.dispatch_spirv(spirv, len(spirv), result_buf)
|
|
131
|
+
# return result_buf[0]
|
|
132
|
+
|
|
133
|
+
raise NotImplementedError(
|
|
134
|
+
"GpuVulkan._dispatch() is not yet implemented.\n"
|
|
135
|
+
"Wire up the Vulkan compute dispatch in compiler/gpu/vulkan/backend.py."
|
|
136
|
+
)
|
toyc-0.1.0/toyc/lexer.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
NUMBER = "NUMBER"
|
|
2
|
+
IDENT = "IDENT"
|
|
3
|
+
PLUS = "PLUS"
|
|
4
|
+
MINUS = "MINUS"
|
|
5
|
+
STAR = "STAR"
|
|
6
|
+
SLASH = "SLASH"
|
|
7
|
+
LPAREN = "LPAREN"
|
|
8
|
+
RPAREN = "RPAREN"
|
|
9
|
+
EOF = "EOF"
|
|
10
|
+
|
|
11
|
+
class Token:
|
|
12
|
+
def __init__(self, type, value):
|
|
13
|
+
self.type = type
|
|
14
|
+
self.value = value
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return f"Token({self.type}, {self.value!r})"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Lexer:
|
|
21
|
+
KEYWORDS = {"kernel", "return"}
|
|
22
|
+
|
|
23
|
+
def __init__(self, text):
|
|
24
|
+
self.text = text
|
|
25
|
+
self.pos = 0
|
|
26
|
+
self.current = text[0] if text else None
|
|
27
|
+
|
|
28
|
+
# ── public API ──────────────────────────────────────────────────────────
|
|
29
|
+
@classmethod
|
|
30
|
+
def tokenize(cls, text):
|
|
31
|
+
"""Lex *text* and return a list of Tokens (including the EOF token)."""
|
|
32
|
+
return cls(text)._run()
|
|
33
|
+
|
|
34
|
+
# ── internal helpers ─────────────────────────────────────────────────────
|
|
35
|
+
def _run(self):
|
|
36
|
+
tokens = []
|
|
37
|
+
while True:
|
|
38
|
+
tok = self._next_token()
|
|
39
|
+
tokens.append(tok)
|
|
40
|
+
if tok.type == EOF:
|
|
41
|
+
break
|
|
42
|
+
return tokens
|
|
43
|
+
|
|
44
|
+
def _advance(self):
|
|
45
|
+
self.pos += 1
|
|
46
|
+
self.current = self.text[self.pos] if self.pos < len(self.text) else None
|
|
47
|
+
|
|
48
|
+
def _skip_whitespace(self):
|
|
49
|
+
while self.current and self.current.isspace():
|
|
50
|
+
self._advance()
|
|
51
|
+
|
|
52
|
+
def _read_number(self):
|
|
53
|
+
result = ""
|
|
54
|
+
while self.current and (self.current.isdigit() or self.current == "."):
|
|
55
|
+
result += self.current
|
|
56
|
+
self._advance()
|
|
57
|
+
return Token(NUMBER, float(result) if "." in result else int(result))
|
|
58
|
+
|
|
59
|
+
def _read_ident(self):
|
|
60
|
+
result = ""
|
|
61
|
+
while self.current and (self.current.isalnum() or self.current == "_"):
|
|
62
|
+
result += self.current
|
|
63
|
+
self._advance()
|
|
64
|
+
tok_type = result.upper() if result in self.KEYWORDS else IDENT
|
|
65
|
+
return Token(tok_type, result)
|
|
66
|
+
|
|
67
|
+
_OPS = {"+": PLUS, "-": MINUS, "*": STAR, "/": SLASH,
|
|
68
|
+
"(": LPAREN, ")": RPAREN}
|
|
69
|
+
|
|
70
|
+
def _next_token(self):
|
|
71
|
+
while self.current:
|
|
72
|
+
if self.current.isspace():
|
|
73
|
+
self._skip_whitespace()
|
|
74
|
+
continue
|
|
75
|
+
if self.current.isdigit():
|
|
76
|
+
return self._read_number()
|
|
77
|
+
if self.current.isalpha() or self.current == "_":
|
|
78
|
+
return self._read_ident()
|
|
79
|
+
if self.current in self._OPS:
|
|
80
|
+
tok = Token(self._OPS[self.current], self.current)
|
|
81
|
+
self._advance()
|
|
82
|
+
return tok
|
|
83
|
+
raise SyntaxError(f"Unknown character: {self.current!r}")
|
|
84
|
+
return Token(EOF, None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── test ─────────────────────────────────────────────────────────────────────
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
tests = [
|
|
90
|
+
"a + b * 2",
|
|
91
|
+
"(a + b) * (c - 3)",
|
|
92
|
+
"10.5 / x + y * 2",
|
|
93
|
+
"kernel",
|
|
94
|
+
]
|
|
95
|
+
for src in tests:
|
|
96
|
+
print(f"\nInput: {src!r}")
|
|
97
|
+
for tok in Lexer.tokenize(src): # ← clean API
|
|
98
|
+
print(f" {tok}")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# compiler/parser.py
|
|
2
|
+
|
|
3
|
+
from .lexer import NUMBER, IDENT, PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN, EOF
|
|
4
|
+
from .ast_nodes import Number, Var, BinOp
|
|
5
|
+
|
|
6
|
+
class Parser:
|
|
7
|
+
def __init__(self, tokens):
|
|
8
|
+
self.tokens = tokens
|
|
9
|
+
self.pos = 0
|
|
10
|
+
self.current = tokens[0]
|
|
11
|
+
|
|
12
|
+
# ── public API ───────────────────────────────────────────────────────────
|
|
13
|
+
@classmethod
|
|
14
|
+
def parse(cls, tokens):
|
|
15
|
+
"""Parse a token list and return the AST root node."""
|
|
16
|
+
return cls(tokens)._run()
|
|
17
|
+
|
|
18
|
+
# ── internal helpers ─────────────────────────────────────────────────────
|
|
19
|
+
def _run(self):
|
|
20
|
+
node = self._expr()
|
|
21
|
+
self._eat(EOF)
|
|
22
|
+
return node
|
|
23
|
+
|
|
24
|
+
def _advance(self):
|
|
25
|
+
self.pos += 1
|
|
26
|
+
if self.pos < len(self.tokens):
|
|
27
|
+
self.current = self.tokens[self.pos]
|
|
28
|
+
|
|
29
|
+
def _eat(self, type):
|
|
30
|
+
if self.current.type == type:
|
|
31
|
+
tok = self.current
|
|
32
|
+
self._advance()
|
|
33
|
+
return tok
|
|
34
|
+
raise SyntaxError(
|
|
35
|
+
f"Expected {type}, got {self.current.type!r} ({self.current.value!r})"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# ── precedence level 3 (highest): numbers, vars, parenthesised expressions
|
|
39
|
+
def _factor(self):
|
|
40
|
+
tok = self.current
|
|
41
|
+
|
|
42
|
+
if tok.type == NUMBER:
|
|
43
|
+
self._advance()
|
|
44
|
+
return Number(tok.value)
|
|
45
|
+
|
|
46
|
+
if tok.type == IDENT:
|
|
47
|
+
self._advance()
|
|
48
|
+
return Var(tok.value)
|
|
49
|
+
|
|
50
|
+
if tok.type == LPAREN:
|
|
51
|
+
self._advance()
|
|
52
|
+
node = self._expr()
|
|
53
|
+
self._eat(RPAREN)
|
|
54
|
+
return node
|
|
55
|
+
|
|
56
|
+
raise SyntaxError(f"Unexpected token: {tok!r}")
|
|
57
|
+
|
|
58
|
+
# ── precedence level 2: * and /
|
|
59
|
+
def _term(self):
|
|
60
|
+
node = self._factor()
|
|
61
|
+
|
|
62
|
+
while self.current.type in (STAR, SLASH):
|
|
63
|
+
op = self.current.value
|
|
64
|
+
self._advance()
|
|
65
|
+
node = BinOp(op, node, self._factor())
|
|
66
|
+
|
|
67
|
+
return node
|
|
68
|
+
|
|
69
|
+
# ── precedence level 1 (lowest): + and -
|
|
70
|
+
def _expr(self):
|
|
71
|
+
node = self._term()
|
|
72
|
+
|
|
73
|
+
while self.current.type in (PLUS, MINUS):
|
|
74
|
+
op = self.current.value
|
|
75
|
+
self._advance()
|
|
76
|
+
node = BinOp(op, node, self._term())
|
|
77
|
+
|
|
78
|
+
return node
|
toyc-0.1.0/toyc/vm.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# compiler/vm.py
|
|
2
|
+
#
|
|
3
|
+
# Responsible for:
|
|
4
|
+
# • The Cpu class (execute bytecode on the host CPU)
|
|
5
|
+
# • Input resolution (list[Instr], .toyc path, .toy path) — shared by
|
|
6
|
+
# all backends so file loading lives in one place
|
|
7
|
+
# • Re-exporting GpuVulkan so callers can import both backends from here:
|
|
8
|
+
#
|
|
9
|
+
# from compiler.vm import Cpu, GpuVulkan
|
|
10
|
+
#
|
|
11
|
+
# Calling styles (identical interface for both backends):
|
|
12
|
+
#
|
|
13
|
+
# Cpu.run("script.toy") → lex+parse+compile in memory, run on CPU
|
|
14
|
+
# Cpu.run("script.toyc") → load compiled file, run on CPU
|
|
15
|
+
# Cpu.run(bytecode_list) → run in memory directly
|
|
16
|
+
#
|
|
17
|
+
# GpuVulkan.run("script.toy") → same input forms, dispatches on GPU
|
|
18
|
+
# GpuVulkan.run("script.toyc") → load compiled file, dispatch on GPU
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from .compiler import (
|
|
24
|
+
Instr,
|
|
25
|
+
Compiler,
|
|
26
|
+
read_bytecode,
|
|
27
|
+
is_compiled_bytecode,
|
|
28
|
+
)
|
|
29
|
+
from .lexer import Lexer
|
|
30
|
+
from .parser import Parser
|
|
31
|
+
|
|
32
|
+
# Re-export so `from compiler.vm import GpuVulkan` works
|
|
33
|
+
from .gpu.vulkan import GpuVulkan
|
|
34
|
+
|
|
35
|
+
from .gpu.opengl import GpuOpengl
|
|
36
|
+
|
|
37
|
+
__all__ = ["Cpu", "GpuVulkan"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Cpu: execute bytecode ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
class Cpu:
|
|
43
|
+
"""Stack-based virtual machine.
|
|
44
|
+
|
|
45
|
+
``Cpu.run()`` accepts three forms of *program*:
|
|
46
|
+
|
|
47
|
+
1. ``list[Instr]``
|
|
48
|
+
Already-compiled bytecode; executed directly, no I/O.
|
|
49
|
+
|
|
50
|
+
2. ``str`` ending in ``.toyc``
|
|
51
|
+
Load the compiled file from disk and execute it.
|
|
52
|
+
No lexing, parsing, or compiling happens.
|
|
53
|
+
|
|
54
|
+
3. ``str`` ending in ``.toy``
|
|
55
|
+
Read the source file, lex it, parse it, compile it in memory,
|
|
56
|
+
then execute. Nothing is written to disk.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def run(program, env: dict = None, silent: bool = False) -> Any:
|
|
61
|
+
"""
|
|
62
|
+
Execute *program* and return the top-of-stack result.
|
|
63
|
+
|
|
64
|
+
By default the result is printed to stdout automatically.
|
|
65
|
+
Pass ``silent=True`` when you are capturing the return value yourself
|
|
66
|
+
and do not want it printed as well.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
program : list[Instr] | str
|
|
71
|
+
Bytecode list, a .toyc compiled file path, or a .toy source path.
|
|
72
|
+
env : dict, optional
|
|
73
|
+
Variable bindings available to LOAD instructions {name: value}.
|
|
74
|
+
silent : bool, optional
|
|
75
|
+
False (default) → result is printed before returning.
|
|
76
|
+
True → result is returned quietly, no stdout output.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
Cpu.run("out.toyc") # prints result automatically
|
|
81
|
+
value = Cpu.run("out.toyc", silent=True) # capture only, no print
|
|
82
|
+
"""
|
|
83
|
+
instructions = Cpu._resolve(program)
|
|
84
|
+
result = Cpu._execute(instructions, env or {})
|
|
85
|
+
|
|
86
|
+
if not silent:
|
|
87
|
+
print(result)
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
# ── input resolution ──────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _resolve(program) -> list:
|
|
95
|
+
"""Return a list[Instr] no matter what form *program* arrives in."""
|
|
96
|
+
|
|
97
|
+
# ① Already compiled in memory — use directly
|
|
98
|
+
if isinstance(program, list):
|
|
99
|
+
return program
|
|
100
|
+
|
|
101
|
+
if not isinstance(program, str):
|
|
102
|
+
raise TypeError(
|
|
103
|
+
f"Cpu.run() expects a list[Instr] or a file path str, "
|
|
104
|
+
f"got {type(program).__name__}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
path = program
|
|
108
|
+
|
|
109
|
+
# ② .toyc path — load compiled bytecode, skip all compile steps
|
|
110
|
+
if path.endswith(".toyc"):
|
|
111
|
+
if not os.path.isfile(path):
|
|
112
|
+
raise FileNotFoundError(f"Bytecode file not found: {path!r}")
|
|
113
|
+
return read_bytecode(path)
|
|
114
|
+
|
|
115
|
+
# ③ .toy source path — lex → parse → compile in memory, no disk write
|
|
116
|
+
if path.endswith(".toy"):
|
|
117
|
+
if not os.path.isfile(path):
|
|
118
|
+
raise FileNotFoundError(f"Source file not found: {path!r}")
|
|
119
|
+
return Cpu._compile_toy(path)
|
|
120
|
+
|
|
121
|
+
# ④ Unknown extension — peek at magic bytes as a last resort
|
|
122
|
+
if os.path.isfile(path) and is_compiled_bytecode(path):
|
|
123
|
+
return read_bytecode(path)
|
|
124
|
+
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Cannot load {path!r}: expected a .toy source or .toyc bytecode file"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _compile_toy(toy_path: str) -> list:
|
|
131
|
+
"""
|
|
132
|
+
Lex → parse → compile a .toy source file entirely in memory.
|
|
133
|
+
Nothing is written to disk (path=None passed to Compiler.compile).
|
|
134
|
+
"""
|
|
135
|
+
with open(toy_path, "r", encoding="utf-8") as f:
|
|
136
|
+
source = f.read()
|
|
137
|
+
|
|
138
|
+
tokens = Lexer.tokenize(source)
|
|
139
|
+
ast = Parser.parse(tokens)
|
|
140
|
+
# path=None → memory only, no .toyc file created
|
|
141
|
+
return Compiler.compile(ast, path=None)
|
|
142
|
+
|
|
143
|
+
# ── core interpreter ──────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _execute(instructions: list, env: dict) -> Any:
|
|
147
|
+
stack: list = []
|
|
148
|
+
|
|
149
|
+
for instr in instructions:
|
|
150
|
+
if instr.op == "PUSH":
|
|
151
|
+
stack.append(instr.arg)
|
|
152
|
+
|
|
153
|
+
elif instr.op == "LOAD":
|
|
154
|
+
if instr.arg not in env:
|
|
155
|
+
raise NameError(f"Undefined variable: {instr.arg!r}")
|
|
156
|
+
stack.append(env[instr.arg])
|
|
157
|
+
|
|
158
|
+
elif instr.op == "ADD":
|
|
159
|
+
b, a = stack.pop(), stack.pop()
|
|
160
|
+
stack.append(a + b)
|
|
161
|
+
|
|
162
|
+
elif instr.op == "SUB":
|
|
163
|
+
b, a = stack.pop(), stack.pop()
|
|
164
|
+
stack.append(a - b)
|
|
165
|
+
|
|
166
|
+
elif instr.op == "MUL":
|
|
167
|
+
b, a = stack.pop(), stack.pop()
|
|
168
|
+
stack.append(a * b)
|
|
169
|
+
|
|
170
|
+
elif instr.op == "DIV":
|
|
171
|
+
b, a = stack.pop(), stack.pop()
|
|
172
|
+
if b == 0:
|
|
173
|
+
raise ZeroDivisionError("Division by zero in VM")
|
|
174
|
+
stack.append(a / b)
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
raise RuntimeError(f"Unknown opcode: {instr.op!r}")
|
|
178
|
+
|
|
179
|
+
if not stack:
|
|
180
|
+
raise RuntimeError("Execution finished with an empty stack")
|
|
181
|
+
|
|
182
|
+
return stack[-1] # top of stack is the result
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toyc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A toy compiler with GPU backends (Vulkan + OpenGL)
|
|
5
|
+
Author: spy1345a
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 spy1345a
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
11
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
12
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
14
|
+
following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
17
|
+
portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
20
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
21
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
22
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
23
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
Project-URL: Homepage, https://github.com/spy1345a/toyc-repo
|
|
26
|
+
Project-URL: Repository, https://github.com/spy1345a/toyc-repo
|
|
27
|
+
Keywords: compiler,gpu,vulkan,opengl,toy
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
32
|
+
Requires-Python: >=3.10
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
Requires-Dist: pyopengl>=3.1
|
|
35
|
+
Requires-Dist: vulkan>=1.3.275.1
|
|
36
|
+
Provides-Extra: vulkan
|
|
37
|
+
Requires-Dist: vulkan; extra == "vulkan"
|
|
38
|
+
Provides-Extra: opengl
|
|
39
|
+
Requires-Dist: PyOpenGL; extra == "opengl"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
toyc/LICENSE
|
|
4
|
+
toyc/__init__.py
|
|
5
|
+
toyc/ast_nodes.py
|
|
6
|
+
toyc/compiler.py
|
|
7
|
+
toyc/evaluator.py
|
|
8
|
+
toyc/lexer.py
|
|
9
|
+
toyc/parser.py
|
|
10
|
+
toyc/vm.py
|
|
11
|
+
toyc.egg-info/PKG-INFO
|
|
12
|
+
toyc.egg-info/SOURCES.txt
|
|
13
|
+
toyc.egg-info/dependency_links.txt
|
|
14
|
+
toyc.egg-info/requires.txt
|
|
15
|
+
toyc.egg-info/top_level.txt
|
|
16
|
+
toyc/gpu/__init__.py
|
|
17
|
+
toyc/gpu/flattener.py
|
|
18
|
+
toyc/gpu/instructions.py
|
|
19
|
+
toyc/gpu/opengl/__init__.py
|
|
20
|
+
toyc/gpu/opengl/backend.py
|
|
21
|
+
toyc/gpu/vulkan/__init__.py
|
|
22
|
+
toyc/gpu/vulkan/backend.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
toyc
|