Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
191 changes: 189 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,190 @@ 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 4 possible return annotations for struct fields:

- `returns(clone)` (**the default**): Invokes `Clone::clone` on the field type.
- `returns(ref)`: Returns a `&Type` reference to the field type.
- `returns(deref)`: Invokes `Deref::deref` on the field type.
- `returns(copy)`: Returns an owned copy of the field value.

In order to better illustrate the workings of each `returns` annotation, we will
use this simple struct example:

```rust
/// Number wraps an i32 and is Copy.
#[derive(PartialEq, Eq, Copy, Debug)]
struct Number(i32);

// Dummy clone implementation that logs the Clone::clone call.
impl Clone for Number {
fn clone(&self) -> Self {
println!("Cloning {self:?}...");
Number(self.0)
}
}

// Deref into the wrapped i32 and log the call.
impl std::ops::Deref for Number {
type Target = i32;

fn deref(&self) -> &Self::Target {
println!("Dereferencing {self:?}...");
&self.0
}
}
```

The `Number` struct can be copied, cloned or derefed. Now let's define a salsa
struct with a `Number` field so we can explain the different `returns`
annotations:

```rust
#[salsa::input]
struct Input {
number: Number,
}
```

We will also need a salsa db for this example:

```rust
/// Salsa database to use in our example.
#[salsa::db]
#[derive(Clone, Default)]
struct NumDb {
storage: salsa::Storage<Self>,
}

#[salsa::db]
impl salsa::Database for NumDb {}
```

And finally a little program to test each annotation:

```rust
let db: NumDb = Default::default();
let input = Input::new(&db, Number(42));

// Access the number field.
let number = input.number(&db);
eprintln!("number: {number:?}");
```

### `returns(clone)` (default behavior)

If our field contains no `returns` attribute at all or contains the
`returns(clone)` then accessing the field returns a clone.

Implicit clone:

```rust
#[salsa::input]
struct Input {
number: Number,
}
```

Explicit clone:


```rust
#[salsa::input]
struct Input {
#[returns(clone)]
number: Number,
}
```

Output of the program in both cases is:

```
Cloning Number(42)...
number: Number(42)
```

Type of the `number` variable is:

```rust
let number: Number = input.number(&db);
```

### `returns(ref)`

This attribute simply returns a reference instead of calling `Clone::clone`:


```rust
#[salsa::input]
struct Input {
#[returns(ref)]
number: Number,
}
```

Output:

```
number: Number(42)
```

Type of `number`:

```rust
let number: &Number = input.number(&db);
```

### `returns(deref)`

Using this annotation invokes our implementation of `std::ops::Deref`, therefore
also changing the returned type in this example to `&i32` which is our `&Deref::Target`.

```rust
#[salsa::input]
struct Input {
#[returns(deref)]
number: Number,
}
```

Output:

```
Dereferencing Number(42)...
number: 42
```

Type of `number`:

```rust
let number: &i32 = input.number(&db);
```

### `returns(copy)`

If the field type is `Copy`, then `returns(copy)` can returned an owned copy of
the value without calling `Clone::clone`.

```rust
#[salsa::input]
struct Input {
#[returns(copy)]
number: Number,
}
```

Output:

```
number: Number(42)
```

Type of `number`:

```rust
let number: Number = input.number(&db);
```
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.

I find this section too long. I think what I would do instead is use an existing definition from the ir file or we define a made up struct and use the simple bullet list above to explain the different return types:

  • clone: Reading the field returns a clone (requires Clone), e.g. Option<String>
  • copy: Reading the field returns a copy (requires Copy), e.g. Option<String>
  • ref: Reading the field returns a reference, e.g. &Option<String>
  • deref: Reading the field returns the dereferenced field (requires SalsaAsDeref), e.g. Option<&str>
  • asref: Reading the ... (requires SalsaAsRef), e.g. Option<&String>

Loading
Loading