Skip to content

Commit 75fd52f

Browse files
committed
author some docs
1 parent 5e164d7 commit 75fd52f

File tree

19 files changed

+479
-99
lines changed

19 files changed

+479
-99
lines changed

book/src/SUMMARY.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Summary
22

33
- [Intro](./intro.md)
4-
- [Formality](./formality.md)
5-
- [Terms](./terms.md)
6-
- [Inference rules](./inference_rules.md)
4+
- [`formality_core`: the Formality system](./formality_core.md)
5+
- [Defining your lang](./formality_core/lang.md)
6+
- [Defining terms with the `term` macro](./formality_core/terms.md)
7+
- [Implementing `Fold` and `Visit` by hand](./formality_core/impl_fold_visit.md)
8+
- [Implementing `Parse` by hand](./formality_core/impl_parse.md)
9+
- [Variables](./formality_core/variables.md)
10+
- [Collections](./formality_core/collections.md)
11+
- [Judgment functions and inference rules](./formality_core/judgment_fn.md)
12+
- [FAQ and troubleshooting](./formality_core/faq.md)

book/src/faq.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# FAQ and troubleshooting

book/src/formality_core.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# `formality_core`: the Formality system
2+
3+
`a-mir-formality` is build on the formality core system,
4+
defined by the `formality_core` crate.
5+
Formality core is a mildly opnionated series of macros, derives, and types
6+
that let you write high-level Rust code
7+
in a way that resembles standard type theory notation.
8+
Its primary purpose is to be used by a-mir-formality
9+
but it can be used for other projects.
10+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Collections

book/src/formality_core/faq.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# FAQ and troubleshooting
2+
3+
## Why am I getting errors about undefined references to `crate::FormalityLang`?
4+
5+
The various derive macros need to know what language you are working in.
6+
To figure this out, they reference `crate::FormalityLang`, which you must define.
7+
See the [chapter on defining your language](./lang.md) for more details.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Implementing `Fold` and `Visit` by hand
2+
3+
The `#[term]` macro auto-generates impls of `Fold`, `Visit`, and `Parse`.
4+
But sometimes you don't want to use the macro.
5+
Sometimes you want to write your own code.
6+
One common reason is for substitution of a variable.
7+
For example, in the Rust model,
8+
the code that replaces type variables with types from a substitution
9+
is defined by a manual impl of `Fold`.
10+
Because `Fold` and `Visit` are trait aliases, you need to implement the underlying
11+
trait (`CoreFold`) by hand.
12+
Here is the custom impl of fold for `Ty` from `formality_types`:
13+
14+
```rust
15+
{{#include ../../../crates/formality-types/src/grammar/ty/term_impls.rs:core_fold_ty}}
16+
```
17+
18+
That same module contains other examples, such as impls of `CoreVisit`.
19+
20+
## Derives
21+
22+
You can also manually derive `Visit` and `Fold` instead of using `#[term]`.

