Skip to content

Commit ebc8ac5

Browse files
committed
finish wasm backend
1 parent 39615c6 commit ebc8ac5

File tree

5 files changed

+38
-41
lines changed

5 files changed

+38
-41
lines changed

README.md

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# chocopy-python-compiler
22

3-
Ahead-of-time compiler for [Chocopy](https://chocopy.org/), a subset of Python 3.6 with type annotations static type checking.
3+
Ahead-of-time compiler for [Chocopy](https://chocopy.org/), a subset of Python 3.6 with type annotations and static type checking.
44

55
Chocopy is used in compiler courses at several universities. This project has no relation to those courses, and is purely for my own learning/practice/fun.
66

@@ -13,12 +13,13 @@ Progress is documented on my [blog](https://yangdanny97.github.io/blog/):
1313

1414
This compiler is written entirely in Python. Since Chocopy is itself a subset of Python, lexing and parsing can be entirely handled by Python's `ast` module.
1515

16-
This compiler matches the functionality of the first 2 passes (parsing & typechecking) Chocopy's reference compiler implementation, and outputs the AST in a JSON format that is compatible with the reference implementation's backend. That means that you can parse and typecheck the Chocopy file with this compiler, then use the reference implementation's backend to handle assembly code generation.
16+
The frontend of this compiler matches the functionality of the first 2 passes (parsing & typechecking) Chocopy's reference compiler implementation, and outputs the AST in a JSON format that is compatible with the reference implementation's backend. That means that you can parse and typecheck the Chocopy file with this compiler, then use the reference implementation's backend to handle assembly code generation.
1717

18-
Additionally, this compiler contains 2 backends not found in the reference implementation:
18+
This compiler contains multiple backends not found in the reference implementation:
1919
- Untyped Python 3 source code
2020
- JVM bytecode, formatted for the Krakatau assembler
2121
- CIL bytecode, formatted for the Mono ilasm assembler
22+
- WASM, in WAT format
2223

2324
The test suite includes both static validation of generated/annotated ASTs, as well as runtime tests that actually execute the output programs to check correctness. Many of the AST validation test cases are taken from test suites included in the release code for Berkeley's CS164, with some additional tests written for more coverage.
2425

@@ -42,6 +43,7 @@ The input file should have extension `.py`. If the output file is not provided,
4243
- Python source outputs will be written to a file of the same name/location as the input file, with extension `.out.py`
4344
- JVM outputs will be written to the same location as the input file, with the extension `.j`
4445
- CIL outputs will be written to the same location as the input file, with the extension `.cil`
46+
- WASM outputs will be written to the same location as the input file, with the extension `.wat`
4547

4648
**Flags:**
4749

@@ -56,7 +58,7 @@ The input file should have extension `.py`. If the output file is not provided,
5658
- `hoist` - output untyped Python 3 source code w/o nonlocals or nested function definitions
5759
- `jvm` - output JVM bytecode formatted for the Krakatau assembler
5860
- `cil` - output CIL bytecode formatted for the Mono ilasm assembler
59-
- `wasm` - output WASM as plaintext in WAT format (WIP)
61+
- `wasm` - output WASM as plaintext in WAT format
6062

6163
## Differences from the reference implementation:
6264

@@ -110,8 +112,6 @@ The `demo_cil.sh` script is a useful utility to compile and run files with the C
110112

111113
## WASM Backend Notes:
112114

113-
This is WIP, not all features are supported (the binary tree example itself actually does not work, but you can try another one).
114-
115115
The WASM backend for this compiler outputs WASM in plaintext `.wat` format which can be converted to `.wasm` using `wat2wasm`:
116116
1. Use this compiler to generate plaintext WebAssembly
117117
- Format: `python3 main.py --mode wasm <input file> <output dir>`
@@ -126,26 +126,22 @@ The WASM backend for this compiler outputs WASM in plaintext `.wat` format which
126126
The `demo_wasm.sh` script is a useful utility to compile and run files with the WASM backend with a single command (provide the path to the input source file as an argument).
127127
- To run the same example as above, run `./demo_wasm.sh tests/runtime/binary_tree.py`
128128

129-
### WASM Backend - Supported Features:
130-
- int, bool, string, list
131-
- most operators
132-
- assignment
133-
- control flow
134-
- stdlib: print, len, and assert
135-
- globals
129+
The `wasm.js` file contains all the runtime support needed to run the WASM generated by this compiler. This backend was designed was to minimize runtime JavaScript dependencies, so the only imported functions are for assertions and printing strings/integers/booleans.
136130

137131
### WASM Backend - Unsupported Features:
138-
- nonlocal referencing function param
139-
- stdlib: input (node.js does not have synchronous I/O out of the box so this is difficult)
132+
- `input` stdlib function (node.js does not have synchronous I/O out of the box so this is difficult)
140133

141134
### WASM Backend - Memory Format, Safety, and Management:
142135

143136
- strings (utf-8) - first 4 bytes for length, followed by 1 byte for each character
144137
- lists - first 4 bytes for length, followed by 8 bytes for each element
145138
- ints - i64
146139
- pointers (objects, strings, lists) - i32, where `None` is 0
140+
- objects - first 8 bytes for vtable offset, followed by 8 bytes for each attribute, followed by 8 bytes for each method index. inherited attribute/method positions are same as parent.
141+
142+
Strings, lists, objects, and refs holding nonlocals are stored in the heap, aligned to 8 bytes. Right now, memory does not get freed/garbage collected once it is allocated, so large programs may run out of memory.
147143

148-
Strings, lists, objects, and refs holding nonlocals are stored in the heap, aligned to 8 bytes. Right now, memory does not get freed/garbage collected once it is allocated. To provide memory safety, string/list indexing have bounds checking and list operations have a null-check, which crashes the program with a generic "unreachable" instruction.
144+
To provide memory safety, string/list indexing have bounds checking and list operations have a null-check, which crashes the program with a generic "unreachable" instruction.
149145

150146
## FAQ
151147

compiler/astnodes/typedvar.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,3 @@ def toJSON(self, dump_location=True):
2121
d["identifier"] = self.identifier.toJSON(dump_location)
2222
d["type"] = self.type.toJSON(dump_location)
2323
return d
24-
25-
def getWasmParam(self, paramIdx, funcType):
26-
isRef = paramIdx in funcType.refParams
27-
t = "i32" if isRef else self.t.getWasmName()
28-
return f"(param ${self.identifier.name} {t})"

compiler/types/functype.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ def getJavaSignature(self) -> str:
5050
params.append(sig)
5151
return "({}){}".format("".join(params), r)
5252

53+
def getWasmSignature(self, names=None) -> str:
54+
params = []
55+
for i in range(len(self.parameters)):
56+
p = self.parameters[i]
57+
paramName = ("$" + names[i]) if names else ""
58+
if i in self.refParams and isinstance(p, ClassValueType):
59+
sig = f"(param {paramName} i32)"
60+
else:
61+
sig = f"(param {paramName} {p.getWasmName()})"
62+
params.append(sig)
63+
params = " ".join(params)
64+
result = "" if self.returnType.isNone() else f" (result {self.returnType.getWasmName()})"
65+
return params + result
66+
5367
def methodEquals(self, other):
5468
if isinstance(other, FuncType) and len(self.parameters) > 0 and len(other.parameters) > 0:
5569
return self.parameters[1:] == other.parameters[1:] and self.returnType == other.returnType

compiler/wasm_backend.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,9 @@ def _else(self):
3434
self.newLine(f"(else")
3535
self.indent()
3636

37-
def func(self, name: str, params: List[str] = [], resType=None) -> Builder:
37+
def func(self, name: str, sig: str="") -> Builder:
3838
# return new block for declaring extra locals
39-
params = " ".join(params)
40-
result = ""
41-
if resType is not None:
42-
result = f" (result {resType})"
43-
self.newLine(f"(func ${name} {params}{result}")
39+
self.newLine(f"(func ${name} {sig}")
4440
self.indent()
4541
return self.newBlock()
4642

@@ -231,18 +227,16 @@ def FuncDef(self, node: FuncDef):
231227
self.funcDefHelper(node, node.name.name)
232228

233229
def funcDefHelper(self, node: FuncDef, name: str):
234-
params = []
235-
for i in range(len(node.params)):
236-
params.append(node.params[i].getWasmParam(i, node.type))
237230
self.returnType = node.type.returnType
238231
ret = None if self.returnType.isNone() else self.returnType.getWasmName()
239-
self.locals = self.builder.func(name, params, ret)
232+
paramNames = [x.identifier.name for x in node.params]
233+
self.locals = self.builder.func(name, node.type.getWasmSignature(paramNames))
240234
for d in node.declarations:
241235
self.visit(d)
242236
self.visitStmtList(node.statements)
243237
# implicitly return None if possible
244238
if ret is not None and not isinstance(node.statements[-1], ReturnStmt):
245-
if self.returnType.getWasmName() == "i32":
239+
if self.returnType.getWasmName() == "i32" and not self.returnType.isSpecialType():
246240
self.instr("i32.const 0")
247241
else:
248242
self.instr("unreachable")
@@ -532,9 +526,7 @@ def MethodCallExpr(self, node: MethodCallExpr):
532526
self.instr("call $log_int")
533527
self.getLocal(temp)
534528

535-
params = " ".join([f"(param {t.getWasmName()})" for t in funcType.parameters])
536-
result = "" if funcType.returnType.isNone() else f"(result {funcType.returnType.getWasmName()})"
537-
self.instr(f"call_indirect {params} {result}")
529+
self.instr(f"call_indirect {funcType.getWasmSignature()}")
538530

539531
if funcType.returnType.isNone():
540532
self.NoneLiteral(None) # push null for void return

test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
error_flags = {"error", "Error", "Exception", "exception", "Expected", "expected", "failed"}
1313

1414
def run_all_tests():
15-
# run_parse_tests()
16-
# run_typecheck_tests()
17-
# run_python_backend_tests()
18-
# run_closure_tests()
19-
# run_jvm_tests()
20-
# run_cil_tests()
15+
run_parse_tests()
16+
run_typecheck_tests()
17+
run_python_backend_tests()
18+
run_closure_tests()
19+
run_jvm_tests()
20+
run_cil_tests()
2121
run_wasm_tests()
2222

2323
def run_parse_tests():

0 commit comments

Comments
 (0)