feat(report): write theory chapter

This commit is contained in:
2026-07-15 19:04:25 +02:00
parent cc39119789
commit 08f81bf4da
10 changed files with 1178 additions and 9 deletions

211
midas.sublime-syntax Normal file
View File

@@ -0,0 +1,211 @@
%YAML 1.2
---
name: Midas
file_extensions:
- midas
scope: source.midas
variables:
identifier: "[a-zA-Z_][a-zA-Z0-9_]*"
contexts:
prototype:
- include: comments
main:
- include: keywords
- include: types
comments:
- match: "//"
scope: punctuation.definition.comment.midas
push:
- meta_scope: comment.line.midas
- match: $
pop: true
- match: /\*
scope: punctuation.definition.comment.midas
push:
- meta_scope: comment.block.midas
- match: \*/
pop: true
string:
- meta_include_prototype: false
- meta_scope: string.quoted.double.c
- match: '"'
pop: true
keywords:
- match: \balias\b
scope: keyword.declaration.midas
push: alias-stmt
- match: \btype\b
scope: keyword.declaration.midas
push: type-stmt
- match: \bextend\b
scope: keyword.declaration.midas
push: extend-stmt
- match: \bpredicate\b
scope: keyword.declaration.midas
push: predicate-stmt
alias-stmt:
- match: "{{identifier}}"
scope: entity.name.type
- match: "="
scope: keyword.operator.equal.midas
push: type-expr
- match: $
pop: true
type-stmt:
- match: "{{identifier}}"
scope: entity.name.type
- match: \[
push: type-params
- match: "="
scope: keyword.operator.equal.midas
push: type-expr
- match: $
pop: true
type-expr:
- match: \b(fn)\s*(\()
captures:
1: keyword.other.midas
2: punctuation.section.group.begin
push: fn-params
- match: \b(where)\b
scope: keyword.other.midas
set: constraint
- match: "Frame"
scope: entity.name.type
push:
- match: \[
push: frame-schema
- match: $
pop: true
- match: "{{identifier}}"
scope: entity.name.type
- match: $
pop: 2
fn-params:
- match: "({{identifier}})(:)"
captures:
1: variable.parameter.midas
2: punctuation.separator.annotation.midas
push:
- include: type-expr
- match: \?
scope: keyword.operator.qmark.midas
- match: "(?=,)"
scope: punctuation.separator.midas
pop: true
- match: '(?=\))'
pop: true
- include: type-expr
- match: '\)'
set:
- match: "->"
scope: keyword.operator.arrow.midas
set: type-expr
constraint:
- match: $
pop: 2
- match: \d+(\.\d+)?
scope: constant.numeric.midas
- match: \b(true|false|none)\b
scope: constant.language.midas
- match: '"'
push: string
- match: (<=|>=|<|>|==|!=|&)
scope: keyword.operator
- match: _
scope: variable.language.midas
- match: '{{identifier}}(?=\s*\()'
scope: variable.function.midas
- match: "{{identifier}}"
scope: variable.other.readwrite.midas
type-params:
- match: "<:"
scope: keyword.operator.subtype.midas
- match: "[a-zA-Z][a-zA-Z_0-9]*"
scope: entity.name.type
- match: "]"
pop: true
extend-stmt:
- match: "{{identifier}}"
scope: entity.name.type
- match: \[
push: type-params
- match: \{
scope: punctuation.section.block.begin
set: extend-body
extend-body:
- include: member-stmt
- match: \}
scope: punctuation.section.block.end
pop: true
member-stmt:
- match: \b(prop|def)\b
scope: keyword.other.midas
push:
- match: "{{identifier}}"
scope: variable.other.member
- match: ":"
push: type-expr
- match: $
pop: true
predicate-stmt:
- match: "{{identifier}}"
scope: entity.name.function.midas
- match: '\('
push: predicate-params
- match: "="
scope: keyword.operator.equal.midas
set: constraint
- match: $
pop: true
predicate-params:
- match: "({{identifier}})(:)"
captures:
1: variable.parameter.midas
2: punctuation.separator.annotation.midas
push:
- include: type-expr
- match: "(?=,)"
scope: punctuation.separator.midas
pop: true
- match: '(?=\))'
pop: true
- match: '\)'
pop: true
frame-schema:
- include: frame-column
- match: \]
# scope: punctuation.section.block.end
pop: true
frame-column:
- match: "{{identifier}}"
scope: variable.other.member
- match: ":"
push: type-expr

View File

@@ -1,14 +1,8 @@
#import "@preview/acrostiche:0.7.0": *
#let acronyms = (
"DVI": (link("https://fr.wikipedia.org/wiki/Digital_visual_interface")[Digital Visual Interface],),
"FPGA": (link("https://en.wikipedia.org/wiki/Field-programmable_gate_array")[Field Programmable Gates Array],),
"HDMI": (link("https://en.wikipedia.org/wiki/HDMI")[High Definition Multimedia Interface],),
"LED": (link("https://en.wikipedia.org/wiki/Light-emitting_diode")[Light Emitting Diodes],),
"LCD": (link("https://en.wikipedia.org/wiki/Liquid-crystal_display")[Liquid Crystal Display],),
"PMod": (link("https://en.wikipedia.org/wiki/Pmod_Interface")[Peripheral Module],),
"USB": (link("https://en.wikipedia.org/wiki/USB")[Universal Serial Bus],),
"VGA": (link("https://fr.wikipedia.org/wiki/Video_Graphics_Array")[Virtual Graphics Array],),
"TaPL": ([Types and Progamming Languages@tapl],),
"LSP": (link("https://en.wikipedia.org/wiki/Liskov_substitution_principle")[Liskov substitution principle],),
)
#init-acronyms(acronyms)
#init-acronyms(acronyms)

View File

@@ -0,0 +1,88 @@
#import "@preview/curryst:0.6.0": prooftree, rule, rule-set
#import "@preview/gentle-clues:1.3.1" as gc
#import "@preview/lovelace:0.3.1": pseudocode-list
#let req = math.op("req")
This document formalizes the logic used when checking whether a function is a subtype of another.
= Definitions
A *Parameter specification* has a list of positional-only parameters $P$, mixed parameters $M$ and keyword-only parameters $K$:
$ S = (P, M, K) $
A *Parameter* has an index $i$, a name $n$ and a required flag $r$:
$ p = (i, n, r) $
A *Function* has a param spec $S$ and a return type $R$:
$ F = (S, R) $
= Main rules
We want to define a rule for checking structural subtyping of functions, i.e. to check when $F_1 <: F_2$
There are two conditions to check:
#align(center, prooftree(
rule(
$Gamma tack S_2 <: S_1$,
$Gamma tack R_1 <: R_2$,
$Gamma tack F_1 <: F_2$,
),
))
The second condition is trivial to check.
The first condition is a bit more tricky.
For a parameter specification $S_2$ to be a subtype of another $S_1$, the latter needs to be fully compatible with any usages of the former. What this means is that #link(<cond-1>)[(1)] any argument that can be passed to $S_2$ must be accepted by $S_1$, and #link(<cond-2>)[(2)] $S_1$ must not have additional required arguments that are not in $S_2$. However, $S_1$ can have additional optional parameters not present in $S_2$.
After mapping parameters of $S_1$ and $S_2$, types must be checked such that if a parameter $p_i: T in S_1$ is mapped to a parameter $q_j: U in S_2$, $U <: T$.
= Detailed rules for *ParamSpec* subtyping
#gc.info(title: [Notation])[
In the following equations:
- the notation $S in.rev a^i: T$ will be used to denote that a parameter spec $S$ accepts an argument named $a$ at index $i$ of type $T$
- the special name $alpha$ will be used to denote any argument without a specific name
- the special name $phi$ will be used to denote any parameter without a specific name
- $AA$ denotes the group of all arguments, i.e. $alpha in AA$
- $PP$ denotes the group of all parameters, i.e. $p in PP$
- $req(S, alpha)$ is a predicate checking whether $alpha$ is required in $S$
]
== Arguments of $S_2$ are compatible with $S_1$ <cond-1>
Formally, the condition is:
$
forall alpha in AA, S_2 in.rev alpha => S_1 in.rev alpha
$
Let $S_1 = (P_1, M_1, K_1)$ and $S_2 = (P_2, M_2, K_2)$.
For each positional-only parameter $phi_i in P_2$, $phi_i in P_1 or phi_i in M_1$. A positional-only parameter of $S_2$ can either be positional-only or mixed in $S_1$. Additionally, $not req(S_2, phi_i) => not req(S_1, phi_i)$. If $phi_i$ is optional in $S_2$, it must also be optional in $S_1$ so that a call omitting it is valid.
Similarly, for each keyword-only parameter $p in K_2$, $p in K_1 or p in M_1$. A keyword-only parameter of $S_2$ can either be keyword-only or mixed in $S_1$. Additionally, $not req(S_2, p) => not req(S_1, p)$. If $p$ is optional in $S_2$, it must also be optional in $S_1$ so that a call omitting it is valid.
Finally, for mixed parameters, the rule is slightly more complex. Either there is a corresponding mixed parameter in $S_1$, or the parameter is covered by both a positional/mixed and a keyword/mixed parameter. In the second case, we must keep in mind that only one of the match will made at runtime, or maybe even none if the parameter is optional in $S_2$, meaning the parameters in $S_1$ _must_ be optional.
We can thus split the rule in two. $forall p_i in M_2$:
$
cases(
p_i in M_1 and not req(S_2, p_i) => not req(S_1, p_i),
"or",
underbrace((phi_i in P_1 or phi_i in M_1), "Positional") and
underbrace((p in K_1 or p in M_1), "Keyword") and
underbrace((not req(S_1, phi_i) and not req(S_1, p)), "Optional")
)
$
== No additional required arguments in $S_1$ <cond-2>
Formally, the condition is:
$
forall alpha in AA, S_1 in.rev alpha and req(S_1, alpha) => S_2 in.rev alpha and req(S_2, alpha)
$
Each parameter in $S_1$ that is not matched by a parameter in $S_2$ must be optional:
$
forall p_i in S_1, p_i in.not S_2 => not req(S_1, p_i)
$

