Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions book/src/tutorial/accumulators.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ If we did so, the error would only be reported the first time the function was c
on subsequent calls in the situation where the simply returns its memoized value.

Salsa defines a mechanism for managing this called an **accumulator**.
In our case, we define an accumulator struct called `Diagnostics` in the `ir` module:
In our case, we define an accumulator struct called `Diagnostic` in the `ir` module:

```rust
{{#include ../../../examples/calc/ir.rs:diagnostic}}
Expand All @@ -30,7 +30,7 @@ we invoke the associated `accumulated` function:

```rust
let accumulated: Vec<Diagnostic> =
parse_statements::accumulated::<Diagnostics>(db);
parse_statements::accumulated::<Diagnostic>(db);
// -----------
// Use turbofish to specify
// the diagnostics type.
Expand Down
2 changes: 2 additions & 0 deletions book/src/tutorial/checker.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Defining the checker

// TODO
113 changes: 98 additions & 15 deletions book/src/tutorial/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,119 @@

As the final part of the parser, we need to write some tests.
To do so, we will create a database, set the input source text, run the parser, and check the result.
Before we can do that, though, we have to address one question: how do we inspect the value of an interned type like `Expression`?
Before we can do that, though, we have to address one question: how do we inspect the value of a `salsa::tracked`
type like `Program`?

## The `DebugWithDb` trait
## The `salsa::Database::attach` method

Because an interned type like `Expression` just stores an integer, the traditional `Debug` trait is not very useful.
To properly print a `Expression`, you need to access the Salsa database to find out what its value is.
To solve this, `salsa` provides a `DebugWithDb` trait that acts like the regular `Debug`, but takes a database as argument.
For types that implement this trait, you can invoke the `debug` method.
This returns a temporary that implements the ordinary `Debug` trait, allowing you to write something like
Because a tracked type like `Program` just stores an integer, the traditional `Debug` trait is not very useful.
To properly print a `Program`, you need to access the Salsa database to find out what its value is.
To solve this, `salsa` provides the `debug` option when declaring tracked structs: `#[salsa::tracked(debug)]` which
creates an implementation of `Debug` that can access the values of `Program` in
the salsa database:

```rust
eprintln!("Expression = {:?}", expr.debug(db));
{{#include ../../../examples/calc/ir.rs:program}}
```

and get back the output you expect.
Specifying the `debug` option allows us to use our types in formatted strings,
but it's not enough to get the full value. Simply writing this code:

The `DebugWithDb` trait is automatically derived for all `#[input]`, `#[interned]`, and `#[tracked]` structs.
```rust
let db: CalcDatabaseImpl = Default::default();

let surce_text = "print 1 + 2";
let source_program = SourceProgram::new(db, source_text.to_string());

let statements = parse_statements(db, source_program);

println!("{:#?}", statements);
```

gives us this output:

```
Program {
[salsa id]: Id(800),
}
```

## Forwarding to the ordinary `Debug` trait
And that is because when `println!` calls `Debug::fmt` on our `statements` variable
of type `Program`, the `Debug::fmt` implementation has no access to the Salsa database
to inspect the values.

For consistency, it is sometimes useful to have a `DebugWithDb` implementation even for types, like `Op`, that are just ordinary enums. You can do that like so:
In order to allow `Debug::fmt` to access the database, we can call it inside a
function passed to `Database::attach` which sets a thread-local variable to the
`Database` value it was called on, allowing the debug implementation to access it
and inspect values:

```rust
{{#include ../../../examples/calc/ir.rs:op_debug_impl}}
let db: CalcDatabaseImpl = Default::default();

db.attach(|db| {
let surce_text = "print 1 + 2";
let source_program = SourceProgram::new(db, source_text.to_string());

let statements = parse_statements(db, source_program);

println!("{:#?}", statements);
})
```

Now we should see something like this:

```
Program {
[salsa id]: Id(800),
statements: [
Statement {
span: Span {
[salsa id]: Id(404),
start: 0,
end: 11,
},
data: Print(
Expression {
span: Span {
[salsa id]: Id(403),
start: 6,
end: 11,
},
data: Op(
Expression {
span: Span {
[salsa id]: Id(400),
start: 6,
end: 7,
},
data: Number(
1.0,
),
},
Add,
Expression {
span: Span {
[salsa id]: Id(402),
start: 10,
end: 11,
},
data: Number(
2.0,
),
},
),
},
),
},
],
}
```

