Files
TB-Docs/report/chapters/04_implementation/06_generation.typ

67 lines
4.8 KiB
Typst

#import "../../requirements.typ": isc-hei-bthesis
#import isc-hei-bthesis: todo
#import "../../utils.typ": code-ref
#import "@preview/acrostiche:0.7.0": acr
= Code Generation <sec:impl-generation>
The final stage of compiling a program with Midas is the generation of a runnable file.
This process comes after type checking, which verified the program's soundness and collected some assertions to perform at runtime.
== Stubs <sec:gen-stubs>
As Midas provides its own language to define custom types, the Python interpreter and other type checkers are completely oblivious to their existence and meaning. Although theoretically not necessary for runtime, types may be used while interpreting annotations, cast calls or through reflection. One solution is to generate stubs that can be used in Python code representing custom types defined in Midas.
Simple derived types are translated through class inheritance, type aliases using simple assignments, and generics with Python native `Generic` and `TypeVar` classes. Functions are converted to `Protocol` subclasses for rich structural subtyping with other type checkers.
Obviously, not all features of Midas can be translated into Python. Constraint types for example are simply represented like derived types, omitting the constraint aspect.
The stubs can be manually generated with a #acr("CLI") command, to use when developing, and are automatically output when compiling for runtime use.
The stubs generator implementation is available in the repository in #code-ref(<stubs-gen>, "midas/generator/stubs.py").
== Output Code <sec:gen-code>
Given the `TypedAST` produced by `PythonTyper`, we must then convert `p.Stmt` and `p.Expr` nodes back to Python's `ast.stmt` and `ast.expr`. This process is quite similar to how `PythonParser` handled the opposite situation in @sec:python-parsing. The `Generator` thus implements `p.Stmt.Visitor[ast.stmt]` and `p.Expr.Visitor[ast.expr]`.
In the end, an `ast.Module` is produced and unparsed into raw Python code. The full implementation of `Generator` is available in the repository in #code-ref(<generator>, "midas/generator/generator.py").
== Assertions <sec:gen-assertions>
`Generator` also needs to produce assertions for each `cast` expression, as described in @sec:gen-assertions. Such expressions may appear anywhere in the program, including in other expressions. Because Python assertions are statements, we cannot simply insert them in the surrounding expression. Instead, the approach we implement is to define an alias variable for the expression, before the statement where it appears, add the `assert` statement and insert back the alias where the expression is used.
In practice, this looks like @fig:example-assert-alias.
#figure(
[
*Source*
```python
bar = cast(float, foo)
```
*Generated*
```python
__midas_a0__ = foo
assert isinstance(__midas_a0__, float), f'source.py:L1:7: CastError: Cannot cast {type(__midas_a0__).__name__} to float'
bar = __midas_a0__
del __midas_a0__
```
],
caption: [Example expression alias for runtime assertion]
) <fig:example-assert-alias>
Several aspects must be noted:
- aliases are given unique names with an incremental id.
- assertions are given detailed messages, including the expression's location in the original source file. Indeed, this is necessary to provide users with useful error messages since any runtime `AssertionError` will show the context in the generated file. This is a simple solution which could be improved by specially crafting and raising an exception with a manipulated traceback. However, we must not assume that the source file will always be available at runtime, as users may copy only generated files to another location.
- we also generate a `del` statement right after the last usage of an alias to avoid uselessly polluting the environment.
One limitation is to be mentioned. As described in the #code-ref(<manual>, "docs/manual.pdf", body: [user manual]), aliases are eagerly evaluated before their actual uses meaning logical short-circuiting may be bypassed. Like many languages, Python implements logical `and` and `or` operations as short-circuiting operators, which skip evaluating the second operand if unnecessary.
A tradeoff is also made for members (properties and methods), which are not checked at runtime. This could be one possible future improvement, as discussed in @chap:conclusion.
For constraint expressions, we must generate equivalent Python functions that can be used to verify predicates at runtime. This is handled by the `ConstraintGenerator` class, available in the repository in #code-ref(<constraint-gen>, "midas/generator/constraints.py").
Special care is taken to avoid generating unused functions and only generating one function for each constraint expression.
/*
- Code generation
- Stubs
- Assertions
*/