Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
63 changes: 61 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 Expand Up @@ -177,3 +177,62 @@ whenever anything in a function body changes, we consider the entire function bo
It usually makes sense to draw some kind of "reasonably coarse" boundary like this.

One downside of the way we have set things up: we inlined the position into each of the structs.

## The `returns` attribute for struct fields

You may have noticed that some fields of salsa structs are annotated with
`returns(ref)`. There are 6 possible return annotations for struct fields.
Some of them require implementing standard library traits or `salsa` specific
traits.

Given this salsa struct definition:

```rust
/// Number wraps an i32 and implements Copy & Clone required for
/// `returns(copy)` and `returns(clone)`.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
struct Number(i32);

/// Deref is required for `returns(deref)`.
impl std::ops::Deref for Number {
type Target = i32;

fn deref(&self) -> &Self::Target {
&self.0
}
}

/// SalsaAsDeref is required for `returns(as_deref)`.
impl salsa::SalsaAsDeref for Number {
type AsDeref<'a> = i32;

fn as_deref(&self) -> i32 {
self.0
}
}

/// SalsaAsRef is required for `returns(as_ref)`.
impl salsa::SalsaAsRef for Number {
type AsRef<'a> = &'a i32;

fn as_ref(&self) -> &i32 {
&self.0
}
}

/// Salsa struct.
#[salsa::input]
struct Input {
#[returns(clone)] // We can use the different returns annotations here.
number: Number,
}
```

We can use one of these annotations for the `Input::number` field:

- `returns(clone)` (**the default**): Reading the field returns a clone: `Number`. Requires `Clone`.
- `returns(ref)`: Reading the field returns a ref: `&Number`.
- `returns(copy)`: Reading the field returns a Copy: `Number`. Requires `Copy`.
- `returns(deref)`: Reading the field returns the dereferenced value: `&i32`. Requires `Deref`.
- `returns(as_ref)`: Reading the field returns a ref to internal values: `&i32`. Requires `SalsaAsRef`.
- `returns(as_deref)`: Reading the field returns the dereferenced value: `i32`. Requires `SalsaAsDeref`.
25 changes: 16 additions & 9 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 @@ -57,12 +57,19 @@ Tracked functions may take other arguments as well, though our examples here do
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.)
## The `returns` attribute for functions

You may have noticed that `parse_statements` is tagged with `#[salsa::tracked]`.
Ordinarily, when you call a tracked function, the result you get back
**is cloned** out of the database. You may recall the different `returns(_)`
annotations from the [IR](./ir.md#the-returns-attribute-for-struct-fields) chapter.

They all apply to salsa functions the same way:

- `salsa::tracked(returns(clone))` (**the default**): Returns a clone of the return type. Requires `Clone`.
- `salsa::tracked(returns(ref))`: Returns a ref to the return type.
- `salsa::tracked(returns(copy))`: Returns a copy. Requires `Copy`.
- `salsa::tracked(returns(deref))`: Returns the result of calling `Deref::deref`. Requires `Deref`.
- `salsa::tracked(returns(as_ref))`: Returns the result of `SalsaAsRef::as_ref`. Requires `SalsaAsRef`.
- `salsa::tracked(returns(as_deref))`: Returns the result of `SalsaAsDeref::as_deref`. Requires `SalsaAsDeref`.
Loading