## Writing the unit test

Now that we have our `DebugWithDb` impls in place, we can write a simple unit test harness.
The `parse_string` function below creates a database, sets the source text, and then invokes the parser:
Now that we know how to inspect all the values of Salsa structs, we can write a simple unit test harness.
The `parse_string` function below creates a database, sets the source text, and then invokes the parser
to get the statements and creates a formatted string with all the values:

```rust
{{#include ../../../examples/calc/parser.rs:parse_string}}
Expand Down
2 changes: 2 additions & 0 deletions book/src/tutorial/interpreter.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Defining the interpreter

// TODO
4 changes: 2 additions & 2 deletions book/src/tutorial/ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ in other words, you cannot change the values of a `salsa::Input`.

The `'db` lifetime also allows tracked structs to be implemented
using a pointer (versus the numeric id found in `salsa::input` structs).
This doesn't really effect you as a user except that it allows accessing fields from tracked structs—
This doesn't really affect you as a user except that it allows accessing fields from tracked structs—
a very common operation—to be optimized.

## Representing functions
Expand All @@ -113,7 +113,7 @@ The `Function` struct is going to be created by the parser to represent each of
{{#include ../../../examples/calc/ir.rs:functions}}
```

If we had created some `Function` instance `f`, for example, we might find that `the f.body` field changes
If we had created some `Function` instance `f`, for example, we might find that the `f.body` field changes
because the user changed the definition of `f`.
This would mean that we have to re-execute those parts of the code that depended on `f.body`
(but not those parts of the code that depended on the body of _other_ functions).
Expand Down
12 changes: 1 addition & 11 deletions book/src/tutorial/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This means that if the input changes, we will always re-parse the entire input a
We'll see later that this _doesn't_ mean we will always re-run the type checker and other parts of the compiler.

This trade-off makes sense because (a) parsing is very cheap, so the overhead of tracking and enabling finer-grained reuse doesn't pay off
and because (b) since strings are just a big blob-o-bytes without any structure, it's rather hard to identify which parts of the IR need to be reparsed.
and because (b) since strings are just a big blob-of-bytes without any structure, it's rather hard to identify which parts of the IR need to be reparsed.
Some systems do choose to do more granular reparsing, often by doing a "first pass" over the string to give it a bit of structure,
e.g. to identify the functions,
but deferring the parsing of the body of each function until later.
Expand All @@ -56,13 +56,3 @@ The **second** parameter to a tracked function is **always** some kind of Salsa
Tracked functions may take other arguments as well, though our examples here do not.
Functions that take additional arguments are less efficient and flexible.
It's generally better to structure tracked functions as functions of a single Salsa struct if possible.

### The `returns(ref)` annotation

You may have noticed that `parse_statements` is tagged with `#[salsa::tracked(returns(ref))]`.
Ordinarily, when you call a tracked function, the result you get back is cloned out of the database.
The `returns(ref)` attribute means that a reference into the database is returned instead.
So, when called, `parse_statements` will return an `&Vec<Statement>` rather than cloning the `Vec`.
This is useful as a performance optimization.
(You may recall the `returns(ref)` annotation from the [ir](./ir.md) section of the tutorial,
where it was placed on struct fields, with roughly the same meaning.)
Comment on lines -60 to -68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we move this block to the ir section and explain the returns(ref) (and deref etc) there

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to explain it in the parser.md file because ir only talks about structs (input, tracked, interned) whereas parser section talks about tracked functions, I think adding returns(ref) there makes more sense.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just explaining it in different words, I got confused by "parse_statements is tagged with returns(ref)" but it could say "if we tag parse_statements with returns(ref), then this happens".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I suggested moving it to ir is because many structs in the ir module have fields attributed with returns(ref)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that could make sense. I added an explanation of returns for both functions and structs in the parser section, it could be split into returns for structs in ir and then returns for functions in parser.

Loading