book/src/formality_core/impl_parse.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Implementing Parse by hand
2+
3+
The generic `#[term]` macro generates a simple parser, but sometimes you want more flexibility.
4+
Here is the code that implements the parsing of Rust types:
5+
6+
```rust
7+
{{#include ../../../crates/formality-types/src/grammar/ty/parse_impls.rs:ty_parse_impl}}
8+
```
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Judgment functions and inference rules
2+
3+
The next thing is the `judgement_fn!` macro. This lets you write a *judgment* using *inference rules*. A "judgment" just means some kind of predicate that the computer can judge to hold or not hold. Inference rules are those rules you may have seen in papers and things:
4+
5+
```
6+
premise1
7+
premise2
8+
premise3
9+
------------------
10+
conclusion
11+
```
12+
13+
i.e., the conclusion is judged to be true if all the premises are true.
14+
15+
[`prove_wc`]: https://github.com/rust-lang/a-mir-formality/blob/bca36ecd069d6bdff77bffbb628ae3b2ef4f8ef7/crates/formality-prove/src/prove/prove_wc.rs#L21-L125
16+
17+
Judgments in type system papers can look all kinds of ways. For example, a common type system judgment would be the following:
18+
19+
```
20+
Γ ⊢ E : T
21+
```
22+
23+
This can be read as, in the environment Γ, the expression E has type T. You might have rule like these:
24+
25+
```
26+
Γ[X] = ty // lookup variable in the environment
27+
--------------- "variable"
28+
Γ ⊢ X : ty
29+
30+
Γ ⊢ E1 : T // must have the same type
31+
Γ ⊢ E2 : T
32+
--------------- "add"
33+
Γ ⊢ E1 + E2 : T
34+
```
35+
36+
In a-mir-formality, you might write those rules like so:
37+
38+
```rust
39+
judgment_fn! {
40+
pub fn has_type(
41+
env: Env,
42+
expr: Expr,
43+
) => Type {
44+
(
45+
(env.get(&name) => ty)
46+
---------------
47+
(has_type(env, name: Variable) => ty)
48+
)
49+
50+
(
51+
(has_type(env, left) => ty_left)
52+
(has_type(env, right) => ty_right)
53+
(if ty_left == ty_right)
54+
---------------
55+
(has_type(env, Expr::Add(left, right)) => ty_left)
56+
)
57+
}
58+
}
59+
```
60+
61+
Unlike mathematical papers, where judgments can look like whatever you want, judgments in a-mir-formality always have a fixed form that distinguish inputs and outputs:
62+
63+
```
64+
judgment_name(input1, input2, input3) => output
65+
```
66+
67+
In this case, `has_type(env, expr) => ty` is the equivalent of `Γ ⊢ E : T`. Note that, by convention, we tend to use more English style names, so `env` and not `Γ`, and `expr` and not `E`. Of course nothing is stop you from using single letters, it's just a bit harder to read.
68+
69+
When we write the `judgement_fn`, it is going to desugar into an actual Rust function that looks like this:
70+
71+
```rust
72+
pub fn has_type(arg0: impl Upcast<Env>, arg1: impl Upcast<Expr>) -> Set<Type> {
73+
let arg0: Env = arg0.upcast();
74+
let arg1: Expr = arg1.upcast();
75+
76+
...
77+
}
78+
```
79+
80+
Some things to note. First, the function arguments (`arg0`, `arg1`) implicitly accept anything that "upcasts" (infallibly converts) into the desired types. `Upcast` is a trait defined within a-mir-formality and implemented by the `#[term]` macro automatically.
81+
82+
Second, the function always returns a `Set`. This is because there can be more rules, and they may match in any ways. The generated code is going to exhaustively search to find all the ways that the rules could match. At a high-level the code looks like this (at least if we ignore the possibility of cycles; we'll come back to that later):
83+
84+
```rust
85+
pub fn has_type(arg0: impl Upcast<Env>, arg1: impl Upcast<Expr>) -> Set<Type> {
86+
let arg0: Env = arg0.upcast();
87+
let arg1: Expr = arg1.upcast();
88+
89+
let mut results = set![]; // we define this macro
90+
91+
if /* inference rule 1 matches */ {
92+
results.push(/* result from inference rule 1 */);
93+
}
94+
95+
if /* inference rule 2 matches */ {
96+
results.push(/* result from inference rule 1 */);
97+
}
98+
99+
// ...
100+
101+
if /* inference rule N matches */ {
102+
results.push(/* result from inference rule N */);
103+
}
104+
105+
results
106+
}
107+
```
108+
109+
So how do we test if a particular inference rule matches? Let's look more closely at the code we would generate for this inference rule:
110+
111+
```rust
112+
(
113+
(env.get(name) => ty)
114+
---------------
115+
(has_type(env, name: Variable) => ty)
116+
)
117+
```
118+
119+
The first part of the final line, `has_type(env, name: Variable)`, defines patterns that are matched against the arguments. These are matched against the arguments (`arg0`, `arg1`) from the judgment. Patterns can either be trivial bindings (like `env`) or more complex (like `name: Variable` or `Expr::Add(left, right)`). In the latter case, they don't have to match the type of the argument precisely. Instead, we use the `Downcast` trait combined with pattern matching. So this inference rule would compile to something like...
120+
121+
```rust
122+
// Simple variable bindings just clone...
123+
let env = arg0.clone();
124+
125+
// More complex patterns are downcasted and testing...
126+
if let Some(name) = arg1.downcast::<Variable>() {
127+
... // rule successfully matched! See below.
128+
}
129+
```
130+
131+
Once we've matched the arguments, we start trying to execute the inference rule conditions. We have one, `env.get(&name) => ty`. What does that do? A condition written like `$expr => $pat` basically becomes a for loop, so you get...
132+
133+
```rust
134+
let env = arg0.clone();
135+
if let Some(name) = arg1.downcast::<Variable>() {
136+
for ty in env.get(&name) {
137+
... // other conditions, if any
138+
}
139+
}
140+
```
141+
142+
Once we run out of conditions, we can generate the final result, which comes from the `=> $expr` in the conclusion of the rule. In this case, something like this:
143+
144+
```rust
145+
let env = arg0.clone();
146+
if let Some(name) = arg1.downcast::<Variable>() {
147+
for ty in env.get(&name) {
148+
result.push(ty);
149+
}
150+
}
151+
```
152+
153+
Thus each inference rule is converted into a little block of code that may push results onto the final set.
154+
155+
The second inference rule (for addition) looks like...
156+
157+
```rust
158+
// given this...
159+
// (
160+
// (has_type(env, left) => ty_left)
161+
// (has_type(env, right) => ty_right)
162+
// (if ty_left == ty_right)
163+
// ---------------
164+
// (has_type(env, Expr::Add(left, right)) => ty_left)
165+
// )
166+
// we generate this...
167+
let env = arg0.clone();
168+
if let Some(Expr::Add(left, right)) = arg1.downcast() {
169+
for ty_left in has_type(env, left) {
170+
for ty_right in has_type(env, right) {
171+
if ty_left == ty_right {
172+
result.push(ty_left);
173+
}
174+
}
175+
}
176+
}
177+
```
178+
179+
If you want to see a real judgement, take a look at the one for [proving where clauses][`prove_wc`].
180+
181+
[`prove_wc`]: https://github.com/rust-lang/a-mir-formality/blob/bca36ecd069d6bdff77bffbb628ae3b2ef4f8ef7/crates/formality-prove/src/prove/prove_wc.rs#L21-L125
182+
183+
### Handling cycles
184+
185+
Judgment functions must be **inductive**, which means that cycles are considered failures. We have a tabling implementation, which means we detect cycles and try to handle them intelligently. Basically we track a stack and, if a cycle is detected, we return an empty set of results. But we remember that the cycle happened. Then, once we are done, we'll have computed some intermediate set of results `R[0]`, and we execute again. This time, when we get the cycle, we return `R[0]` instead of an empty set. This will compute some new set of results, `R[1]`. So then we try again. We keep doing this until the new set of results `R[i]` is equal to the previous set of results `R[i-1]`. At that point, we have reached a fixed point, so we stop. Of course, it could be that you get an infinitely growing set of results, and execution never terminates. This means your rules are broken. Don't do that.

book/src/formality_core/lang.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Defining your language
2+
3+
The very first thing you do to define your own language is to use the `formality_core::declare_language!` macro.
4+
You use it like so:
5+
6+
```rust
7+
{{#include ../../../crates/formality-types/src/lib.rs:declare_rust_language}}
8+
```
9+
10+
The `declare_language` macro will create a module with the name you specify (here, `rust`).
11+
You have to tell it a few things:
12+
13+
* The `NAME` of your language, a string for debugging.
14+
* An enum defining the *kinds* of variables in your language; see the [variables](./variables.md) chapter for more information. For Rust, the kinds are types, lifetimes, and constants.
15+
* An enum defining a *parameter*, which is the terms that can be used to represent the *value* of a variable; see the [variables](./variables.md) chapter for more information.
16+
* Two characters `BINDER_OPEN` and `BINDER_CLOSED` defining the opening and closing characters for binders, e.g., `<` and `>`, to use when parsing.
17+
18+
## Contents of the language module
19+
20+
The language module you create has various items in it:
21+
22+
* A `struct FormalityLang` that defines your language. Some of the contains of `formality_core` (notably the traits that involve bound variables) are
23+
24+
## Specifying the language for a crate
25+
26+
That module will contain a language struct named `FormalityLang`. It
27+
Other parts of the formality system (e.g., autoderives and the like)
28+
will need to know the current language you are defining,
29+
and they expect to find it at `crate::FormalityLang`.
30+
Best practice is to add a `use` at the top of your crate defining your language.
31+
For example, the `formality_types` crate has:
32+
33+
```rust
34+
{{#include ../../../crates/formality-types/src/lib.rs:use_rust_language}}
35+
```
36+
37+
and other crates like `formality_rust` have:
38+
39+
```rust
40+
{{#include ../../../crates/formality-rust/src/lib.rs:use_rust_language}}
41+
```

book/src/formality_core/terms.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Defining terms with the `term` macro
2+
3+
There are two or three key things to understand. The first is the [`#[term]`][term] macro. This is a procedural macro that you can attach to a `struct` or `enum` declaration that represents a piece of Rust syntax or part of the trait checking rules. It auto-derives a bunch of traits and functionality...
4+
5+
[term]: https://github.com/rust-lang/a-mir-formality/blob/bca36ecd069d6bdff77bffbb628ae3b2ef4f8ef7/crates/formality-macros/src/term.rs#L14-L36
6+
7+
* rules for parsing from a string
8+
* a `Debug` impl to serialize back out
9+
* folding and substitution
10+
* upcasting and downcasting impls for convenient type conversion
11+
12+
For some types, we opt not to use `#[term]`, and instead implement the traits by hand. There are also derives so you can derive some of the traits but not all.
13+
14+
### Using `#[term]`
15+
16+
Let's do a simple example. If we had a really simple language, we might define expressions like this:
17+
18+
```rust
19+
#[term]
20+
enum Expr {
21+
#[cast]
22+
Variable(Variable),
23+
24+
#[grammar($v0 + $v1)]
25+
Add(Box<Expr>, Box<Expr>),
26+
}
27+
28+
#[term($name)]
29+
struct Variable {
30+
name: String
31+
}
32+
```
33+
34+
The `#[term]` macro says that these are terms and we should generate all the boilerplate automatically. Note that it will generate code that references `crate::FormalityLang` so be sure to [define your language appropriately](./lang.md).
35+
36+
The `#[term]` also accepts some internal annotations:
37+
38+
* `#[cast]` can be applied on an enum variant with a single argument. It says that we should generate upcast/downcast impls to allow conversion. In this case, between a `Variable` and an `Expr`. This would generate an impl `Variable: Upcast<Expr>` that lets you convert a variable to an expression, and a downcast impl `Expr: Downcast<Variable>` that lets you try to convert from an expression to a variable. Downcasting is *fallible*, which means that the downcast will only succeed if this is an `Expr::Variable`. If this is a `Expr::Add`, the downcast would return `None`.
39+
* `#[grammar]` tells the parser and pretty printer how to parse this variant. The `$v0` and `$v1` mean "recursively parse the first and second arguments". This will parse a `Box<Expr>`, which of course is implemented to just parse an `Expr`.
40+
41+
If you are annotating a struct, the `#[term]` just accepts the grammar directly, so `#[term($name)] struct Variable` means "to parse a variable, just parse the name field (a string)".
42+
43+
We could also define types and an environment, perhaps something like
44+
45+
```rust
46+
#[term]
47+
enum Type {
48+
Integer, // Default grammar is just the word `integer`
49+
String // Default grammar is just the word `string`
50+
}
51+
52+
#[term] // Default grammar is just `env($bindings)`
53+
struct Env {
54+
bindings: Set<(Variable, Type)>
55+
}
56+
```
57+
58+
You can see that the `#[term]` macro will generate some default parsing rules if you don't say anything.
59+
60+
We can then write code like
61+
62+
```rust
63+
let env: Env = term("env({(x, integer)})");
64+
```
65+
66+
This will parse the string, panicking if either the string cannot be parsed or or if it is ambiguous (can be parsing in mutiple ways). This is super useful in tests.
67+
68+
These terms are just Rust types, so you can define methods in the usual way, e.g. this `Env::get` method will search for a variable named `v`:
69+
70+
```rust
71+
impl Env {
72+
pub fn get(&self, v: &Variable) -> Option<&Type> {
73+
self.bindings.iter()
74+
.filter(|b| &b.0 == v)
75+
.map(|b| &b.1)
76+
.next()
77+
}
78+
}
79+
```

0 commit comments

Comments
 (0)