View File

@@ -0,0 +1,243 @@
#import "@preview/fervojo:0.1.1": default-css, render
#let extra-css = ```css
svg.railroad .terminal rect {
fill: #F7DCD4;
}
```
#let css = default-css() + bytes(extra-css.text)
#let literal = ```
{[`literal` <
[`number` 'digit' * ! <!, ["." 'digit' * !]>],
[`boolean` <"False", "True">],
[`string` <["\"" 'char'*! "\""], ["'" 'char'*! "'"]>],
[`none` "None"]
>]}
```
#let grouping = ```
{[`grouping` "(" 'expression' ")"]}
```
#let primary = ```
{[`primary` <"_", 'literal', 'identifier', 'grouping'>]}
```
#let reference = ```
{[`reference` 'primary' <!, ["." 'identifier']*!>]}
```
#let call-args = ```
{[`call-args` "(" <!, <'expression', ['identifier' "=" 'expression']>*","#`Same rules as Python`> ")"]}
```
#let call = ```
{[`call` 'reference' <!, 'call-args'*!>]}
```
#let unary = ```
{[`unary` <[<"+", "-", "!"> 'unary'], 'call'>]}
```
#let factor = ```
{[`factor` 'unary'*<"*", "/">]}
```
#let term = ```
{[`term` 'factor'*<"+", "-">]}
```
#let comparison = ```
{[`comparison` 'term'*<">", "<", ">=", "<=">]}
```
#let equality = ```
{[`equality` 'comparison'*<"==", "!=">]}
```
#let expression = ```
{[`expression` 'equality'*"&"]}
```
#let constraint = ```
{[`constraint` 'expression']}
```
#let template-param = ```
{[`template-param` 'identifier' <!, ["<:" 'type']>]}
```
#let template = ```
{[`template` "[" <!, 'template-param'*","> "]"]}
```
#let named-type = ```
{[`named-type` 'identifier']}
```
#let type-args = ```
{[`type-args` "[" <!, 'type'*","> "]"]}
```
#let frame-schema = ```
{[`frame-schema` "[" <!, ['TOKEN' ":" 'type']*","> "]"]}
```
#let generic-type = ```
{[`generic-type` <["Frame" 'frame-schema'], ['named-type' <!, 'type-args'>]>]}
```
#let grouped-type = ```
{[`grouped-type` "(" 'type' ")"]}
```
#let base-type = ```
{[`base-type` <'grouped-type', 'generic-type'>]}
```
#let constraint-type = ```
{[`constraint-type` 'base-type' <!, ["where" 'constraint']>]}
```
#let pos-param = ```
{[`pos-param` <!, ['identifier' ":"]> 'type' <!, "?">]}
```
#let kw-param = ```
{[`kw-param` 'identifier' ":" 'type' <!, "?">]}
```
#let param-spec = ```
{[`param-spec` "(" <!, <'pos-param', "/", "*", 'kw-param'>*",">#`Same rules as Python` ")"]}
```
#let func-type = ```
{[`func-type` "fn" 'param-spec' "->" 'type']}
```
#let type = ```
{[`type` <'func-type', 'constraint-type'>]}
```
#let alias-statement = ```
{[`alias-statement` "alias" 'identifier' "=" 'type']}
```
#let type-statement = ```
{[`type-statement` "type" 'identifier' <!, 'template'> "=" 'type']}
```
#let member-stmt = ```
{[`member-stmt` <"prop", "def"> 'identifier' ":" 'type']}
```
#let extend-statement = ```
{[`extend-statement` "extend" 'type' "{" <!, 'member-stmt'*!> "}"]}
```
#let predicate-statement = ```
{[`predicate-statement` "predicate" 'identifier' <!, 'param-spec'*!> "=" 'constraint']}
```
#let statement = ```
{[`statement` <'alias-statement', 'type-statement', 'extend-statement', 'predicate-statement'>]}
```
#let rules = (
literal: literal,
grouping: grouping,
primary: primary,
reference: reference,
call-args: call-args,
call: call,
unary: unary,
factor: factor,
term: term,
comparison: comparison,
equality: equality,
expression: expression,
constraint: constraint,
template-param: template-param,
template: template,
named-type: named-type,
type-args: type-args,
generic-type: generic-type,
grouped-type: grouped-type,
base-type: base-type,
constraint-type: constraint-type,
pos-param: pos-param,
kw-param: kw-param,
param-spec: param-spec,
func-type: func-type,
type: type,
alias-statement: alias-statement,
type-statement: type-statement,
member-stmt: member-stmt,
extend-statement: extend-statement,
predicate-statement: predicate-statement,
statement: statement,
)
#let inline = (
"grouping",
"literal",
"template-param",
"template",
"call-args",
"type-args",
"named-type",
"grouped-type",
"generic-type",
"base-type",
"constraint-type",
"pos-param",
"kw-param",
"func-type",
"member-stmt",
"alias-statement",
"type-statement",
"extend-statement",
"predicate-statement",
)
= Statements and expressions
#let all-rules = for (name, rule) in rules.pairs().rev() {
[== #name]
render(rule, css: css)
}
#let substitute(base-rule) = {
let new-rule = base-rule
for name in inline {
let rule = rules.at(name)
let replacement = rule.text.slice(1, -1).replace(regex("\[`.*?`"), "[")
replacement = "[" + replacement + "#`" + name + "`]"
new-rule = new-rule.replace(
"'" + name + "'",
replacement,
)
}
if new-rule != base-rule {
new-rule = substitute(new-rule)
}
return new-rule
}
#let capitalize(word) = {
return upper(word.slice(0, 1)) + word.slice(1)
}
#let display-name(name) = {
let words = name.split("-").map(capitalize)
return [#words.join(" ") (#raw(name))]
}
#let all-combined = for (name, rule) in rules.pairs() {
if not name in inline {
[== #display-name(name)]
let combined = substitute(rule.text)
render(raw(combined), css: css)
//raw(block: true, combined)
}
}

View File

@@ -9,10 +9,22 @@
#import "requirements.typ": isc-hei-bthesis
#import isc-hei-bthesis: *
#import "@preview/codly:1.3.0": codly-init, codly
#import "@preview/codly-languages:0.1.10": codly-languages
#import "../meta.typ"
#import "utils.typ": include-offset
#let doc_language = "en"
#show: codly-init
#codly(
languages: codly-languages,
)
#set raw(
syntaxes: path("../midas.sublime-syntax")
)
#show: thesis.with(
title: meta.title,
subtitle: meta.subtitle,
@@ -120,6 +132,8 @@ You can also change the order or the names of the sections, for instance, if you
#appendix-page()
#pagebreak()
#set heading(numbering: "A.1", supplement: [Appendix])
// Table of acronyms (optional). Defined near the acronyms above.
#acronym-table()
@@ -140,3 +154,15 @@ You can also change the order or the names of the sections, for instance, if you
),
caption: "Code included from the file example.scala"
)
#pagebreak()
= Midas Language Definition <app:midas-syntax>
#import "appendices/midas_railway.typ": all-combined
#all-combined
= Function Subtyping Rules <app:function-subtyping>
#include-offset("appendices/function_subtyping.typ")

View File

@@ -1,8 +1,239 @@
#import "../requirements.typ": isc-hei-bthesis
#import isc-hei-bthesis: todo
#import "@preview/gentle-clues:1.3.1" as gc
#import "@preview/acrostiche:0.7.0": acr
#import "@preview/curryst:0.6.0": prooftree, rule
#import "03_theory/python_syntax.typ": python-syntax
#import "03_theory/midas_syntax.typ": midas-syntax
#import "03_theory/typing.typ"
= Underlying Theory and Typing Rules <chap:theory>
#gc.info[
This chapter presents some type theory notions that form the basis of a type checker and establishes some typing rules we would wish to implement for Python. The reader may skip this more theoretical chapter to the implementation in @chap:impl. These notions are only explained to justify some implementation decisions and provide a solid basis on which to build the type checker but are not strictly necessary to understand the concrete development.
]
The first step to build a useful type checker for Python is to identify the concrete requirements. By identifying what kind of expressions and relationships we are dealing with, we can then select the rules we need to define and handle, and implement appropriate solutions. The concepts and ideas covered in this chapter are mainly derived from the wonderful #acr("TaPL") by Benjamin C. Pierce. This books serves as a reference for all type theory notions presented in this report. This report does not intend to become the new #acr("TaPL").
== Principles <sec:theory-principles>
To talk about type theory and typing rules, we must first define some concepts. B. C. Pierce suggests one definition for type systems:
#quote(block: true)[
A type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.@tapl
]
At its core, a type system provides techniques to ensure the absence of unwanted behavior due to incompatibilities between value types. Although often associated with computer science and programming languages, the fundamental concept of a type system is a mathematical construct.
In the following sections, we will define both syntax rules, to lay out legal constructs in the languages, and typing rules, to build relationships and theorems in our type system.
Syntactically, we will use different kinds of elements, as described in @tab:calculus-elmts.
#figure(
table(
columns: 3,
table.header[*Element*][*Meta-variable*][*Description*],
[Terms], $"t"$, [Building blocks of the language, expressions which can be evaluated],
[Values], $"v"$, [Subset of terms, possible final results of evaluation],
[Statements], $"s"$, [Language constructs that do not evaluate to a value and may have side-effects],
[Types], $"T"$, [Category of values of the same kind],
),
caption: [Calculus elements]
) <tab:calculus-elmts>
A term $"t"$ is said to be of type $"T"$, written $"t": "T"$ if it can be statically proven that it evaluates to a value belonging to type $"T"$.
Additionally, we will need to define rules that link different types together in a compatibility relationship. For example, we can say that a car is a kind of vehicle. In this case, the type `Car` is said to be a "subtype" of `Vehicle`. This relationship is asymmetric and transitive, and is written $"S" <: "T"$ (read "S is a subtype of T").
Finally, because we are dealing with a complex programming language such as Python, we need to introduce the concept of a context. In Python, as in most other languages, users can define variables and functions taking named parameters. This fact means that evaluating an expression, or even typing it, requires knowing some things about the context of what comes before. Taking the example of @fig:example-context, we can see that evaluating or typing the expression `a + a` on line 2 requires knowing the type (or value) of `a`, which was set in a different statement on line 1.
#figure(
```python
a: float = 3
b = a + a
```,
caption: [Example Python expression requiring context]
) <fig:example-context>
The notations for context are as follows:
#align(center, grid(
columns: 2,
gutter: 1em,
align: (center, left),
$Gamma$,
[the context, a list of bindings, mapping terms to types],
$Gamma, "t": "T"$,
[the context extended with the binding of $"t"$ to the type $"T"$],
))
Finally, the rules expressed in @sec:theory-syntax are written in the form of theorems using the following mathematical syntaxes:
#align(center, grid(
columns: 2,
gutter: 1em,
align: (center, left),
prooftree(rule($P_1$, $...$, $P_n$, $C$)),
[Given premises $P_1, ..., P_n$, conclusion $C$ is true],
$Gamma tack P$,
[$Gamma$ "judges" that $P$ is true, i.e. $P$ is true given context $Gamma$],
$P tack.l Gamma'$,
[Evaluating $P$ produces the new context $Gamma'$]
))
== Syntax rules <sec:theory-syntax>
In this section, we define syntax rules for both Python and our type definition language Midas. Obviously, Python already has syntax rules defining what is or is not valid code. What is defined in @sec:theory-syntax-python are a subset of constructs which our type checker will be able to process.
=== Python syntax <sec:theory-syntax-python>
@tab:python-syntax-rules lists mosts Python constructs we want to support with our type checker (notable exceptions are listed below). This subset of the Python language is sufficient to express a wide range of programs, including data processing pipelines and moderately complex scripts.
#figure(
python-syntax,
caption: [Python syntax rules],
supplement: [Table],
kind: table,
) <tab:python-syntax-rules>
Some syntaxes are simplified or omitted from @tab:python-syntax-rules for the sake of conciseness, readability and scope. Indeed, this work's primary focus is not on the theoretical and formal approach to type checking but rather on the concrete application of it to produce a useful tool for developers. For example, function signature in Python are rather complex and flexible, allowing positional-only, keyword-only and mixed parameters, argument sinks, and default values. Expressing this level of flexibility in a formal definition as those presented above is tricky and not necessarily pertinent to this work.
Other syntaxes not referenced in this report are voluntarily left out because they either fall out of the scope of this work or are either not fundamental to the language or not necessary to most developers. Such constructs include classes, match statements, while loops, `for`/`else` blocks and the walrus operator~(`:=`).
=== Midas syntax <sec:theory-syntax-midas>
Our type system includes a dedicated language to define custom types that can be used in Python code. This language is hereafter referred to as the Midas language, or simply Midas. Its goal is to allow users to concisely define types, aliases, subtypes and the like in a simple and intuitive language. All syntax rules are listed in @tab:midas-syntax-rules.
#figure(
midas-syntax,
caption: [Midas syntax rules],
supplement: [Table],
kind: table,
) <tab:midas-syntax-rules>
Parameter specifications in function types and predicates are simplified here for conciseness. In actuality, the supported syntax is similar to Python's regarding positional-only, keyword-only and mixed arguments, with the following notable differences:
- the name of positional-only parameters is optional, i.e. `fn(float, /) -> bool` is valid
- default values are not supported but optional parameters may be indicated with a question mark (`?`) after the type, e.g. `fn(a: float?) -> bool`
A more detailed definition of the syntax is available in @app:midas-syntax.
Some elements are worth explaining here. Midas offers four base statements through four keywords: `alias`, `type`, `extend` and `predicate`.\
The first is rather straightforward: ```midas alias X = T``` defines an alias named `X` for the type `T`.\
The second is a simple subtype definition. ```midas type X = T``` is similar to an alias, but it creates a subtype relationship instead of an equivalence. `X` is thus a subtype of `T`. This also allows defining generic types using type parameters, bounded or not, which can be referenced in the right-hand side of the definition. For example ```midas type Foo[T <: U] = list[T]``` defines a type generic `Foo` with one parameter `T` subtype of `U`, which is a subtype of `list[T]`.\
The third, `extend`, is linked to a subtype definition. It allows users to define members on a type, such as properties/attributes (`prop`) and methods (`def`). Methods may be overloaded to combine multiple signatures under the same name. The type specific after `extend` must obviously be defined.\
The last kind of statement provides user with a way to define named constraint expressions and reuse them as functions in other constraint expressions. Predicates are defined like functions, with a list of parameters. This list may be split into multiple parameter specifications, for example ```midas predicate foo(a: float)(b: int) = a < b``` to allow partial application.
A particularity of Midas are its constraint types. A constraint type `T where e` is built from a base type `T` and a constraint expression `e`. It allows binding a constraint on the values belonging to this type, which will be materialized by assertions at runtime.
== Typing rules <sec:theory-typing>
Now that we have defined what Python syntax we will support and our own definition language, we need to define how our type checker will be able to bind types to Python expressions. This section is heavily inspired by the _Pure simply typed lambda-calculus~($lambda_->$)_, _Simply typed lambda-calculus with subtyping~($lambda_(<:)$)_, _Polymorphic lambda-calculus (System F)_ and _Bounded quantification (kernel $F_(<:)$)_ presented in Chapters 9, 15, 23 and 26 of #acr("TaPL")@tapl.
=== Literals
First and foremost, we can define elementary typing rules for all literal values. These rules having no premises, they are simple axioms. @tab:typing-literals lists all literal typing rules.
#figure(
typing.literals,
caption: [Typing rules: literals],
kind: table,
supplement: [Table],
) <tab:typing-literals>
Other constructs, although not constants, directly map to builtin types. These include literal lists, tuples and dictionaries. Each of these has a corresponding `list[T]`, `tuple` and `dict[K, V]` type. We will not define formal rules in this section regarding these elements, but a more in depth explanation will be given in the implementation of the type checker, in @sec:impl-python. #todo[replace ref]
=== Expressions
Apart from literals, developers also need some building blocks to express their programs. These expressions include variables, function calls, operations, etc. and need their own typing rules. The rules listed in @tab:typing-expressions cover most expressions we will handle, as defined in @sec:theory-syntax-python. Some syntaxes are omitted to keep this chapter short and because they don't necessarily bring new theoretical concepts.
#figure(
typing.expressions,
caption: [Typing rules: expressions],
kind: table,
supplement: [Table],
) <tab:typing-expressions>
#smallcaps[T-Var] simply states that a variable can be typed iff it is in the context.
#smallcaps[T-Call] is a simplified rule of the concrete behavior. Taking into account all call forms with positional and keyword arguments is a rather tedious process which is left as an exercise to the reader.
#smallcaps[T-Tern] is worth a few more words too. Typing rules used by Midas are, by design, stricter than what Python allows. In this case, because we have chosen not to include union types, an expression can only have at most one type, thus the branches must belong to a common type. The specific types of each branch may differ thanks to #smallcaps[T-Sub], defined in @tab:typing-subtyping-base. Additionally, the test expression must be of type $"bool"$, whereas Python can evaluate any object to a boolean.
#smallcaps[T-Op] is used for all binary operations. In Python, these are implemented in _dunder-methods_, such as `__add__` for the `+` operator. A similar rule could be defined for unary operations but is omitted here for brevity.
=== Statements
Finally, expressions can be used in statements, which are special constructs that can have side-effects. We listed in @sec:theory-syntax-python the particular statements that Midas should support, and @tab:typing-statements provides rules to type check their components and effects.
#figure(
typing.statements,
caption: [Typing rules: statements],
kind: table,
supplement: [Table],
) <tab:typing-statements>
#smallcaps[T-Annot] is a rule which handles Python type annotations. It states that the type checker must take note of type hints in the context.
#smallcaps[T-Seq] allows sequences of statements.
#smallcaps[T-IfElse], like #smallcaps[T-Tern] is a stricter version of how Python processes `if` statements. Indeed, Midas will not allow leaking context from inside the bodies of any branch. If a variable is declared inside an `if` statement, Midas will not allow references outside of it.
#gc.info[
You may be familiar with the walrus operator `:=` which is often used in `if` statements to simultaneously declare a variable and use it as an expression. It it thus a mix between a simple expression, which may be used everywhere, and a statement, with the side-effect of declaring a variable.
]
#smallcaps[T-Def] is a gross simplification of how functions can be defined. It does not cover multiple parameters, positional-only and keyword-only parameters, complex bodies with multiple and implicit returns, etc. Again, for the sake of simplicity and readability, we will not define a complete rule to handle all forms and features, but the implementation will go into more details in @sec:impl-python. #todo[replace ref]
=== Subtyping
Similarly to some calculi described in #acr("TaPL")@tapl and to allow flexibility, we must equip our type system with a subtyping relationship. This relationship allows using some types in place of others where it is sound to do so. The subtyping rules presented in @tab:typing-subtyping-base cover the base cases of subtyping.
#figure(
typing.subtyping,
caption: [Typing rules: subtyping base],
kind: table,
supplement: [Table],
) <tab:typing-subtyping-base>
The subtyping relationship is by definition both reflective (#smallcaps[S-Refl]) and transitive (#smallcaps[S-Trans]).
#smallcaps[T-Sub] is a consequence of the #acr("LSP"). A term $"t"$ of type $"S"$ can be used anywhere a term of type $"T"$ is expected if $"S"$ is a subtype of $"T"$.
In addition to these base rules, we need to define how functions, constraint types and generic types are handled.
For constraint types, we will take the simple approach of considering only the base type for the subtype relationship. A more complete and interesting implementation could implement subtyping of constraint expression, where a condition such as `_ < 10` is a subtype of `_ < 100`, but the added complexity was deemed out of scope for this particular project. We do nonetheless define #smallcaps[S-Constr] in @tab:typing-subtyping-constraint to handle the base case.
#figure(
typing.subtyping-constraint,
caption: [Typing rules: subtyping constraint type],
kind: table,
supplement: [Table],
) <tab:typing-subtyping-constraint>
Generics are handled using similar rules to the _Bounded quantification (kernel $F_(<:)$)_ calculus presented in #acr("TaPL") Chapter 26@tapl. Additionally, Midas supports implicit variance, which is inferred from the locations in which type parameters are used, and is used when checking subtypes of applied generic types. A detailed explanation of variance inference is provided in the corresponding implementation section, @sec:impl-python #todo[replace ref].
Finally, functions in Python are complex types. The general subtyping rule is given in @tab:typing-subtyping-function, but the complete set of rules for checking parameter specifications is too complex to include here. It is however detailed in @app:function-subtyping.
#figure(
typing.subtyping-function,
caption: [Typing rules: subtyping functions],
kind: table,
supplement: [Table],
) <tab:typing-subtyping-function>
=== Special types
Two special types can be added to our type system for practical purposes. The first is a regular top type, as represented by `Any` in Python. Every type is a subtype of `Any`, as stated by #smallcaps[S-Any] in @tab:typing-subtyping-top.
In addition to `Any`, we will add a second top type to represent unknowns. When a user omits an annotation, the type checker can use this type as a placeholder to track that a particular expression was indeed processed but its type could not be determined. This has many practical benefits, among which avoiding early exits and propagating unknowns. Additionally, this allows the type checker to differentiate a variable which has specifically been typed as `Any`, and can contain _any_ kind of value, from a variable whose type the checker does not know.
#figure(
typing.subtyping-top,
caption: [Typing rules: subtyping top types],
kind: table,
supplement: [Table],
) <tab:typing-subtyping-top>
The special type `None`, also later referred to as `UnitType`, is used in Midas as a unit type, mapping Python functions' behavior regarding implicit returns.
/*
- Identify requirements
- Introduction to TAPL

View File

@@ -0,0 +1,61 @@
#import "utils.typ": *
#let types = (
($"X"$, [_named type_]),
($"T" ["T"^(i in 1..n)]$, [_type application_]),
($"T" #syntax[where] "e"$, [_constraint type_]),
($#syntax[fn] (attach("a"_i: "T"_i, tr: i in 1..n)) -> "T"$, [_function type_]),
)
#let statements = (
($#syntax[alias] "X" = "T"$, [_alias definition_]),
($#syntax[type] "X" = "T"$, [_type definition_]),
($#syntax[type] "X"["P"^(i in 1..n)] = "T"$, [_generic definition_]),
($ cases(
#syntax[extend] "X"["P"^(i in 1..n)] space \{,
quad m^(j in 1..M),
\},
delim: "["
) $, [_generic definition_]),
($#syntax[predicate] "p" = "e"$, [_predicate alias_]),
($#syntax[predicate] "p" (attach("a"_i: "T"_i, tr: i in 1..n))^(j in 1..m) = "e"$, none),
($$, [_predicate definition_]),
)
#let type-parameters = (
($X$, [_type variable_]),
($X <: T$, [_bounded type\ variable_]),
)
#let member-statements = (
($#syntax[prop] x: T$, [_property_]),
($#syntax[def] x: T$, [_method_]),
)
#let expressions = (
($x$, [_variable_]),
($"i"$, [_integer literal_]),
($"f"$, [_float literal_]),
($"z"$, [_string literal_]),
($"True"$, [_constant true_]),
($"False"$, [_constant false_]),
($"None"$, [_constant none_]),
($f("t")$, [_call_]),
($"t"_1 "op" "t"_2$, [_operation_]),
($"t"."x"$, [_attribute_]),
($\_$, [_self reference_])
)
#let midas-syntax = grid(
columns: (1fr, 1fr),
gutter: 2em,
syntax-grid(
("T", "types", types),
("e", "expressions", expressions),
),
syntax-grid(
("s", "statements", statements),
("P", "type parameters", type-parameters),
("m", "member statements", member-statements),
),
)

View File

@@ -0,0 +1,68 @@
#import "utils.typ": *
#let terms = (
($"x"$, [_variable_]),
($"i"$, [_integer literal_]),
($"f"$, [_float literal_]),
($"z"$, [_string literal_]),
($"True"$, [_constant true_]),
($"False"$, [_constant false_]),
($"None"$, [_constant none_]),
($f("t")$, [_call_]),
($"t"_21 #syntax[if] "t"_1 #syntax[else] "t"_22$, [_ternary_]),
($"t"_1 "op" "t"_2$, [_operation_]),
($"t" [ "t" ]$, [_subscript_]),
($"t"."x"$, [_attribute_]),
)
#let values = (
($"i"$, [_integer literal_]),
($"f"$, [_float literal_]),
($"z"$, [_string literal_]),
($"True"$, [_constant true_]),
($"False"$, [_constant false_]),
($"None"$, [_constant none_]),
)
#let statements = (
($"t"$, [_expression_]),
($"s"; "t"$, [_inline sequence_]),
($ cases(
"s" #nl,
"s",
delim: "["
) $, [_multiline sequence_]),
($"x" = "t"$, [_assignment_]),
($"x": "T"$, [_variable declaration_]),
($ cases(
#syntax[def] f("x": "T") -> "T": #nl,
#tab "t" #nl,
delim: "["
) $, [_def_]),
($ cases(
#syntax[if] "t": #nl,
tab "s" #nl,
#syntax[else]: #nl,
tab "s" #nl,
delim: "["
) $, [_if / else_]),
($ cases(
#syntax[for] "x" #syntax[in] "t":#nl,
#tab "s",
delim: "["
) $, [_for loop_]),
($#syntax[return]$, [_bare return_]),
($#syntax[return] "t"$, [_value return_]),
)
#let python-syntax = grid(
columns: (1fr, 1fr),
gutter: 2em,
syntax-grid(
("t", "terms", terms),
("v", "values", values),
),
syntax-grid(
("s", "statements", statements),
),
)

View File

@@ -0,0 +1,199 @@
#import "@preview/curryst:0.6.0"
#import "utils.typ": *
#let rule(
name,
..premises,
conclusion,
python,
) = (
return (
name: name,
rule: curryst.prooftree(
curryst.rule(
..premises.pos(),
conclusion,
name: smallcaps[(#name)],
),
),
python: python,
)
)
#let literals-rules = (
rule(
"T-Int",
$Gamma tack "i": "int"$,
```py 12: int```
),
rule(
"T-Float",
$Gamma tack "f": "float"$,
```py 12.34: float```
),
rule(
"T-Str",
$Gamma tack "z": "str"$,
```py "foo": str```
),
rule(
"T-True",
$Gamma tack "True": "bool"$,
```py True: bool```
),
rule(
"T-False",
$Gamma tack "False": "bool"$,
```py False: bool```
),
rule(
"T-None",
$Gamma tack "None": "None"$,
```py None: None```
),
)
#let expressions-rules = (
rule(
"T-Var",
$"x": "T" in Gamma$,
$Gamma tack "x": "T"$,
```py x```,
),
rule(
"T-Call",
$Gamma tack f: "T"_1 -> "T"_2$,
$Gamma tack "t": "T"_1$,
$Gamma tack f("t"): "T"_2$,
```py
def func(a: S) -> T: ...
...
func(a): T
```,
),
rule(
"T-Tern",
$Gamma tack "t"_1: "bool"$,
$Gamma tack "t"_21: "T"$,
$Gamma tack "t"_22: "T"$,
$Gamma tack "t"_21 #syntax[if] "t"_1 #syntax[else] "t"_22: "T"$,
```py (true if cond else false): T ```,
),
rule(
"T-Op",
$Gamma tack "t"_1: "T"_1$,
$Gamma tack "t"_2: "T"_2$,
$"op": "T"_1 -> "T"_2 -> "T"_3 in Gamma$,
$Gamma tack "t"_1 "op" "t"_2: "T"_3$,
```py (a + b): T```,
),
)
#let statements-rules = (
rule(
"T-Annot",
$Gamma tack "x": "T" tack.l Gamma, "x": "T"$,
```py x: T```,
),
rule(
"T-Seq",
$Gamma tack "s"_1 tack.l Gamma'$,
$Gamma' tack "s"_2 tack.l Gamma''$,
$Gamma tack "s"_1; "s"_2 tack.l Gamma''$,
```py a = 3; a: int```
),
rule(
"T-IfElse",
$Gamma tack "t": "bool"$,
$Gamma tack "s"_1 tack.l Gamma'$,
$Gamma tack "s"_2 tack.l Gamma''$,
$Gamma tack #syntax[if] "t": "s"_1 #syntax[else]: "s"_2 tack.l Gamma$,
```py
if cond:
...
else:
...
```,
),
rule(
"T-Def",
$Gamma, f: "T"_1 -> "T"_2, "x": "T"_1 tack "t": "T"_2$,
$ Gamma tack cases(delim: #none, #syntax[def] &f("x": "T"_1) -> "T"_2:, &#syntax[return] "t") space tack.l space Gamma, f: "T"_1 -> "T"_2 $,
```py
def func(a: S) -> T:
return ...: T
```,
),
)
#let subtyping-rules = (
rule(
"S-Refl",
$"S" <: "S"$,
none,
),
rule(
"S-Trans",
$"S" <: "U"$,
$"U" <: "T"$,
$"S" <: "T"$,
none,
),
rule(
"T-Sub",
$Gamma tack "t": "S"$,
$"S" <: "T"$,
$Gamma tack "t": "T"$,
none,
),
)
#let rules-grid(rules, columns: 1, large: false) = {
let cells = rules.map(
r => (r.rule, r.python)
).chunks(columns).flatten()
let header = ([*Rule*], [*Python example*]) * columns
grid(
columns: (auto, if large {1fr} else {auto}) * columns,
//row-gutter: 2em,
//column-gutter: 1em,
column-gutter: (0pt, 3em) * columns,
inset: (x:.5em, y: 1em),
align: (center+horizon, right+horizon),
//stroke: (x, _) => if calc.rem(x, 2) == 0 and x != 0 {(left: black)},
grid.header(..header),
..cells,
)
}
#let literals = rules-grid(literals-rules, columns: 2)
#let expressions = rules-grid(expressions-rules)
#let statements = rules-grid(statements-rules, large: true)
#let subtyping = curryst.rule-set(
..subtyping-rules.map(r => r.rule)
)
#let subtyping-constraint = curryst.prooftree(
curryst.rule(
$"S" <: "T"$,
$"S" #syntax[where] "e" <: "T"$,
name: smallcaps[(S-Constr)],
)
)
#let subtyping-function = curryst.prooftree(
curryst.rule(
$"S"_2 <: "S"_1$,
$"R"_1 <: "R"_2$,
$#syntax[fn] ("S"_1) -> "R"_1 <: #syntax[fn] ("S"_2) -> "R"_2$,
name: smallcaps[(S-Func)],
)
)
#let subtyping-top = curryst.rule-set(
curryst.prooftree(curryst.rule(
$"T" <: "Any"$,
name: smallcaps[(S-Any)],
)),
curryst.prooftree(curryst.rule(
$"T" <: "Unknown"$,
name: smallcaps[(S-Unknown)],
)),
)

View File

@@ -0,0 +1,48 @@
#let syntax(body) = {
set text(weight: "bold", fill: rgb(100, 50, 0))
body
}
#let blank(r, c) = grid.cell(
rowspan: r,
colspan: c
)[]
#let tab = sym.arrow.r.stop
#let nl = move(dx: -2pt, dy: 2pt, sym.arrow.curve.l)
#let syntax-preamble = doc => {
show tab: set text(fill: gray.darken(20%))
show sym.arrow.curve.l: set text(fill: gray.darken(20%))
set math.cases(gap: 4pt)
show math.equation: set align(left)
doc
}
#let syntax-grid(..sections) = {
let cells = ()
for (i, (meta-var, name, elements)) in sections.pos().enumerate() {
if i != 0 {
cells.push(grid.cell(colspan: 4)[])
}
cells += ($#meta-var$, $::=$, none, [_#{name}_])
cells.push(blank(elements.len(), 2))
cells += elements.map(
e => if e.last() == none {
grid.cell(colspan: 2, e.first())
} else {
e
}
).flatten()
}
show: syntax-preamble
grid(
columns: (auto, auto, 1fr, auto),
align: (left, left, left, right).map(a => a + horizon),
column-gutter: .4em,
row-gutter: 1.2em,
..cells,
)
}