Repository navigation
Expand file tree
/
Copy pathmain.py
More file actions
248 lines (201 loc) · 8.91 KB
/
Copy pathmain.py
File metadata and controls
248 lines (201 loc) · 8.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python3
"""
voidc.main
==========
The `voidc` command-line driver. Wires together every compiler stage:
source.void
-> Lexer (lexer.py) -> tokens
-> Parser (parser.py) -> AST (ast_nodes.py)
-> Analyzer (semantic.py) -> checked AST + SymbolTable
-> CodeGen (codegen_c.py) -> C source
-> system cc (subprocess) -> native executable
Usage:
voidc build <file.void> [-o out] [--emit-c] [--keep-c] [-O0|-O1|-O2]
voidc run <file.void> [-- program-args...]
voidc check <file.void> # parse + semantic analysis only, no codegen
"""
import sys
import os
import argparse
import subprocess
import shutil
from typing import List
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from voidc.lexer import Lexer
from voidc.parser import Parser
from voidc.semantic import Analyzer
from voidc.codegen_c import CodeGen
from voidc.errors import Diagnostics, VoidCompileError
from voidc import ast_nodes as A
VOIDC_VERSION = "0.1.0-bootstrap"
RUNTIME_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "runtime"))
# Visibility levels that make a top-level declaration importable from another file.
_IMPORTABLE_VIS = ("public", "protected")
def _is_file_import(path: str) -> bool:
"""Distinguishes a real file import (`import "./util.void";` or
`import "../lib/math.void";`) from a `std::`-style logical module path
(`import std::io;`), which the bootstrap compiler doesn't resolve to a
file on disk yet -- it's accepted but treated as a no-op with a warning,
since the standard library modules themselves aren't written yet."""
return path.startswith("./") or path.startswith("../") or path.endswith(".void")
def _decl_name(decl) -> str:
return getattr(decl, "name", "")
def _is_exported(decl) -> bool:
vis = getattr(decl, "visibility", "private")
return vis in _IMPORTABLE_VIS
def load_program(entry_path: str, diags: Diagnostics) -> A.Program:
"""Stage 0(.5): whole-program assembly via `import` resolution.
Recursively follows every relative-file `import "./x.void";` starting
from the entry file, parsing each file at most once (tracked by
absolute path, so import cycles terminate instead of looping forever),
and merges their declarations into a single combined `Program` that the
rest of the pipeline (semantic analysis, codegen) treats exactly as it
would a single file.
Visibility rule: the entry file's declarations are all available
(as if written inline); an *imported* file only contributes
declarations marked `export`/`public`/`protected` -- anything left at
the default `private` visibility stays local to the file that defined
it, same as Rust's `pub`/private split or Go's capitalized exports.
`import std::whatever;`-style logical module paths are accepted
syntactically but currently resolve to nothing (the standard library
isn't implemented as real files yet); a warning is emitted so the gap
is visible rather than silently wrong.
"""
entry_abs = os.path.abspath(entry_path)
visited: dict = {} # abs path -> A.Program
order: List[str] = []
def parse_file(abs_path: str) -> A.Program:
with open(abs_path, "r", encoding="utf-8") as f:
source = f.read()
diags.add_source(abs_path, source)
old_filename = diags.filename
diags.filename = abs_path
try:
lexer = Lexer(source, abs_path, diags)
tokens = lexer.tokenize()
parser = Parser(tokens, diags)
prog = parser.parse_program()
finally:
diags.filename = old_filename
return prog
def visit(abs_path: str, is_entry: bool):
if abs_path in visited:
return
prog = parse_file(abs_path)
visited[abs_path] = prog
order.append(abs_path)
base_dir = os.path.dirname(abs_path)
for imp in prog.imports:
if _is_file_import(imp.path):
target = os.path.normpath(os.path.join(base_dir, imp.path))
if not os.path.isfile(target):
diags.error(f"cannot find imported file '{imp.path}' (looked for {target})",
imp.line, imp.col, code="E0600", file=abs_path)
continue
visit(target, is_entry=False)
else:
diags.warn(f"module '{imp.path}' is not resolved yet (standard library "
f"modules aren't implemented as real files in this bootstrap compiler)",
imp.line, imp.col, code="W0001", file=abs_path)
visit(entry_abs, is_entry=True)
diags.raise_if_errors()
combined = A.Program(line=1, col=1)
for abs_path in order:
prog = visited[abs_path]
is_entry = (abs_path == entry_abs)
combined.imports.extend(prog.imports)
for decl in prog.decls:
if is_entry or _is_exported(decl):
combined.decls.append(decl)
return combined
def compile_to_c(path: str) -> tuple:
"""Runs (multi-file import resolution ->) lexer -> parser (per file) ->
semantic analysis -> codegen on the merged program. Returns (c_source, diags)."""
diags = Diagnostics(path)
program = load_program(path, diags)
diags.raise_if_errors()
analyzer = Analyzer(program, diags)
symtab = analyzer.analyze()
diags.raise_if_errors()
codegen = CodeGen(program, symtab, diags)
c_source = codegen.generate()
diags.raise_if_errors()
return c_source, diags
def find_cc() -> str:
for candidate in ("cc", "gcc", "clang"):
path = shutil.which(candidate)
if path:
return candidate
raise RuntimeError("No C compiler found on PATH (looked for cc, gcc, clang). "
"Void compiles to C and needs a system C toolchain to finish producing a native binary.")
def cmd_build(args):
src_path = args.file
out_path = args.output or os.path.splitext(os.path.basename(src_path))[0]
try:
c_source, diags = compile_to_c(src_path)
except VoidCompileError as e:
print(str(e), file=sys.stderr)
return 1
if diags.warning_count():
print(diags.dump(), file=sys.stderr)
c_path = out_path + ".c"
with open(c_path, "w") as f:
f.write(c_source)
if args.emit_c:
print(c_path)
return 0
cc = find_cc()
cmd = [cc, c_path, "-I", RUNTIME_DIR, "-o", out_path, "-lpthread", f"-O{args.opt}"]
if args.extra_cflags:
cmd += args.extra_cflags.split()
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("error: backend C compiler failed:", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return 1
if not args.keep_c:
os.remove(c_path)
print(out_path)
return 0
def cmd_run(args):
out_path = "/tmp/voidc_run_" + os.path.splitext(os.path.basename(args.file))[0]
build_ns = argparse.Namespace(file=args.file, output=out_path, emit_c=False,
keep_c=False, opt="1", extra_cflags=None)
rc = cmd_build(build_ns)
if rc != 0:
return rc
result = subprocess.run([out_path] + (args.program_args or []))
return result.returncode
def cmd_check(args):
try:
_, diags = compile_to_c(args.file)
except VoidCompileError as e:
print(str(e), file=sys.stderr)
return 1
if diags.items:
print(diags.dump())
print(f"check passed: {diags.error_count()} errors, {diags.warning_count()} warnings")
return 0
def main():
ap = argparse.ArgumentParser(prog="voidc", description="The Void compiler (bootstrap, Python-hosted)")
ap.add_argument("--version", action="version", version=f"voidc {VOIDC_VERSION}")
sub = ap.add_subparsers(dest="command", required=True)
p_build = sub.add_parser("build", help="compile a .void file to a native executable")
p_build.add_argument("file")
p_build.add_argument("-o", "--output", default=None)
p_build.add_argument("--emit-c", action="store_true", help="stop after emitting C source")
p_build.add_argument("--keep-c", action="store_true", help="keep the intermediate .c file")
p_build.add_argument("-O", "--opt", default="1", choices=["0", "1", "2", "3"])
p_build.add_argument("--cflags", dest="extra_cflags", default=None)
p_build.set_defaults(func=cmd_build)
p_run = sub.add_parser("run", help="compile and immediately execute a .void file")
p_run.add_argument("file")
p_run.add_argument("program_args", nargs=argparse.REMAINDER)
p_run.set_defaults(func=cmd_run)
p_check = sub.add_parser("check", help="parse + type-check without generating code")
p_check.add_argument("file")
p_check.set_defaults(func=cmd_check)
args = ap.parse_args()
sys.exit(args.func(args))
if __name__ == "__main__":
main()