mirror of
https://github.com/salsa-rs/salsa.git
synced 2025-08-04 02:48:38 +00:00
Merge #308
308: Tracked proposal r=nikomatsakis a=nikomatsakis I sketched out a variation of the entity API that I am calling tracked (this is a hat-tip to Ember's terminology, cc `@wycats).` It is the mostly the same ideas, but repackaged in a way that I think is more intuitive. This was inspired by writing the previous tutorial and thinking how things could be a bit cleaner. To read the proposal, check out the "overview" page on the netlify preview. The tutorial and code have not been fully updated to match yet. cc #305 Co-authored-by: Niko Matsakis <niko@alum.mit.edu>
This commit is contained in:
commit
3ba3dbb19b
39 changed files with 1387 additions and 1098 deletions
|
@ -19,7 +19,6 @@ parking_lot = "0.12.1"
|
|||
rustc-hash = "1.0"
|
||||
smallvec = "1.0.0"
|
||||
oorandom = "11"
|
||||
|
||||
salsa-macros = { version = "0.17.0-pre.2", path = "components/salsa-macros" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -37,4 +36,5 @@ members = [
|
|||
"components/salsa-entity-mock",
|
||||
"components/salsa-entity-macros",
|
||||
"calc-example/calc",
|
||||
"salsa-2022-tests"
|
||||
]
|
|
@ -9,7 +9,7 @@
|
|||
- [Basic structure](./tutorial/structure.md)
|
||||
- [Jars and databases](./tutorial/jar.md)
|
||||
- [Defining the database struct](./tutorial/db.md)
|
||||
- [Defining the IR: interned and entity structs](./tutorial/ir.md)
|
||||
- [Defining the IR: the various "salsa structs"](./tutorial/ir.md)
|
||||
- [Defining the parser: memoized functions and inputs](./tutorial/parser.md)
|
||||
- [Defining the parser: reporting errors](./tutorial/accumulators.md)
|
||||
- [Defining the parser: debug impls and testing](./tutorial/debug.md)
|
||||
|
@ -17,9 +17,6 @@
|
|||
- [Defining the interpreter](./tutorial/interpreter.md)
|
||||
- [Reference](./reference.md)
|
||||
- [Algorithm](./reference/algorithm.md)
|
||||
- [`salsa::interned`](./reference/interned.md)
|
||||
- [`salsa::entity`](./reference/entity.md)
|
||||
- [`salsa::memoized`](./reference/memoized.md)
|
||||
- [Common patterns](./common_patterns.md)
|
||||
- [Selection](./common_patterns/selection.md)
|
||||
- [On-demand (Lazy) inputs](./common_patterns/on_demand_inputs.md)
|
||||
|
|
|
@ -2,99 +2,270 @@
|
|||
|
||||
{{#include caveat.md}}
|
||||
|
||||
This page contains a brief overview of the pieces of a salsa program. For a more detailed look, check out the [tutorial](./tutorial.md), which walks through the creation of an entire project end-to-end.
|
||||
This page contains a brief overview of the pieces of a salsa program.
|
||||
For a more detailed look, check out the [tutorial](./tutorial.md), which walks through the creation of an entire project end-to-end.
|
||||
|
||||
## Goal of Salsa
|
||||
|
||||
The goal of salsa is to support efficient **incremental recomputation**.
|
||||
salsa is used in rust-analyzer, for example, to help it recompile your program quickly as you type.
|
||||
|
||||
The basic idea of a salsa program is like this:
|
||||
|
||||
```rust
|
||||
let mut input = ...;
|
||||
loop {
|
||||
let output = your_program(&input);
|
||||
modify(&mut input);
|
||||
}
|
||||
```
|
||||
|
||||
You start out with an input that has some value.
|
||||
You invoke your program to get back a result.
|
||||
Some time later, you modify the input and invoke your program again.
|
||||
**Our goal is to make this second call faster by re-using some of the results from the first call.**
|
||||
|
||||
In reality, of course, you can have many inputs and "your program" may be many different methods and functions defined on those inputs.
|
||||
But this picture still conveys a few important concepts:
|
||||
|
||||
- Salsa separates out the "incremental computation" (the function `your_program`) from some outer loop that is defining the inputs.
|
||||
- Salsa gives you the tools to define `your_program`.
|
||||
- Salsa assumes that `your_program` is a purely deterministic function of its inputs, or else this whole setup makes no sense.
|
||||
- The mutation of inputs always happens outside of `your_program`, as part of this master loop.
|
||||
|
||||
## Database
|
||||
|
||||
Every salsa program has an omnipresent _database_, which stores all the data across revisions. As you change the inputs to your program, we will consult this database to see if there are old computations that can be reused. The database is also used to implement interning and other convenient features.
|
||||
Each time you run your program, salsa remembers the values of each computation in a **database**.
|
||||
When the inputs change, it consults this database to look for values that can be reused.
|
||||
The database is also used to implement interning (making a canonical version of a value that can be copied around and cheaply compared for equality) and other convenient salsa features.
|
||||
|
||||
## Memoized functions
|
||||
## Inputs
|
||||
|
||||
The most basic concept in salsa is a **memoized function**. When you mark a function as memoized, that indicates that you would like to store its value in the database:
|
||||
Every Salsa program begins with an **input**.
|
||||
Inputs are special structs that define the starting point of your program.
|
||||
Everything else in your program is ultimately a deterministic function of these inputs.
|
||||
|
||||
For example, in a compiler, there might be an input defining the contents of a file on disk:
|
||||
|
||||
```rust
|
||||
#[salsa::memoized]
|
||||
fn parse_module(db: &dyn Db, module: Module) -> Ast {
|
||||
...
|
||||
#[salsa::input]
|
||||
pub struct ProgramFile {
|
||||
pub path: PathBuf,
|
||||
pub contents: String,
|
||||
}
|
||||
```
|
||||
|
||||
When you call a memoized function, we first check if we can find the answer in the database. In that case, we return a clone of the saved answer instead of executing the function twice.
|
||||
|
||||
Sometimes you have memoized functions whose return type might be expensive to clone. In that case, you can mark the memoized function as `return_ref`. When you call a `return_ref` function, we will return a reference to the memoized result in the database:
|
||||
You create an input by using the `new` method.
|
||||
Because the values of input fields are stored in the database, you also give an `&mut`-reference to the database:
|
||||
|
||||
```rust
|
||||
#[salsa::memoized(return_ref)]
|
||||
fn module_text(db: &dyn Db, module: Module) -> &String {
|
||||
...
|
||||
}
|
||||
let file: ProgramFile = ProgramFile::new(
|
||||
&mut db,
|
||||
PathBuf::from("some_path.txt"),
|
||||
String::from("fn foo() { }"),
|
||||
);
|
||||
```
|
||||
|
||||
## Inputs and revisions
|
||||
### Salsa structs are just an integer
|
||||
|
||||
Each memoized function has an associated `set` method that can be used to set a return value explicitly. Memoized functions whose values are explicitly set are called _inputs_.
|
||||
The `ProgramFile` struct generates by the `salsa::input` macro doesn't actually store any data. It's just a newtyped integer id:
|
||||
|
||||
```rust
|
||||
fn load_module_source(db: &mut dyn Db, module: Module) {
|
||||
let source: String = load_source_text();
|
||||
module_text::set(db, module, source);
|
||||
// ^^^ set function!
|
||||
}
|
||||
// Generated by the `#[salsa::input]` macro:
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgramFile(salsa::Id);
|
||||
```
|
||||
|
||||
Often, inputs don't have a function body, but simply panic in the case that they are not set explicitly, but this is not required. For example, the `module_text` function returns the raw bytes for a module. This is likely not something we can compute from "inside" the system, so the definition might just panic:
|
||||
This means that, when you have a `ProgramFile`, you can easily copy it around and put it wherever you like.
|
||||
To actually read any of its fields, however, you will need to use the database and a getter method.
|
||||
|
||||
### Reading fields and `return_ref`
|
||||
|
||||
You can access the value of an input's fields by using the getter method.
|
||||
As this is only reading the field, it just needs a `&`-reference to the database:
|
||||
|
||||
```rust
|
||||
#[salsa::memoized(return_ref)]
|
||||
fn module_text(db: &dyn Db, module: Module) -> String {
|
||||
panic!("text for module `{module:?}` not set")
|
||||
}
|
||||
let contents: String = file.contents(&db);
|
||||
```
|
||||
|
||||
Each time you invoke `set`, you begin a new **revision** of the database. Each memoized result in the database tracks the revision in which it was computed; invoking `set` may invalidate memoized results, causing functions to be re-executed (see the reference for [more details on how salsa decides when a memoized result is outdated](./reference/algorithm.md)).
|
||||
|
||||
## Entity values
|
||||
|
||||
Entity structs are special structs whose fields are versioned and stored in the database. For example, the `Module` type that we have been passing around could potentially be declared as an entity:
|
||||
Invoking the accessor clones the value from the database.
|
||||
Sometimes this is not what you want, so you can annotate fields with `#[return_ref]` to indicate that they should return a reference into the database instead:
|
||||
|
||||
```rust
|
||||
#[salsa::entity]
|
||||
struct Module {
|
||||
#[salsa::input]
|
||||
pub struct ProgramFile {
|
||||
pub path: PathBuf,
|
||||
#[return_ref]
|
||||
path: String,
|
||||
pub contents: String,
|
||||
}
|
||||
```
|
||||
|
||||
A new module could be created with the `new` method:
|
||||
Now `file.contents(&db)` will return an `&String`.
|
||||
|
||||
You can also use the `data` method to access the entire struct:
|
||||
|
||||
```rust
|
||||
let m: Module = Module::new(db, "some_path".to_string());
|
||||
file.data(&db)
|
||||
```
|
||||
|
||||
Despite the struct declaration above, the actual `Module` struct is just a newtyped integer, guaranteed to be unique within this database revision. You can access fields via accessors like `m.path(db)` (the `#[return_ref]` attribute here indicates that a `path` returns an `&String`, and not a cloned `String`).
|
||||
### Writing input fields
|
||||
|
||||
## Interned values
|
||||
Finally, you can also modify the value of an input field by using the setter method.
|
||||
Since this is modifyingg the input, the setter takes an `&mut`-reference to the database:
|
||||
|
||||
In addition to entities, you can also declare _interned structs_ (and enums). Interned structs take arbitrary data and replace it with an integer. Unlike an entity, where each call to `new` returns a fresh integer, interning the same data twice gives back the same integer.
|
||||
```rust
|
||||
file.set_contents(String::from("fn foo() { /* add a comment */ }"));
|
||||
```
|
||||
|
||||
A common use for interning is to intern strings:
|
||||
## Tracked functions
|
||||
|
||||
Once you've defined your inputs, the next thing to define are **tracked functions**:
|
||||
|
||||
```rust
|
||||
#[salsa::tracked]
|
||||
fn parse_file(db: &dyn crate::Db, file: ProgramFile) -> Ast {
|
||||
let contents: &str = file.contents(db);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
When you call a tracked function, salsa will track which inputs it accesses (in this example, `file.contents(db)`).
|
||||
It will also memoize the return value (the `Ast`, in this case).
|
||||
If you call a tracked function twice, salsa checks if the inputs have changed; if not, it can return the memoized value.
|
||||
The algorithm salsa uses to decide when a tracked function needs to be re-executed is called the [red-green algorithm](./reference/algorithm.md), and it's where the name salsa comes from.
|
||||
|
||||
Tracked functions have to follow a particular structure:
|
||||
|
||||
- They must take a `&`-reference to the database as their first argument.
|
||||
- Note that because this is an `&`-reference, it is not possible to create or modify inputs during a tracked function!
|
||||
- They must take a "salsa struct" as the second argument -- in our example, this is an input struct, but there are other kinds of salsa structs we'll describe shortly.
|
||||
- They _can_ take additional arguments, but it's faster and better if they don't.
|
||||
|
||||
Tracked functions can return any clone-able type. A clone is required since, when the value is cached, the result will be cloned out of the database. Tracked functions can also be annotated with `#[return_ref]` if you would prefer to return a reference into the database instead (if `parse_file` were so annotated, then callers would actually get back an `&Ast`, for example).
|
||||
|
||||
## Tracked structs
|
||||
|
||||
**Tracked structs** are intermediate structs created during your computation.
|
||||
Like inputs, their fields are stored inside the database, and the struct itself just wraps an id.
|
||||
Unlike inputs, they can only be created inside a tracked function, and their fields can never change once they are created.
|
||||
Getter methods are provided to read the fields, but there are no setter methods[^specify]. Example:
|
||||
|
||||
```rust
|
||||
#[salsa::tracked]
|
||||
struct Ast {
|
||||
#[return_ref]
|
||||
top_level_items: Vec<Item>,
|
||||
}
|
||||
```
|
||||
|
||||
Just as with an input, new values are created by invoking `Ast::new`.
|
||||
Unlike with an input, the `new` for a tracked struct only requires a `&`-reference to the database:
|
||||
|
||||
```rust
|
||||
#[salsa::tracked]
|
||||
fn parse_file(db: &dyn crate::Db, file: ProgramFile) -> Ast {
|
||||
let contents: &str = file.contents(db);
|
||||
let parser = Parser::new(contents);
|
||||
let mut top_level_items = vec![];
|
||||
while let Some(item) = parser.parse_top_level_item() {
|
||||
top_level_items.push(item);
|
||||
}
|
||||
Ast::new(db, top_level_items) // <-- create an Ast!
|
||||
}
|
||||
```
|
||||
|
||||
### `#[id]` fields
|
||||
|
||||
When a tracked function is re-executed because its inputs have changed, the tracked structs it creates in the new execution are matched against those from the old execution, and the values of their fields are compared.
|
||||
If the field values have not changed, then other tracked functions that only read those fields will not be re-executed.
|
||||
|
||||
Normally, tracked structs are matched up by the order in which they are created.
|
||||
For example, the first `Ast` that is created by `parse_file` in the old execution will be matched against the first `Ast` created by `parse_file` in the new execution.
|
||||
In our example, `parse_file` only ever creates a single `Ast`, so this works great.
|
||||
Sometimes, however, it doesn't work so well.
|
||||
For example, imagine that we had a tracked struct for items in the file:
|
||||
|
||||
```rust
|
||||
#[salsa::tracked]
|
||||
struct Item {
|
||||
name: Word, // we'll define Word in a second!
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Maybe our parser first creates an `Item` with the name `foo` and then later a second `Item` with the name `bar`.
|
||||
Then the user changes the input to reorder the functions.
|
||||
Although we are still creating the same number of items, we are now creating them in the reverse order, so the naive algorithm will match up the _old_ `foo` struct with the new `bar` struct.
|
||||
This will look to salsa as though the `foo` function was renamed to `bar` and the `bar` function was renamed to `foo`.
|
||||
We'll still get the right result, but we might do more recomputation than we needed to do if we understood that they were just reordered.
|
||||
|
||||
To address this, you can tag fields in a tracked struct as `#[id]`. These fields are then used to "match up" struct instances across executions:
|
||||
|
||||
```rust
|
||||
#[salsa::tracked]
|
||||
struct Item {
|
||||
#[id]
|
||||
name: Word, // we'll define Word in a second!
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Specified the result of tracked functions for particular structs
|
||||
|
||||
Sometimes it is useful to define a tracked function but specify its value for some particular struct specially.
|
||||
For example, maybe the default way to compute the representation for a function is to read the AST, but you also have some built-in functions in your language and you want to hard-code their results.
|
||||
This can also be used to simulate a field that is initialized after the tracked struct is created.
|
||||
|
||||
To support this use case, you can use the `specify` method associated with tracked functions.
|
||||
To enable this method, you need to add the `specify` flag to the function to alert users that its value may sometimes be specified externally.
|
||||
|
||||
```rust
|
||||
#[salsa::tracked(specify)] // <-- specify flag required
|
||||
fn representation(db: &dyn crate::Db, item: Item) -> Representation {
|
||||
// read the user's input AST by default
|
||||
let ast = ast(db, item);
|
||||
// ...
|
||||
}
|
||||
|
||||
fn create_builtin_item(db: &dyn crate::Db) -> Item {
|
||||
let i = Item::new(db, ...);
|
||||
let r = hardcoded_representation();
|
||||
representation::specify(db, i, r); // <-- use the method!
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
Specifying is only possible for tracked functions that take a single tracked struct as argument (besides the database).
|
||||
|
||||
## Interned structs
|
||||
|
||||
The final kind of salsa struct are **interned structs**.
|
||||
Interned structs are useful for quick equality comparison.
|
||||
They are commonly used to represent strings or other primitive values.
|
||||
|
||||
Most compilers, for example, will define a type to represent a user identifier:
|
||||
|
||||
```rust
|
||||
#[salsa::interned]
|
||||
struct Word {
|
||||
#[return_ref]
|
||||
text: String
|
||||
pub text: String,
|
||||
}
|
||||
```
|
||||
|
||||
Interning the same value twice gives the same integer, so in this code...
|
||||
As with input and tracked structs, the `Word` struct itself is just a newtyped integer, and the actual data is stored in the database.
|
||||
|
||||
You can create a new interned struct using `new`, just like with input and tracked structs:
|
||||
|
||||
```rust
|
||||
let w1 = Word::new(db, "foo".to_string());
|
||||
let w2 = Word::new(db, "foo".to_string());
|
||||
let w2 = Word::new(db, "bar".to_string());
|
||||
let w3 = Word::new(db, "foo".to_string());
|
||||
```
|
||||
|
||||
...we know that `w1 == w2`.
|
||||
When you create two interned structs with the same field values, you are guaranted to get back the same integer id. So here, we know that `assert_eq!(w1, w3)` is true and `assert_ne!(w1, w2)`.
|
||||
|
||||
You can access the fields of an interned struct using a getter, like `word.text(db)`. These getters respect the `#[return_ref]` annotation. Like tracked structs, the fields of interned structs are immutable.
|
||||
|
||||
## Accumulators
|
||||
|
||||
|
@ -107,17 +278,17 @@ To create an accumulator, you declare a type as an _accumulator_:
|
|||
pub struct Diagnostics(String);
|
||||
```
|
||||
|
||||
It must be a newtype of something, like `String`. Now, during a memoized function's execution, you can push those values:
|
||||
It must be a newtype of something, like `String`. Now, during a tracked function's execution, you can push those values:
|
||||
|
||||
```rust
|
||||
Diagnostics::push(db, "some_string".to_string())
|
||||
```
|
||||
|
||||
Then later, from outside the execution, you can ask for the set of diagnostics that were accumulated by some particular memoized function. For example, imagine that we have a type-checker and, during type-checking, it reports some diagnostics:
|
||||
Then later, from outside the execution, you can ask for the set of diagnostics that were accumulated by some particular tracked function. For example, imagine that we have a type-checker and, during type-checking, it reports some diagnostics:
|
||||
|
||||
```rust
|
||||
#[salsa::memoized]
|
||||
fn type_check(db: &dyn Db, module: Module) {
|
||||
#[salsa::tracked]
|
||||
fn type_check(db: &dyn Db, item: Item) {
|
||||
// ...
|
||||
Diagnostics::push(db, "some error message".to_string())
|
||||
// ...
|
||||
|
|
|
@ -8,18 +8,18 @@ The salsa database always tracks a single **revision**. Each time you set an inp
|
|||
|
||||
### Basic rule: when inputs change, re-execute!
|
||||
|
||||
When you invoke a memoized function, in addition to storing the value that was returned, we also track what _other_ memoized functions it depends on, and the revisions when their value last changed. When you invoke the function again, if the database is in a new revision, then we check whether any of the inputs to this function have changed in that new revision. If not, we can just return our cached value. But if the inputs _have_ changed (or may have changed), we will re-execute the function to find the most up-to-date answer.
|
||||
When you invoke a tracked function, in addition to storing the value that was returned, we also track what _other_ tracked functions it depends on, and the revisions when their value last changed. When you invoke the function again, if the database is in a new revision, then we check whether any of the inputs to this function have changed in that new revision. If not, we can just return our cached value. But if the inputs _have_ changed (or may have changed), we will re-execute the function to find the most up-to-date answer.
|
||||
|
||||
Here is a simple example, where the `parse_module` function invokes the `module_text` function:
|
||||
|
||||
```rust
|
||||
#[salsa::memoized]
|
||||
#[salsa::tracked]
|
||||
fn parse_module(db: &dyn Db, module: Module) -> Ast {
|
||||
let module_text: &String = module_text(db, module);
|
||||
Ast::parse_text(module_text)
|
||||
}
|
||||
|
||||
#[salsa::memoized(ref)]
|
||||
#[salsa::tracked(ref)]
|
||||
fn module_text(db: &dyn Db, module: Module) -> String {
|
||||
panic!("text for module `{module:?}` not set")
|
||||
}
|
||||
|
@ -47,10 +47,10 @@ parse_module(db, module); // executes again!
|
|||
|
||||
### Backdating: sometimes we can be smarter
|
||||
|
||||
Often, though, memoized functions don't depend directly on the inputs. Instead, they'll depend on some _processed_ version. For example, perhaps we have a `type_check` function that reads the AST:
|
||||
Often, though, tracked functions don't depend directly on the inputs. Instead, they'll depend on some other tracked function. For example, perhaps we have a `type_check` function that reads the AST:
|
||||
|
||||
```rust
|
||||
#[salsa::memoized]
|
||||
#[salsa::tracked]
|
||||
fn type_check(db: &dyn Db, module: Module) {
|
||||
let ast = parse_module(db, module);
|
||||
...
|
||||
|
@ -64,7 +64,7 @@ If the module text is changed, we saw that we have to re-execute `parse_module`,
|
|||
|
||||
## Durability: an optimization
|
||||
|
||||
As an optimization, salsa includes the concept of **durability**. When you set the value of a memoized function, you can also set it with a given _durability_:
|
||||
As an optimization, salsa includes the concept of **durability**. When you set the value of a tracked function, you can also set it with a given _durability_:
|
||||
|
||||
```rust
|
||||
module_text::set_with_durability(
|
||||
|
@ -75,6 +75,6 @@ module_text::set_with_durability(
|
|||
);
|
||||
```
|
||||
|
||||
For each durability, we track the revision in which _some input_ with that durability changed. If a memoized function depends (transitively) only on high durability inputs, and you change a low durability input, then we can very easily determine that the memoized function result is still valid, avoiding the need to traverse the input edges one by one.
|
||||
For each durability, we track the revision in which _some input_ with that durability changed. If a tracked function depends (transitively) only on high durability inputs, and you change a low durability input, then we can very easily determine that the tracked function result is still valid, avoiding the need to traverse the input edges one by one.
|
||||
|
||||
An example: if compiling a Rust program, you might mark the inputs from crates.io as _high durability_ inputs, since they are unlikely to change. The current workspace could be marked as _low durability_.
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
# `salsa::entity`
|
|
@ -1 +0,0 @@
|
|||
# `salsa::interned`
|
|
@ -1 +0,0 @@
|
|||
# `salsa::memoized`
|
|
@ -1,81 +1,88 @@
|
|||
# Defining the IR
|
||||
|
||||
Before we can define the [parser](./parser.md), we need to define the intermediate representation (IR) that we will use for `calc` programs.
|
||||
In the [basic structure](./structure.md), we defined some "pseudo-Rust" structures like `Statement`, `Expression`, and so forth, and now we are going to define them for real.
|
||||
In the [basic structure](./structure.md), we defined some "pseudo-Rust" structures like `Statement` and `Expression`;
|
||||
now we are going to define them for real.
|
||||
|
||||
## Interning
|
||||
## "Salsa structs"
|
||||
|
||||
We'll start with `FunctionId` and `VariableId`.
|
||||
These were two different names for interned strings.
|
||||
Interning is a builtin feature to salsa where you take a complex data structure (in this case, a string) and replace it with a single integer.
|
||||
The integer you get back is arbitrary, but whenever you intern the same thing twice (within one revision -- more on this caveat later), you get back the same integer.
|
||||
In addition to regular Rust types, we will make use of various **salsa structs**.
|
||||
A salsa struct is a struct that has been annotated with one of the salsa annotations:
|
||||
|
||||
Interned structs in Salsa are defined with the `#[salsa::interned]` attribute macro:
|
||||
* [`#[salsa::input]`](#input-structs), which designates the "base inputs" to your computation;
|
||||
* [`#[salsa::tracked]`](#tracked-structs), which designate intermediate values created during your computation;
|
||||
* [`#[salsa::interned]`](#interned-structs), which designate small values that are easy to compare for equality.
|
||||
|
||||
All salsa structs store the actual values of their fields in the salsa database.
|
||||
This permits us to track when the values of those fields change to figure out what work will need to be re-executed.
|
||||
|
||||
When you annotate a struct with one of the above salsa attributes, salsa actually generates a bunch of code to link that struct into the database.
|
||||
This code must be connected to some [jar](./jar.md).
|
||||
By default, this is `crate::Jar`, but you can specify a different jar with the `jar=` attribute (e.g., `#[salsa::input(jar = MyJar)]`).
|
||||
You must also list the struct in the jar definition itself, or you will get errors.
|
||||
|
||||
## Input structs
|
||||
|
||||
The first thing we will define is our **input**.
|
||||
Every salsa program has some basic inputs that drive the rest of the computation.
|
||||
The rest of the program must be some deterministic function of those base inputs,
|
||||
such that when those inputs change, we can try to efficiently recompute the new result of that function.
|
||||
|
||||
Inputs are defined as Rust structs with a `#[salsa::input]` annotation:
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/ir.rs:interned_ids}}
|
||||
{{#include ../../../calc-example/calc/src/ir.rs:input}}
|
||||
```
|
||||
|
||||
Note though that the structs that result from this declaration are _very different_ from what you wrote. When you intern a struct, the actual _data_ of the struct is stored in the salsa database, and the struct you get back is just a lightweight struct that wraps a `salsa::Id` struct. Since this struct just wraps a simple integer, it is `Copy`, `Eq`, `Hash`, and `Ord`:
|
||||
In our compiler, we have just one simple input, the `ProgramSource`, which has a `text` field (the string).
|
||||
|
||||
### The data lives in the database
|
||||
|
||||
Although they are declared like other Rust structs, salsa structs are implemented quite differently.
|
||||
The values of their fields are stored in the salsa database, and the struct itself just contains a numeric identifier.
|
||||
This means that the struct instances are copy (no matter what fields they contain).
|
||||
Creating instances of the struct and accessing fields is done by invoking methods like `new` as well as getters and setters.
|
||||
|
||||
More concretely, the `#[salsa::input]` annotation will generate a struct for `ProgramSource` like this:
|
||||
|
||||
```rust
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash)]
|
||||
pub struct VariableId(salsa::Id);
|
||||
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash)]
|
||||
pub struct FunctionId(salsa::Id);
|
||||
#[define(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProgramSource(salsa::Id);
|
||||
```
|
||||
|
||||
These interned structs also have a few methods:
|
||||
|
||||
- The `new` method creates an interned struct given a database `db` and a value for each field (e.g., `let v = VariableId::new(db, "foo".to_string())`).
|
||||
- For each field of the interned struct, there is an accessor method with the same name (e.g., `v.name(db)`).
|
||||
- If the field is marked with `#[id(ref)]`, as it is here, than this accessor returns a reference! In this case, it returns an `&String` tied to the database `db`. This is useful when the values of the fields are not `Copy`.
|
||||
- The `id` here refers to the fact that the value of this field is part of the _identity_, i.e., it affects the `salsa::Id` integer. For interned structs, all fields are `id` fields, but later on we'll see entity structs, which also have `value` fields.
|
||||
|
||||
### The data struct
|
||||
|
||||
In addition to the main `VariableId` and `FunctionId` structs, the `salsa::interned` macro also creates a "data struct".
|
||||
This is normally named the same as the original struct, but with `Data` appended to the end (i.e., `VariableIdData` and `FunctionIdData`).
|
||||
You can override the name by using the `data` option (`#[salsa::interned(data = MyName)]`).
|
||||
The data struct also inherits all the `derive` and other attributes from the original source.
|
||||
In the case of our examples, the generated data struct would be something like:
|
||||
It will also generate a method `new` that lets you create a `ProgramSource` in the database.
|
||||
For an input, a `&mut db` reference is required, along with the values for each field:
|
||||
|
||||
```rust
|
||||
// Generated by `salsa::interned`
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Hash)]
|
||||
pub struct VariableIdData {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Hash)]
|
||||
pub struct FunctionIdData {
|
||||
pub text: String,
|
||||
}
|
||||
let source = ProgramSource::new(&mut db, "print 11 + 11".to_string());
|
||||
```
|
||||
|
||||
## Expressions and statements
|
||||
You can read the value of the field with `source.text(&db)`,
|
||||
and you can set the value of the field with `source.set_text(&mut db, "print 11 * 2".to_string())`.
|
||||
|
||||
We'll also intern expressions and statements. This is convenient primarily because it allows us to have recursive structures very easily. Since we don't really need the "cheap equality comparison" aspect of interning, this isn't the most efficient choice, and many compilers would opt to represent expressions/statements in some other way.
|
||||
### Database revisions
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/ir.rs:statements_and_expressions}}
|
||||
```
|
||||
Whenever a function takes an `&mut` reference to the database,
|
||||
that means that it can only be invoked from outside the incrementalized part of your program,
|
||||
as explained in [the overview](../overview.md#goal-of-salsa).
|
||||
When you change the value of an input field, that increments a 'revision counter' in the database,
|
||||
indicating that some inputs are different now.
|
||||
When we talk about a "revision" of the database, we are referring to the state of the database in between changes to the input values.
|
||||
|
||||
## Function entities
|
||||
## Tracked structs
|
||||
|
||||
The final piece of our IR is the representation of _functions_. Here, we use an _entity struct_:
|
||||
Next we will define a **tracked struct** to represent the functions in our input.
|
||||
Whereas inputs represent the *start* of a computation, tracked structs represent intermediate values created during your computation.
|
||||
In this case, we are going to parse the raw input program, and create a `Function` for each of the functions defined by the user.
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/ir.rs:functions}}
|
||||
```
|
||||
|
||||
An **entity** is very similar to an interned struct, except that it has own identity. That is, each time you invoke `Function::new`, you will get back a new `Function, even if all the values of the fields are equal.
|
||||
Unlike with inputs, the fields of tracked structs are immutable once created. Otherwise, working with a tracked struct is quite similar to an input:
|
||||
|
||||
### Interning vs entities
|
||||
|
||||
Unless you want a cheap way to compare for equality across functions, you should prefer entities to interning. They correspond most closely to creating a "new" struct.
|
||||
* You can create a new value by using `new`, but with a tracked struct, you only need an `&dyn` database, not `&mut` (e.g., `Function::new(&db, some_name, some_args, some_body)`)
|
||||
* You use a getter to read the value of a field, just like with an input (e.g., `my_func.args(db)` to read the `args` field).
|
||||
|
||||
### id fields
|
||||
|
||||
|
@ -84,3 +91,46 @@ Normally, you would do this on fields that represent the "name" of an entity.
|
|||
This indicates that, across two revisions R1 and R2, if two functions are created with the same name, they refer to the same entity, so we can compare their other fields for equality to determine what needs to be re-executed.
|
||||
Adding `#[id]` attributes is an optimization and never affects correctness.
|
||||
For more details, see the [algorithm](../reference/algorithm.md) page of the reference.
|
||||
|
||||
## Interned structs
|
||||
|
||||
The final kind of salsa struct are *interned structs*.
|
||||
As with input and tracked structs, the data for an interned struct is stored in the database, and you just pass around a single integer.
|
||||
Unlike those structs, if you intern the same data twice, you get back the **same integer**.
|
||||
|
||||
A classic use of interning is for small strings like function names and variables.
|
||||
It's annoying and inefficient to pass around those names with `String` values which must be cloned;
|
||||
it's also inefficient to have to compare them for equality via string comparison.
|
||||
Therefore, we define two interned structs, `FunctionId` and `VariableId`, each with a single field that stores the string:
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/ir.rs:interned_ids}}
|
||||
```
|
||||
|
||||
When you invoke e.g. `FunctionId::new(&db, "my_string".to_string())`, you will get back a `FunctionId` that is just a newtype'd integer.
|
||||
But if you invoke the same call to `new` again, you get back the same integer:
|
||||
|
||||
```rust
|
||||
let f1 = FunctionId::new(&db, "my_string".to_string());
|
||||
let f2 = FunctionId::new(&db, "my_string".to_string());
|
||||
assert_eq!(f1, f2);
|
||||
```
|
||||
|
||||
### Expressions and statements
|
||||
|
||||
We'll also intern expressions and statements. This is convenient primarily because it allows us to have recursive structures very easily. Since we don't really need the "cheap equality comparison" aspect of interning, this isn't the most efficient choice, and many compilers would opt to represent expressions/statements in some other way.
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/ir.rs:statements_and_expressions}}
|
||||
```
|
||||
|
||||
### Interned ids are guaranteed to be consistent within a revision, but not across revisions (but you don't have to care)
|
||||
|
||||
Interned ids are guaranteed not to change within a single revision, so you can intern things from all over your program and get back consistent results.
|
||||
When you change the inputs, however, salsa may opt to clear some of the interned values and choose different integers.
|
||||
However, if this happens, it will also be sure to re-execute every function that interned that value, so all of them still see a consistent value,
|
||||
just a different one than they saw in a previous revision.
|
||||
|
||||
In other words, within a salsa computation, you can assume that interning produces a single consistent integer, and you don't have to think about it.
|
||||
If however you export interned identifiers outside the computation, and then change the inputs, they may not longer be valid or may refer to different values.
|
||||
|
||||
|
|
|
@ -1,38 +1,36 @@
|
|||
# Jars and databases
|
||||
|
||||
Salsa programs are composed in **jars**[^jar].
|
||||
A **jar** is the salsa version of a Rust crate or module.
|
||||
It is a struct that contains the memoized results for some subset of your program.
|
||||
Before we can define the interesting parts of our salsa program, we have to setup a bit of structure that defines the salsa **database**.
|
||||
The database is a struct that ultimately stores all of salsa's intermediate state, such as the memoized return values from [tracked functions].
|
||||
|
||||
Typically you define one jar per crate in your program, and you define it at the path `crate::Jar`.
|
||||
You don't have to do this, but it's more convenient because the various salsa macros have defaults that expect the jar to be at this location.
|
||||
[tracked functions]: ../overview.md#tracked-functions
|
||||
|
||||
Our `calc` example has only a single crate, but we'll still put the `Jar` struct at the root of the crate:
|
||||
The database itself is defined in terms of intermediate structures, called called **jars**[^jar], which themselves contain the data for each function.
|
||||
This setup allows salsa programs to be divided amongst many crates.
|
||||
Typically, you define one jar struct per crate, and then when you construct the final database, you simply list the jar structs.
|
||||
This permits the crates to define private functions and other things that are members of the jar struct, but not known directly to the database.
|
||||
|
||||
[^jar]: Jars of salsa -- get it? Get it??[^java]
|
||||
|
||||
[^java]: OK, maybe it also brings to mind Java `.jar` files, but there's no real relationship. A jar is just a Rust struct, not a packaging format.
|
||||
|
||||
## Defining a jar struct
|
||||
|
||||
To define a jar struct, you create a tuple struct with the `#[salsa::jar]` annotation:
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/main.rs:jar_struct}}
|
||||
```
|
||||
|
||||
The `#[salsa::jar]` annotation indicates that this struct is a Salsa jar.
|
||||
The struct must be a tuple struct, and the fields in the struct correspond to the salsa [memoized functions], [entities], and other concepts that we are going to introduce in this tutorial.
|
||||
The idea is that the field type will contain the storage needed to implement that particular salsa-ized thing.
|
||||
Although it's not required, it's highly recommended to put the `jar` struct at the root of your crate, so that it can be referred to as `crate::Jar`.
|
||||
All of the other salsa annotations reference a jar struct, and they all default to the path `crate::Jar`.
|
||||
If you put the jar somewhere else, you will have to override that default.
|
||||
|
||||
[memoized functions]: ../reference/memoized.md
|
||||
[entities]: ../reference/entity.md
|
||||
## Defining the database trait
|
||||
|
||||
The `salsa::jar` annotation also has a parameter, `db = Db`.
|
||||
In general, salsa annotations take arguments of this form.
|
||||
This particular argument is mandatory, so you'll get an error if you leave it out.
|
||||
It identifies the **database trait** for this jar.
|
||||
|
||||
[^jar]: Jars of salsa -- get it? Get it??
|
||||
|
||||
## Database trait for the jar
|
||||
|
||||
Whereas a salsa jar contains all the storage needed for a particular crate,
|
||||
the salsa **database** is a struct that contains all the storage needed for an entire program.
|
||||
Jars, however, don't refer directly to this database struct.
|
||||
Instead, each jar defines a trait, typically called `Db`, that the struct must implement.
|
||||
The `#[salsa::jar]` annotation also includes a `db = Db` field.
|
||||
The value of this field (normally `Db`) is the name of a trait that represents the database.
|
||||
Salsa programs never refer *directly* to the database; instead, they take a `&dyn Db` argument.
|
||||
This allows for separate compilation, where you have a database that contains the data for two jars, but those jars don't depend on one another.
|
||||
|
||||
The database trait for our `calc` crate is very simple:
|
||||
|
@ -61,3 +59,14 @@ and that's what we do here:
|
|||
```rust
|
||||
{{#include ../../../calc-example/calc/src/main.rs:jar_db_impl}}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
If the concept of a jar seems a bit abstract to you, don't overthink it. The TL;DR is that when you create a salsa program, you need to do:
|
||||
|
||||
- In each of your crates:
|
||||
- Define a `#[salsa::jar(db = Db)]` struct, typically at `crate::Jar`, and list each of your various salsa-annotated things inside of it.
|
||||
- Define a `Db` trait, typically at `crate::Db`, that you will use in memoized functions and elsewhere to refer to the database struct.
|
||||
- Once, typically in your final crate:
|
||||
- Define a database `D`, as described in the [next section](./db.md), that will contain a list of each of the jars for each of your crates.
|
||||
- Implement the `Db` traits for each jar for your database type `D` (often we do this through blanket impls in the jar crates).
|
||||
|
|
|
@ -1,102 +1,73 @@
|
|||
# Defining the parser: memoized functions and inputs
|
||||
|
||||
The next step in the `calc` compiler is to define the parser.
|
||||
The role of the parser will be to take the raw bytes from the input and create the `Statement`, `Function`, and `Expression` structures that [we defined in the `ir` module](./ir.md).
|
||||
The role of the parser will be to take the `ProgramSource` input,
|
||||
read the string from the `text` field,
|
||||
and create the `Statement`, `Function`, and `Expression` structures that [we defined in the `ir` module](./ir.md).
|
||||
|
||||
To minimize dependencies, we are going to write a [recursive descent parser][rd].
|
||||
Another option would be to use a [Rust parsing framework](https://rustrepo.com/catalog/rust-parsing_newest_1).
|
||||
We won't cover the parsing itself in this tutorial -- you can read the code if you want to see how it works.
|
||||
We're going to focus only on the salsa-related aspects.
|
||||
|
||||
[rd]: https://en.wikipedia.org/wiki/Recursive_descent_parser
|
||||
|
||||
## The `source_text` the function
|
||||
|
||||
Let's start by looking at the `source_text` function:
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/parser.rs:source_text}}
|
||||
```
|
||||
|
||||
This is a bit of an odd function!
|
||||
You can see it is annotated as memoized,
|
||||
which means that salsa will store the return value in the database,
|
||||
so that if you call it again, it does not re-execute unless its inputs have changed.
|
||||
However, the function body itself is just a `panic!`, so it can never successfully return.
|
||||
What is going on?
|
||||
|
||||
This function is an example of a common convention called an **input**.
|
||||
Whenever you have a memoized function, it is possible to set its return value explicitly
|
||||
(the [chapter on testing](./debug.md) shows how it is done).
|
||||
When you set the return value explicitly, it never executes;
|
||||
instead, when it is called, that return value is just returned.
|
||||
This makes the function into an _input_ to the entire computation.
|
||||
|
||||
In this case, the body is just `panic!`,
|
||||
which indicates that `source_text` is always meant to be set explicitly.
|
||||
It's possible to set a return value for functions that have a body,
|
||||
in which case they can act as either an input or a computation.
|
||||
|
||||
### Arguments to a memoized function
|
||||
|
||||
The first parameter to a memoized function is always the database,
|
||||
which should be a `dyn Trait` value for the database trait associated with the jar
|
||||
(the default jar is `crate::Jar`).
|
||||
|
||||
Memoized functions may take other arguments as well, though our examples here do not.
|
||||
Those arguments must be something that can be interned.
|
||||
|
||||
### Memoized functions with `return_ref`
|
||||
|
||||
`source_text` is not only memoized, it is annotated with `return_ref`.
|
||||
Ordinarily, when you call a memoized function,
|
||||
the result you get back is cloned out of the database.
|
||||
The `return_ref` attribute means that a reference into the database is returned instead.
|
||||
So, when called, `source_text` will return an `&String` rather than cloning the `String`.
|
||||
This is useful as a performance optimization.
|
||||
|
||||
## The `parse_statements` function
|
||||
|
||||
The next function is `parse_statements`, which has the job of actually doing the parsing.
|
||||
The comments in the function explain how it works.
|
||||
The starting point for the parser is the `parse_statements` function:
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/parser.rs:parse_statements}}
|
||||
```
|
||||
|
||||
The most interesting part, from salsa's point of view,
|
||||
is that `parse_statements` calls `source_text` to get its input.
|
||||
Salsa will track this dependency.
|
||||
If `parse_statements` is called again, it will only re-execute if the return value of `source_text` may have changed.
|
||||
This function is annotated as `#[salsa::tracked]`.
|
||||
That means that, when it is called, salsa will track what inputs it reads as well as what value it returns.
|
||||
The return value is *memoized*,
|
||||
which means that if you call this function again without changing the inputs,
|
||||
salsa will just clone the result rather than re-execute it.
|
||||
|
||||
We won't explain how the parser works in detail here.
|
||||
You can read the comments in the source file to get a better understanding.
|
||||
But we will cover a few interesting points that interact with Salsa specifically.
|
||||
### Tracked functions are the unit of reuse
|
||||
|
||||
### Creating interned values with the `from` method
|
||||
Tracked functions are the core part of how salsa enables incremental reuse.
|
||||
The goal of the framework is to avoid re-executing tracked functions and instead to clone their result.
|
||||
Salsa uses the [red-green algorithm](../reference/algorithm.md) to decide when to re-execute a function.
|
||||
The short version is that a tracked function is re-executed if either (a) it directly reads an input, and that input has changed
|
||||
or (b) it directly invokes another tracked function, and that function's return value has changed.
|
||||
In the case of `parse_statements`, it directly reads `ProgramSource::text`, so if the text changes, then the parser will re-execute.
|
||||
|
||||
The `parse_statement` method parses a single statement from the input:
|
||||
By choosing which functions to mark as `#[tracked]`, you control how much reuse you get.
|
||||
In our case, we're opting to mark the outermost parsing function as tracked, but not the inner ones.
|
||||
This means that if the input changes, we will always re-parse the entire input and re-create the resulting statements and so forth.
|
||||
We'll see later that this *doesn't* mean we will always re-run the type checker and other parts of the compiler.
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/parser.rs:parse_statement}}
|
||||
```
|
||||
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.
|
||||
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.
|
||||
Setting up a scheme like this is relatively easy in salsa, and uses the same principles that we will use later to avoid re-executing the type checker.
|
||||
|
||||
The part we want to highlight is how an interned enum is created:
|
||||
### Parameters to a tracked function
|
||||
|
||||
```rust
|
||||
Statement::from(self.db, StatementData::Function(func))
|
||||
```
|
||||
The **first** parameter to a tracked function is **always** the database, `db: &dyn crate::Db`.
|
||||
It must be a `dyn` value of whatever database is associated with the jar.
|
||||
|
||||
On any interned value, the `from` method takes a database and an instance of the "data" type (here, `StatementData`).
|
||||
It then interns this value and returns the interned type (here, `Statement`).
|
||||
The **second** parameter to a tracked function is **always** some kind of salsa struct.
|
||||
The first parameter to a memoized function is always the database,
|
||||
which should be a `dyn Trait` value for the database trait associated with the jar
|
||||
(the default jar is `crate::Jar`).
|
||||
|
||||
### Creating entity values, or interned structs, with the `new` method
|
||||
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 other way to create an interned/entity struct is with the `new` method.
|
||||
This only works when the struct has named fields (i.e., it doesn't work with enums like `Statement`).
|
||||
The `parse_function` method demonstrates:
|
||||
### The `return_ref` annotation
|
||||
|
||||
```rust
|
||||
{{#include ../../../calc-example/calc/src/parser.rs:parse_function}}
|
||||
```
|
||||
You may have noticed that `parse_statements` is tagged with `#[salsa::tracked(return_ref)]`.
|
||||
Ordinarily, when you call a tracked function, the result you get back is cloned out of the database.
|
||||
The `return_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 `return_ref` annotation from the [ir](./ir.md) section of the tutorial,
|
||||
where it was placed on struct fields, with roughly the same meaning.)
|
||||
|
||||
You can see that we invoke `FunctionnId::new` (an interned struct) and `Function::new` (an entity).
|
||||
In each case, the `new` method takes the database, and then the value of each field.
|
||||
|
|
|
@ -16,10 +16,15 @@ print z
|
|||
|
||||
## Parser
|
||||
|
||||
The calc compiler begins with a parser.
|
||||
Because calc is so simple, we don't have to bother separating out the lexer.
|
||||
The parser will take the raw bytes and produce a series of statements that are something like this
|
||||
(this is pseudo-Rust):
|
||||
The calc compiler takes as input a program, represented by a string:
|
||||
|
||||
```rust
|
||||
struct ProgramSource {
|
||||
text: String
|
||||
}
|
||||
```
|
||||
|
||||
The first thing it does it to parse that string into a series of statements that look something like the following pseudo-Rust:[^lexer]
|
||||
|
||||
```rust
|
||||
enum Statement {
|
||||
|
@ -62,6 +67,8 @@ type FunctionId = /* interned string */;
|
|||
type VariableId = /* interned string */;
|
||||
```
|
||||
|
||||
[^lexer]: Because calc is so simple, we don't have to bother separating out the lexer from the parser.
|
||||
|
||||
## Checker
|
||||
|
||||
The "checker" has the job of ensuring that the user only references variables that have been defined.
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
use ordered_float::OrderedFloat;
|
||||
use salsa::debug::DebugWithDb;
|
||||
|
||||
// ANCHOR: input
|
||||
#[salsa::input]
|
||||
pub struct SourceProgram {
|
||||
#[return_ref]
|
||||
text: String,
|
||||
}
|
||||
// ANCHOR_END: input
|
||||
|
||||
// ANCHOR: interned_ids
|
||||
#[salsa::interned]
|
||||
#[derive(Eq, PartialEq, Clone, Hash)]
|
||||
pub struct VariableId {
|
||||
#[return_ref]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
#[derive(Eq, PartialEq, Clone, Hash)]
|
||||
pub struct FunctionId {
|
||||
#[return_ref]
|
||||
pub text: String,
|
||||
|
@ -19,8 +25,12 @@ pub struct FunctionId {
|
|||
|
||||
// ANCHOR: statements_and_expressions
|
||||
#[salsa::interned]
|
||||
pub struct Statement {
|
||||
data: StatementData,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Statement {
|
||||
pub enum StatementData {
|
||||
/// Defines `fn <name>(<args>) = <body>`
|
||||
Function(Function),
|
||||
/// Defines `print <expr>`
|
||||
|
@ -28,8 +38,13 @@ pub enum Statement {
|
|||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct Expression {
|
||||
#[return_ref]
|
||||
data: ExpressionData,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Expression {
|
||||
pub enum ExpressionData {
|
||||
Op(Expression, Op, Expression),
|
||||
Number(OrderedFloat<f64>),
|
||||
Variable(VariableId),
|
||||
|
@ -58,8 +73,8 @@ impl DebugWithDb<dyn crate::Db + '_> for Function {
|
|||
impl DebugWithDb<dyn crate::Db + '_> for Statement {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>, db: &dyn crate::Db) -> std::fmt::Result {
|
||||
match self.data(db) {
|
||||
StatementData::Function(a) => DebugWithDb::fmt(a, f, db),
|
||||
StatementData::Print(a) => DebugWithDb::fmt(a, f, db),
|
||||
StatementData::Function(a) => DebugWithDb::fmt(&a, f, db),
|
||||
StatementData::Print(a) => DebugWithDb::fmt(&a, f, db),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +132,7 @@ impl DebugWithDb<dyn crate::Db + '_> for Diagnostic {
|
|||
}
|
||||
|
||||
// ANCHOR: functions
|
||||
#[salsa::entity]
|
||||
#[salsa::tracked]
|
||||
pub struct Function {
|
||||
#[id]
|
||||
name: FunctionId,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// ANCHOR: jar_struct
|
||||
#[salsa::jar(db = Db)]
|
||||
pub struct Jar(
|
||||
crate::ir::SourceProgram,
|
||||
crate::ir::VariableId,
|
||||
crate::ir::FunctionId,
|
||||
crate::ir::Expression,
|
||||
|
@ -8,7 +9,6 @@ pub struct Jar(
|
|||
crate::ir::Function,
|
||||
crate::ir::Diagnostics,
|
||||
crate::parser::parse_statements,
|
||||
crate::parser::source_text,
|
||||
);
|
||||
// ANCHOR_END: jar_struct
|
||||
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
use ordered_float::OrderedFloat;
|
||||
|
||||
use crate::ir::{
|
||||
Diagnostic, Diagnostics, Expression, ExpressionData, Function, FunctionId, Op, Statement,
|
||||
StatementData, VariableId,
|
||||
Diagnostic, Diagnostics, Expression, ExpressionData, Function, FunctionId, Op, SourceProgram,
|
||||
Statement, StatementData, VariableId,
|
||||
};
|
||||
|
||||
// ANCHOR: source_text
|
||||
#[salsa::memoized(return_ref)]
|
||||
pub fn source_text(_db: &dyn crate::Db) -> String {
|
||||
panic!("input")
|
||||
}
|
||||
// ANCHOR_END: source_text
|
||||
|
||||
// ANCHOR: parse_statements
|
||||
#[salsa::memoized(return_ref)]
|
||||
pub fn parse_statements(db: &dyn crate::Db) -> Vec<Statement> {
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub fn parse_statements(db: &dyn crate::Db, source: SourceProgram) -> Vec<Statement> {
|
||||
// Get the source text from the database
|
||||
let source_text = source_text(db);
|
||||
let source_text = source.text(db);
|
||||
|
||||
// Create the parser
|
||||
let mut parser = Parser {
|
||||
|
@ -121,13 +114,13 @@ impl Parser<'_> {
|
|||
let word = self.word()?;
|
||||
if word == "fn" {
|
||||
let func = self.parse_function()?;
|
||||
Some(Statement::from(self.db, StatementData::Function(func)))
|
||||
Some(Statement::new(self.db, StatementData::Function(func)))
|
||||
// ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// Create a new interned enum... |
|
||||
// using the "data" type.
|
||||
} else if word == "print" {
|
||||
let expr = self.parse_expression()?;
|
||||
Some(Statement::from(self.db, StatementData::Print(expr)))
|
||||
Some(Statement::new(self.db, StatementData::Print(expr)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -191,7 +184,7 @@ impl Parser<'_> {
|
|||
|
||||
while let Some(op) = op(self) {
|
||||
let expr2 = parse_expr(self)?;
|
||||
expr1 = Expression::from(self.db, ExpressionData::Op(expr1, op, expr2));
|
||||
expr1 = Expression::new(self.db, ExpressionData::Op(expr1, op, expr2));
|
||||
}
|
||||
|
||||
Some(expr1)
|
||||
|
@ -206,13 +199,13 @@ impl Parser<'_> {
|
|||
let f = FunctionId::new(self.db, w);
|
||||
let args = self.parse_expressions()?;
|
||||
self.ch(')')?;
|
||||
return Some(Expression::from(self.db, ExpressionData::Call(f, args)));
|
||||
return Some(Expression::new(self.db, ExpressionData::Call(f, args)));
|
||||
}
|
||||
|
||||
let v = VariableId::new(self.db, w);
|
||||
Some(Expression::from(self.db, ExpressionData::Variable(v)))
|
||||
Some(Expression::new(self.db, ExpressionData::Variable(v)))
|
||||
} else if let Some(n) = self.number() {
|
||||
Some(Expression::from(
|
||||
Some(Expression::new(
|
||||
self.db,
|
||||
ExpressionData::Number(OrderedFloat::from(n)),
|
||||
))
|
||||
|
@ -339,14 +332,14 @@ fn parse_string(source_text: &str) -> String {
|
|||
// Create the database
|
||||
let mut db = crate::db::Database::default();
|
||||
|
||||
// Set the source_text value
|
||||
source_text::set(&mut db, source_text.to_string());
|
||||
// Create the source program
|
||||
let source_program = SourceProgram::new(&mut db, source_text.to_string());
|
||||
|
||||
// Invoke the parser
|
||||
let statements = parse_statements(&db);
|
||||
let statements = parse_statements(&db, source_program);
|
||||
|
||||
// Read out any diagnostics
|
||||
let accumulated = parse_statements::accumulated::<Diagnostics>(&db);
|
||||
let accumulated = parse_statements::accumulated::<Diagnostics>(&db, source_program);
|
||||
|
||||
// Format the result as a string and return it
|
||||
format!("{:#?}", (statements, accumulated).debug(&db))
|
||||
|
|
|
@ -21,6 +21,8 @@ struct Accumulator;
|
|||
impl crate::options::AllowedOptions for Accumulator {
|
||||
const RETURN_REF: bool = false;
|
||||
|
||||
const SPECIFY: bool = false;
|
||||
|
||||
const NO_EQ: bool = false;
|
||||
|
||||
const JAR: bool = true;
|
||||
|
|
|
@ -1,411 +0,0 @@
|
|||
use syn::spanned::Spanned;
|
||||
use syn::{ItemFn, ReturnType};
|
||||
|
||||
use crate::configuration::{self, Configuration, CycleRecoveryStrategy};
|
||||
use crate::options::Options;
|
||||
|
||||
// #[salsa::component(in Jar0)]
|
||||
// fn my_func(db: &dyn Jar0Db, input1: u32, input2: u32) -> String {
|
||||
// format!("Hello, world")
|
||||
// }
|
||||
|
||||
pub(crate) fn component(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let args = syn::parse_macro_input!(args as Args);
|
||||
let item_fn = syn::parse_macro_input!(input as ItemFn);
|
||||
match component_helper(args, item_fn) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn component_helper(args: Args, item_fn: ItemFn) -> syn::Result<proc_macro::TokenStream> {
|
||||
let struct_item = configuration_struct(&item_fn);
|
||||
let configuration = fn_configuration(&args, &item_fn)?;
|
||||
let struct_item_ident = &struct_item.ident;
|
||||
let struct_ty: syn::Type = parse_quote!(#struct_item_ident);
|
||||
let configuration_impl = configuration.to_impl(&struct_ty);
|
||||
let ingredients_for_impl = ingredients_for_impl(&args, &struct_ty);
|
||||
let (getter, item_impl) = wrapper_fns(&args, &item_fn, &struct_ty)?;
|
||||
|
||||
Ok(proc_macro::TokenStream::from(quote! {
|
||||
#struct_item
|
||||
#configuration_impl
|
||||
#ingredients_for_impl
|
||||
#getter
|
||||
#item_impl
|
||||
}))
|
||||
}
|
||||
|
||||
struct Component;
|
||||
|
||||
type Args = Options<Component>;
|
||||
|
||||
impl crate::options::AllowedOptions for Component {
|
||||
const RETURN_REF: bool = true;
|
||||
|
||||
const NO_EQ: bool = true;
|
||||
|
||||
const JAR: bool = true;
|
||||
|
||||
const DATA: bool = false;
|
||||
|
||||
const DB: bool = false;
|
||||
}
|
||||
|
||||
fn configuration_struct(item_fn: &syn::ItemFn) -> syn::ItemStruct {
|
||||
let fn_name = item_fn.sig.ident.clone();
|
||||
let vis = &item_fn.vis;
|
||||
parse_quote! {
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #fn_name {
|
||||
function: salsa::function::FunctionIngredient<Self>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fn_configuration(args: &Args, item_fn: &syn::ItemFn) -> syn::Result<Configuration> {
|
||||
let jar_ty = args.jar_ty();
|
||||
let key_ty = arg_ty(item_fn)?.clone();
|
||||
let value_ty = configuration::value_ty(&item_fn.sig);
|
||||
|
||||
// FIXME: these are hardcoded for now
|
||||
let cycle_strategy = CycleRecoveryStrategy::Panic;
|
||||
|
||||
let backdate_fn = configuration::should_backdate_value_fn(args.should_backdate());
|
||||
let recover_fn = configuration::panic_cycle_recovery_fn();
|
||||
|
||||
// The type of the configuration struct; this has the same name as the fn itself.
|
||||
let fn_ty = item_fn.sig.ident.clone();
|
||||
|
||||
// Make a copy of the fn with a different name; we will invoke this from `execute`.
|
||||
// We need to change the name because, otherwise, if the function invoked itself
|
||||
// recursively it would not go through the query system.
|
||||
let inner_fn_name = &syn::Ident::new("__fn", item_fn.sig.ident.span());
|
||||
let mut inner_fn = item_fn.clone();
|
||||
inner_fn.sig.ident = inner_fn_name.clone();
|
||||
|
||||
// Create the `execute` function, which invokes the function itself (which we embed within).
|
||||
let execute_fn = parse_quote! {
|
||||
fn execute(__db: &salsa::function::DynDb<Self>, __id: Self::Key) -> Self::Value {
|
||||
#inner_fn
|
||||
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db);
|
||||
let __ingredients =
|
||||
<_ as salsa::storage::HasIngredientsFor<#fn_ty>>::ingredient(__jar);
|
||||
#inner_fn_name(__db, __id)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Configuration {
|
||||
jar_ty,
|
||||
key_ty,
|
||||
value_ty,
|
||||
cycle_strategy,
|
||||
backdate_fn,
|
||||
execute_fn,
|
||||
recover_fn,
|
||||
})
|
||||
}
|
||||
|
||||
fn ingredients_for_impl(args: &Args, struct_ty: &syn::Type) -> syn::ItemImpl {
|
||||
let jar_ty = args.jar_ty();
|
||||
parse_quote! {
|
||||
impl salsa::storage::IngredientsFor for #struct_ty {
|
||||
type Ingredients = Self;
|
||||
type Jar = #jar_ty;
|
||||
|
||||
fn create_ingredients<DB>(ingredients: &mut salsa::routes::Ingredients<DB>) -> Self::Ingredients
|
||||
where
|
||||
DB: salsa::DbWithJar<Self::Jar> + salsa::storage::JarFromJars<Self::Jar>,
|
||||
{
|
||||
Self {
|
||||
function: {
|
||||
let index = ingredients.push(|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
|
||||
let ingredients =
|
||||
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
|
||||
&ingredients.function
|
||||
});
|
||||
salsa::function::FunctionIngredient::new(index)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrapper_fns(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
) -> syn::Result<(syn::ItemFn, syn::ItemImpl)> {
|
||||
// The "getter" has same signature as the original:
|
||||
let getter_fn = getter_fn(args, item_fn, struct_ty)?;
|
||||
|
||||
let ref_getter_fn = ref_getter_fn(args, item_fn, struct_ty)?;
|
||||
let accumulated_fn = accumulated_fn(args, item_fn, struct_ty)?;
|
||||
let setter_fn = setter_fn(args, item_fn, struct_ty)?;
|
||||
|
||||
let item_impl: syn::ItemImpl = parse_quote! {
|
||||
impl #struct_ty {
|
||||
#ref_getter_fn
|
||||
#setter_fn
|
||||
#accumulated_fn
|
||||
}
|
||||
};
|
||||
|
||||
Ok((getter_fn, item_impl))
|
||||
}
|
||||
|
||||
fn getter_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ItemFn> {
|
||||
let mut getter_fn = item_fn.clone();
|
||||
let arg_idents: Vec<_> = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|arg| -> syn::Result<syn::Ident> {
|
||||
match arg {
|
||||
syn::FnArg::Receiver(_) => Err(syn::Error::new(arg.span(), "unexpected receiver")),
|
||||
syn::FnArg::Typed(pat_ty) => Ok(match &*pat_ty.pat {
|
||||
syn::Pat::Ident(ident) => ident.ident.clone(),
|
||||
_ => return Err(syn::Error::new(arg.span(), "unexpected receiver")),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
if args.return_ref.is_some() {
|
||||
getter_fn = make_fn_return_ref(getter_fn)?;
|
||||
getter_fn.block = Box::new(parse_quote_spanned! {
|
||||
item_fn.block.span() => {
|
||||
#struct_ty::get(#(#arg_idents,)*)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getter_fn.block = Box::new(parse_quote_spanned! {
|
||||
item_fn.block.span() => {
|
||||
Clone::clone(#struct_ty::get(#(#arg_idents,)*))
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(getter_fn)
|
||||
}
|
||||
|
||||
fn ref_getter_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ItemFn> {
|
||||
let jar_ty = args.jar_ty();
|
||||
let mut ref_getter_fn = item_fn.clone();
|
||||
ref_getter_fn.sig.ident = syn::Ident::new("get", item_fn.sig.ident.span());
|
||||
ref_getter_fn = make_fn_return_ref(ref_getter_fn)?;
|
||||
|
||||
let (db_var, arg_names) = fn_args(item_fn)?;
|
||||
ref_getter_fn.block = parse_quote! {
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#struct_ty>>::ingredient(__jar);
|
||||
__ingredients.function.fetch(#db_var, #(#arg_names),*)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ref_getter_fn)
|
||||
}
|
||||
|
||||
fn setter_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ImplItemMethod> {
|
||||
// The setter has *always* the same signature as the original:
|
||||
// but it takes a value arg and has no return type.
|
||||
let jar_ty = args.jar_ty();
|
||||
let (db_var, arg_names) = fn_args(item_fn)?;
|
||||
let mut setter_sig = item_fn.sig.clone();
|
||||
let value_ty = configuration::value_ty(&item_fn.sig);
|
||||
setter_sig.ident = syn::Ident::new("set", item_fn.sig.ident.span());
|
||||
let value_arg = syn::Ident::new("__value", item_fn.sig.output.span());
|
||||
setter_sig.inputs.push(parse_quote!(#value_arg: #value_ty));
|
||||
setter_sig.output = ReturnType::Default;
|
||||
Ok(syn::ImplItemMethod {
|
||||
attrs: vec![],
|
||||
vis: item_fn.vis.clone(),
|
||||
defaultness: None,
|
||||
sig: setter_sig,
|
||||
block: parse_quote! {
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#struct_ty>>::ingredient(__jar);
|
||||
__ingredients.function.set(#db_var, #(#arg_names),*, #value_arg)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn make_fn_return_ref(mut ref_getter_fn: syn::ItemFn) -> syn::Result<syn::ItemFn> {
|
||||
// The 0th input should be a `&dyn Foo`. We need to ensure
|
||||
// it has a named lifetime parameter.
|
||||
let (db_lifetime, _) = db_lifetime_and_ty(&mut ref_getter_fn)?;
|
||||
|
||||
let (right_arrow, elem) = match ref_getter_fn.sig.output {
|
||||
ReturnType::Default => (
|
||||
syn::Token,
|
||||
parse_quote!(()),
|
||||
),
|
||||
ReturnType::Type(rarrow, ty) => (rarrow, ty),
|
||||
};
|
||||
|
||||
let ref_output = syn::TypeReference {
|
||||
and_token: syn::Token),
|
||||
lifetime: Some(db_lifetime),
|
||||
mutability: None,
|
||||
elem,
|
||||
};
|
||||
|
||||
ref_getter_fn.sig.output = syn::ReturnType::Type(right_arrow, Box::new(ref_output.into()));
|
||||
|
||||
Ok(ref_getter_fn)
|
||||
}
|
||||
|
||||
fn db_lifetime_and_ty(func: &mut syn::ItemFn) -> syn::Result<(syn::Lifetime, &syn::Type)> {
|
||||
match &mut func.sig.inputs[0] {
|
||||
syn::FnArg::Receiver(r) => {
|
||||
return Err(syn::Error::new(r.span(), "expected database, not self"))
|
||||
}
|
||||
syn::FnArg::Typed(pat_ty) => match &mut *pat_ty.ty {
|
||||
syn::Type::Reference(ty) => match &ty.lifetime {
|
||||
Some(lt) => Ok((lt.clone(), &pat_ty.ty)),
|
||||
None => {
|
||||
let and_token_span = ty.and_token.span();
|
||||
let ident = syn::Ident::new("__db", and_token_span);
|
||||
func.sig.generics.params.insert(
|
||||
0,
|
||||
syn::LifetimeDef {
|
||||
attrs: vec![],
|
||||
lifetime: syn::Lifetime {
|
||||
apostrophe: and_token_span,
|
||||
ident: ident.clone(),
|
||||
},
|
||||
colon_token: None,
|
||||
bounds: Default::default(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let db_lifetime = syn::Lifetime {
|
||||
apostrophe: and_token_span,
|
||||
ident,
|
||||
};
|
||||
ty.lifetime = Some(db_lifetime.clone());
|
||||
Ok((db_lifetime, &pat_ty.ty))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
pat_ty.span(),
|
||||
"expected database to be a `&` type",
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn accumulated_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ItemFn> {
|
||||
let jar_ty = args.jar_ty();
|
||||
|
||||
let mut accumulated_fn = item_fn.clone();
|
||||
accumulated_fn.sig.ident = syn::Ident::new("accumulated", item_fn.sig.ident.span());
|
||||
accumulated_fn.sig.generics.params.push(parse_quote! {
|
||||
__A: salsa::accumulator::Accumulator
|
||||
});
|
||||
accumulated_fn.sig.output = parse_quote! {
|
||||
-> Vec<<__A as salsa::accumulator::Accumulator>::Data>
|
||||
};
|
||||
|
||||
let (db_lifetime, _) = db_lifetime_and_ty(&mut accumulated_fn)?;
|
||||
let predicate: syn::WherePredicate = parse_quote!(<#jar_ty as salsa::jar::Jar<#db_lifetime>>::DynDb: salsa::storage::HasJar<<__A as salsa::accumulator::Accumulator>::Jar>);
|
||||
|
||||
if let Some(where_clause) = &mut accumulated_fn.sig.generics.where_clause {
|
||||
where_clause.predicates.push(predicate);
|
||||
} else {
|
||||
accumulated_fn.sig.generics.where_clause = parse_quote!(where #predicate);
|
||||
}
|
||||
|
||||
let (db_var, arg_names) = fn_args(item_fn)?;
|
||||
accumulated_fn.block = parse_quote! {
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#struct_ty>>::ingredient(__jar);
|
||||
__ingredients.function.accumulated::<__A>(#db_var, #(#arg_names),*)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(accumulated_fn)
|
||||
}
|
||||
|
||||
fn fn_args(item_fn: &syn::ItemFn) -> syn::Result<(proc_macro2::Ident, Vec<proc_macro2::Ident>)> {
|
||||
// Check that we have no receiver and that all argments have names
|
||||
if item_fn.sig.inputs.len() == 0 {
|
||||
return Err(syn::Error::new(
|
||||
item_fn.sig.span(),
|
||||
"method needs a database argument",
|
||||
));
|
||||
}
|
||||
|
||||
let mut input_names = vec![];
|
||||
for input in &item_fn.sig.inputs {
|
||||
match input {
|
||||
syn::FnArg::Receiver(r) => {
|
||||
return Err(syn::Error::new(r.span(), "no self argument expected"));
|
||||
}
|
||||
syn::FnArg::Typed(pat_ty) => match &*pat_ty.pat {
|
||||
syn::Pat::Ident(ident) => {
|
||||
input_names.push(ident.ident.clone());
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
pat_ty.pat.span(),
|
||||
"all arguments must be given names",
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Database is the first argument
|
||||
let db_var = input_names[0].clone();
|
||||
let arg_names = input_names[1..].to_owned();
|
||||
|
||||
Ok((db_var, arg_names))
|
||||
}
|
||||
|
||||
fn arg_ty(item_fn: &syn::ItemFn) -> syn::Result<&syn::Type> {
|
||||
// Check that we have no receiver and that all argments have names
|
||||
if item_fn.sig.inputs.len() != 2 {
|
||||
return Err(syn::Error::new(
|
||||
item_fn.sig.span(),
|
||||
"component method needs a database argument and an entity",
|
||||
));
|
||||
}
|
||||
|
||||
match &item_fn.sig.inputs[1] {
|
||||
syn::FnArg::Typed(pat_ty) => Ok(&pat_ty.ty),
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
item_fn.sig.inputs[1].span(),
|
||||
"expected a fn parameter with a type",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
pub(crate) enum DataItem {
|
||||
Struct(syn::ItemStruct),
|
||||
Enum(syn::ItemEnum),
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for DataItem {
|
||||
fn parse(input: &syn::parse::ParseBuffer<'_>) -> Result<Self, syn::Error> {
|
||||
match syn::Item::parse(input)? {
|
||||
syn::Item::Enum(item) => Ok(DataItem::Enum(item)),
|
||||
syn::Item::Struct(item) => Ok(DataItem::Struct(item)),
|
||||
_ => Err(input.error("expected an enum or a struct")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataItem {
|
||||
pub(crate) fn attrs(&self) -> &[syn::Attribute] {
|
||||
match self {
|
||||
DataItem::Struct(s) => &s.attrs,
|
||||
DataItem::Enum(e) => &e.attrs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the name of this struct/enum.
|
||||
pub(crate) fn ident(&self) -> &syn::Ident {
|
||||
match self {
|
||||
DataItem::Struct(s) => &s.ident,
|
||||
DataItem::Enum(e) => &e.ident,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new version of this struct/enum but with the given name `ident`.
|
||||
pub(crate) fn with_ident(&self, ident: syn::Ident) -> DataItem {
|
||||
match self {
|
||||
DataItem::Struct(s) => {
|
||||
let mut s = s.clone();
|
||||
s.ident = ident;
|
||||
DataItem::Struct(s)
|
||||
}
|
||||
DataItem::Enum(s) => {
|
||||
let mut s = s.clone();
|
||||
s.ident = ident;
|
||||
DataItem::Enum(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the visibility of this struct/enum.
|
||||
pub(crate) fn visibility(&self) -> &syn::Visibility {
|
||||
match self {
|
||||
DataItem::Struct(s) => &s.vis,
|
||||
DataItem::Enum(e) => &e.vis,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is a struct, returns the list of fields.
|
||||
///
|
||||
/// If this is an enum, returns None.
|
||||
pub(crate) fn fields(&self) -> Option<&syn::Fields> {
|
||||
match self {
|
||||
DataItem::Struct(s) => Some(&s.fields),
|
||||
DataItem::Enum(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl quote::ToTokens for DataItem {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
match self {
|
||||
DataItem::Struct(s) => s.to_tokens(tokens),
|
||||
DataItem::Enum(e) => e.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
186
components/salsa-entity-macros/src/input.rs
Normal file
186
components/salsa-entity-macros/src/input.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
use proc_macro2::{Literal, TokenStream};
|
||||
|
||||
use crate::salsa_struct::{SalsaField, SalsaStruct};
|
||||
|
||||
/// For an entity struct `Foo` with fields `f1: T1, ..., fN: TN`, we generate...
|
||||
///
|
||||
/// * the "id struct" `struct Foo(salsa::Id)`
|
||||
/// * the entity ingredient, which maps the id fields to the `Id`
|
||||
/// * for each value field, a function ingredient
|
||||
pub(crate) fn input(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
match SalsaStruct::new(args, input).and_then(|el| el.generate_input()) {
|
||||
Ok(s) => s.into(),
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
impl SalsaStruct {
|
||||
fn generate_input(&self) -> syn::Result<TokenStream> {
|
||||
self.validate_input()?;
|
||||
|
||||
let (config_structs, config_impls) = self.field_config_structs_and_impls(self.all_fields());
|
||||
|
||||
let id_struct = self.id_struct();
|
||||
let inherent_impl = self.input_inherent_impl();
|
||||
let ingredients_for_impl = self.input_ingredients(&config_structs);
|
||||
let as_id_impl = self.as_id_impl();
|
||||
|
||||
Ok(quote! {
|
||||
#(#config_structs)*
|
||||
#id_struct
|
||||
#inherent_impl
|
||||
#ingredients_for_impl
|
||||
#as_id_impl
|
||||
#(#config_impls)*
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_input(&self) -> syn::Result<()> {
|
||||
self.disallow_id_fields("input")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate an inherent impl with methods on the entity type.
|
||||
fn input_inherent_impl(&self) -> syn::ItemImpl {
|
||||
let ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let db_dyn_ty = self.db_dyn_ty();
|
||||
let input_index = self.input_index();
|
||||
|
||||
let field_indices = self.all_field_indices();
|
||||
let field_names: Vec<_> = self.all_field_names();
|
||||
let field_tys: Vec<_> = self.all_field_tys();
|
||||
let field_clones: Vec<_> = self.all_fields().map(SalsaField::is_clone_field).collect();
|
||||
let field_getters: Vec<syn::ImplItemMethod> = field_indices.iter().zip(&field_names).zip(&field_tys).zip(&field_clones).map(|(((field_index, field_name), field_ty), is_clone_field)|
|
||||
if !*is_clone_field {
|
||||
parse_quote! {
|
||||
pub fn #field_name<'db>(self, __db: &'db #db_dyn_ty) -> &'db #field_ty
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar);
|
||||
__ingredients.#field_index.fetch(__db, self)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parse_quote! {
|
||||
pub fn #field_name<'db>(self, __db: &'db #db_dyn_ty) -> #field_ty
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar);
|
||||
__ingredients.#field_index.fetch(__db, self).clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.collect();
|
||||
|
||||
let field_setters: Vec<syn::ImplItemMethod> = field_indices.iter().zip(&field_names).zip(&field_tys).map(|((field_index, field_name), field_ty)| {
|
||||
let set_field_name = syn::Ident::new(&format!("set_{}", field_name), field_name.span());
|
||||
parse_quote! {
|
||||
pub fn #set_field_name<'db>(self, __db: &'db mut #db_dyn_ty, __value: #field_ty)
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar_mut(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient_mut(__jar);
|
||||
__ingredients.#field_index.store(__runtime, self, __value, salsa::Durability::LOW);
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
parse_quote! {
|
||||
impl #ident {
|
||||
pub fn new(__db: &mut #db_dyn_ty, #(#field_names: #field_tys,)*) -> Self
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar_mut(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient_mut(__jar);
|
||||
let __id = __ingredients.#input_index.new_input(__runtime);
|
||||
#(
|
||||
__ingredients.#field_indices.store(__runtime, __id, #field_names, salsa::Durability::LOW);
|
||||
)*
|
||||
__id
|
||||
}
|
||||
|
||||
#(#field_getters)*
|
||||
|
||||
#(#field_setters)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the `IngredientsFor` impl for this entity.
|
||||
///
|
||||
/// The entity's ingredients include both the main entity ingredient along with a
|
||||
/// function ingredient for each of the value fields.
|
||||
fn input_ingredients(&self, config_structs: &[syn::ItemStruct]) -> syn::ItemImpl {
|
||||
let ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let all_field_indices: Vec<Literal> = self.all_field_indices();
|
||||
let input_index: Literal = self.input_index();
|
||||
let config_struct_names = config_structs.iter().map(|s| &s.ident);
|
||||
|
||||
parse_quote! {
|
||||
impl salsa::storage::IngredientsFor for #ident {
|
||||
type Jar = #jar_ty;
|
||||
type Ingredients = (
|
||||
#(
|
||||
salsa::function::FunctionIngredient<#config_struct_names>,
|
||||
)*
|
||||
salsa::input::InputIngredient<#ident>,
|
||||
);
|
||||
|
||||
fn create_ingredients<DB>(
|
||||
ingredients: &mut salsa::routes::Ingredients<DB>,
|
||||
) -> Self::Ingredients
|
||||
where
|
||||
DB: salsa::DbWithJar<Self::Jar> + salsa::storage::JarFromJars<Self::Jar>,
|
||||
{
|
||||
(
|
||||
#(
|
||||
{
|
||||
let index = ingredients.push(
|
||||
|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
|
||||
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
|
||||
&ingredients.#all_field_indices
|
||||
},
|
||||
);
|
||||
salsa::function::FunctionIngredient::new(index)
|
||||
},
|
||||
)*
|
||||
{
|
||||
let index = ingredients.push(
|
||||
|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
|
||||
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
|
||||
&ingredients.#input_index
|
||||
},
|
||||
);
|
||||
salsa::input::InputIngredient::new(index)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For the entity, we create a tuple that contains the function ingredients
|
||||
/// for each "other" field and the entity ingredient. This is the index of
|
||||
/// the entity ingredient within that tuple.
|
||||
fn input_index(&self) -> Literal {
|
||||
Literal::usize_unsuffixed(self.all_fields().count())
|
||||
}
|
||||
|
||||
/// For the entity, we create a tuple that contains the function ingredients
|
||||
/// for each field and an entity ingredient. These are the indices
|
||||
/// of the function ingredients within that tuple.
|
||||
fn all_field_indices(&self) -> Vec<Literal> {
|
||||
self.all_fields()
|
||||
.zip(0..)
|
||||
.map(|(_, i)| Literal::usize_unsuffixed(i))
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use proc_macro2::TokenStream;
|
||||
|
||||
use crate::entity_like::EntityLike;
|
||||
use crate::salsa_struct::SalsaStruct;
|
||||
|
||||
// #[salsa::interned(jar = Jar0, data = TyData0)]
|
||||
// #[derive(Eq, PartialEq, Hash, Debug, Clone)]
|
||||
|
@ -14,20 +14,19 @@ pub(crate) fn interned(
|
|||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
match EntityLike::new(args, input).and_then(|el| el.generate_interned()) {
|
||||
match SalsaStruct::new(args, input).and_then(|el| el.generate_interned()) {
|
||||
Ok(s) => s.into(),
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityLike {
|
||||
impl SalsaStruct {
|
||||
fn generate_interned(&self) -> syn::Result<TokenStream> {
|
||||
self.validate_interned()?;
|
||||
let id_struct = self.id_struct();
|
||||
let data_struct = self.data_struct();
|
||||
let ingredients_for_impl = self.ingredients_for_impl();
|
||||
let as_id_impl = self.as_id_impl();
|
||||
let all_fields_impl = self.inherent_impl_for_all_fields();
|
||||
let named_fields_impl = self.inherent_impl_for_named_fields();
|
||||
|
||||
Ok(quote! {
|
||||
|
@ -35,95 +34,42 @@ impl EntityLike {
|
|||
#data_struct
|
||||
#ingredients_for_impl
|
||||
#as_id_impl
|
||||
#all_fields_impl
|
||||
#named_fields_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_interned(&self) -> syn::Result<()> {
|
||||
// Disallow `#[value]` attributes on interned things.
|
||||
//
|
||||
// They don't really make sense -- we intern all the fields of something
|
||||
// to create its id. If multiple queries were to intern the same thing with
|
||||
// distinct values for the value field, what would happen?
|
||||
for ef in self.all_entity_fields() {
|
||||
if ef.has_id_attr {
|
||||
return Err(syn::Error::new(
|
||||
ef.name().span(),
|
||||
"`#[id]` not required in interned structs",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.disallow_id_fields("interned")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates an inherent impl on the id struct with various methods.
|
||||
///
|
||||
/// If there are named fields...
|
||||
///
|
||||
/// * a `new` method that takes an `&db` and each field
|
||||
/// * a method for each field to access its contents
|
||||
///
|
||||
/// Always...
|
||||
///
|
||||
/// * a `from` method that takes an `&db` and a `Data`
|
||||
/// * a `data` method that returns an `&Data`
|
||||
fn inherent_impl_for_all_fields(&self) -> syn::ItemImpl {
|
||||
let vis = self.visibility();
|
||||
let id_ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let data_ident = self.data_ident();
|
||||
|
||||
parse_quote! {
|
||||
impl #id_ident {
|
||||
#vis fn from<DB: ?Sized>(db: &DB, data: #data_ident) -> Self
|
||||
where
|
||||
DB: salsa::storage::HasJar<#jar_ty>,
|
||||
{
|
||||
let (jar, runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
|
||||
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor<#id_ident>>::ingredient(jar);
|
||||
ingredients.intern(runtime, data)
|
||||
}
|
||||
|
||||
#vis fn data<DB: ?Sized>(self, db: &DB) -> & #data_ident
|
||||
where
|
||||
DB: salsa::storage::HasJar<#jar_ty>,
|
||||
{
|
||||
let (jar, runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
|
||||
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #id_ident >>::ingredient(jar);
|
||||
ingredients.data(runtime, self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is an interned struct, then generate methods to access each field,
|
||||
/// as well as a `new` method.
|
||||
fn inherent_impl_for_named_fields(&self) -> Option<syn::ItemImpl> {
|
||||
if !self.has_named_fields() {
|
||||
return None;
|
||||
}
|
||||
|
||||
fn inherent_impl_for_named_fields(&self) -> syn::ItemImpl {
|
||||
let vis = self.visibility();
|
||||
let id_ident = self.id_ident();
|
||||
let db_dyn_ty = self.db_dyn_ty();
|
||||
let jar_ty = self.jar_ty();
|
||||
|
||||
let field_getters: Vec<syn::ImplItemMethod> = self
|
||||
.all_entity_fields()
|
||||
.all_fields()
|
||||
.map(|field| {
|
||||
let field_name = field.name();
|
||||
let field_ty = field.ty();
|
||||
if field.is_clone_field() {
|
||||
parse_quote! {
|
||||
#vis fn #field_name(self, db: &#db_dyn_ty) -> #field_ty {
|
||||
<#field_ty as Clone>::clone(&self.data(db).#field_name)
|
||||
let (jar, runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
|
||||
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #id_ident >>::ingredient(jar);
|
||||
std::clone::Clone::clone(&ingredients.data(runtime, self).#field_name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parse_quote! {
|
||||
#vis fn #field_name<'db>(self, db: &'db #db_dyn_ty) -> &'db #field_ty {
|
||||
&self.data(db).#field_name
|
||||
let (jar, runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
|
||||
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #id_ident >>::ingredient(jar);
|
||||
&ingredients.data(runtime, self).#field_name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,22 +81,24 @@ impl EntityLike {
|
|||
let data_ident = self.data_ident();
|
||||
let new_method: syn::ImplItemMethod = parse_quote! {
|
||||
#vis fn new(
|
||||
__db: &#db_dyn_ty,
|
||||
db: &#db_dyn_ty,
|
||||
#(#field_names: #field_tys,)*
|
||||
) -> Self {
|
||||
Self::from(__db, #data_ident {
|
||||
let (jar, runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
|
||||
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #id_ident >>::ingredient(jar);
|
||||
ingredients.intern(runtime, #data_ident {
|
||||
#(#field_names,)*
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Some(parse_quote! {
|
||||
parse_quote! {
|
||||
impl #id_ident {
|
||||
#(#field_getters)*
|
||||
|
||||
#new_method
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an impl of `salsa::storage::IngredientsFor`.
|
||||
|
|
|
@ -30,6 +30,8 @@ struct Jar;
|
|||
impl crate::options::AllowedOptions for Jar {
|
||||
const RETURN_REF: bool = false;
|
||||
|
||||
const SPECIFY: bool = false;
|
||||
|
||||
const NO_EQ: bool = false;
|
||||
|
||||
const JAR: bool = false;
|
||||
|
|
|
@ -26,16 +26,16 @@ macro_rules! parse_quote_spanned {
|
|||
}
|
||||
|
||||
mod accumulator;
|
||||
mod component;
|
||||
mod configuration;
|
||||
mod data_item;
|
||||
mod db;
|
||||
mod entity;
|
||||
mod entity_like;
|
||||
mod input;
|
||||
mod interned;
|
||||
mod jar;
|
||||
mod memoized;
|
||||
mod options;
|
||||
mod salsa_struct;
|
||||
mod tracked;
|
||||
mod tracked_fn;
|
||||
mod tracked_struct;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
@ -52,22 +52,17 @@ pub fn db(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||
db::db(args, input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn entity(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
entity::entity(args, input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn interned(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
interned::interned(args, input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
component::component(args, input)
|
||||
pub fn input(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
input::input(args, input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn memoized(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
memoized::memoized(args, input)
|
||||
pub fn tracked(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
tracked::tracked(args, input)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,12 @@ pub(crate) struct Options<A: AllowedOptions> {
|
|||
/// If this is `Some`, the value is the `no_eq` identifier.
|
||||
pub no_eq: Option<syn::Ident>,
|
||||
|
||||
/// The `specify` option is used to signal that a tracked function can
|
||||
/// have its value externally specified (at least some of the time).
|
||||
///
|
||||
/// If this is `Some`, the value is the `specify` identifier.
|
||||
pub specify: Option<syn::Ident>,
|
||||
|
||||
/// The `jar = <type>` option is used to indicate the jar; it defaults to `crate::jar`.
|
||||
///
|
||||
/// If this is `Some`, the value is the `<path>`.
|
||||
|
@ -43,6 +49,7 @@ impl<A: AllowedOptions> Default for Options<A> {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
return_ref: Default::default(),
|
||||
specify: Default::default(),
|
||||
no_eq: Default::default(),
|
||||
jar_ty: Default::default(),
|
||||
db_path: Default::default(),
|
||||
|
@ -55,6 +62,7 @@ impl<A: AllowedOptions> Default for Options<A> {
|
|||
/// These flags determine which options are allowed in a given context
|
||||
pub(crate) trait AllowedOptions {
|
||||
const RETURN_REF: bool;
|
||||
const SPECIFY: bool;
|
||||
const NO_EQ: bool;
|
||||
const JAR: bool;
|
||||
const DATA: bool;
|
||||
|
@ -111,6 +119,20 @@ impl<A: AllowedOptions> syn::parse::Parse for Options<A> {
|
|||
"`no_eq` option not allowed here",
|
||||
));
|
||||
}
|
||||
} else if ident == "specify" {
|
||||
if A::SPECIFY {
|
||||
if let Some(old) = std::mem::replace(&mut options.specify, Some(ident)) {
|
||||
return Err(syn::Error::new(
|
||||
old.span(),
|
||||
"option `specify` provided twice",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"`specify` option not allowed here",
|
||||
));
|
||||
}
|
||||
} else if ident == "jar" {
|
||||
if A::JAR {
|
||||
let _eq = Equals::parse(input)?;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//! Common code for `#[salsa::interned]` and `#[salsa::entity]`
|
||||
//! decorators.
|
||||
//! Common code for `#[salsa::interned]`, `#[salsa::input]`, and
|
||||
//! `#[salsa::tracked]` decorators.
|
||||
//!
|
||||
//! Example of usage:
|
||||
//!
|
||||
|
@ -25,17 +25,21 @@
|
|||
//! * data method `impl Foo { fn data(&self, db: &dyn crate::Db) -> FooData { FooData { f: self.f(db), ... } } }`
|
||||
//! * this could be optimized, particularly for interned fields
|
||||
|
||||
use crate::{data_item::DataItem, options::Options};
|
||||
use heck::CamelCase;
|
||||
|
||||
pub(crate) struct EntityLike {
|
||||
use crate::{configuration, options::Options};
|
||||
|
||||
pub(crate) struct SalsaStruct {
|
||||
args: Options<Self>,
|
||||
data_item: DataItem,
|
||||
fields: Option<Vec<EntityField>>,
|
||||
struct_item: syn::ItemStruct,
|
||||
fields: Vec<SalsaField>,
|
||||
}
|
||||
|
||||
impl crate::options::AllowedOptions for EntityLike {
|
||||
impl crate::options::AllowedOptions for SalsaStruct {
|
||||
const RETURN_REF: bool = false;
|
||||
|
||||
const SPECIFY: bool = false;
|
||||
|
||||
const NO_EQ: bool = false;
|
||||
|
||||
const JAR: bool = true;
|
||||
|
@ -45,20 +49,27 @@ impl crate::options::AllowedOptions for EntityLike {
|
|||
const DB: bool = false;
|
||||
}
|
||||
|
||||
const BANNED_FIELD_NAMES: &[&str] = &["data", "from", "new"];
|
||||
const BANNED_FIELD_NAMES: &[&str] = &["from", "new"];
|
||||
|
||||
impl EntityLike {
|
||||
impl SalsaStruct {
|
||||
pub(crate) fn new(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> syn::Result<Self> {
|
||||
let struct_item = syn::parse(input)?;
|
||||
Self::with_struct(args, struct_item)
|
||||
}
|
||||
|
||||
pub(crate) fn with_struct(
|
||||
args: proc_macro::TokenStream,
|
||||
struct_item: syn::ItemStruct,
|
||||
) -> syn::Result<Self> {
|
||||
let args = syn::parse(args)?;
|
||||
let data_item = syn::parse(input)?;
|
||||
let fields = Self::extract_options(&data_item)?;
|
||||
let fields = Self::extract_options(&struct_item)?;
|
||||
|
||||
Ok(Self {
|
||||
args,
|
||||
data_item,
|
||||
struct_item,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
@ -66,55 +77,44 @@ impl EntityLike {
|
|||
/// Extract out the fields and their options:
|
||||
/// If this is a struct, it must use named fields, so we can define field accessors.
|
||||
/// If it is an enum, then this is not necessary.
|
||||
pub(crate) fn extract_options(data_item: &DataItem) -> syn::Result<Option<Vec<EntityField>>> {
|
||||
match data_item.fields() {
|
||||
Some(f) => match f {
|
||||
syn::Fields::Named(n) => Ok(Some(
|
||||
n.named
|
||||
.iter()
|
||||
.map(EntityField::new)
|
||||
.collect::<syn::Result<Vec<_>>>()?,
|
||||
)),
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
f,
|
||||
"must have named fields for a struct",
|
||||
));
|
||||
}
|
||||
},
|
||||
None => Ok(None),
|
||||
pub(crate) fn extract_options(struct_item: &syn::ItemStruct) -> syn::Result<Vec<SalsaField>> {
|
||||
match &struct_item.fields {
|
||||
syn::Fields::Named(n) => Ok(n
|
||||
.named
|
||||
.iter()
|
||||
.map(SalsaField::new)
|
||||
.collect::<syn::Result<Vec<_>>>()?),
|
||||
f => Err(syn::Error::new_spanned(
|
||||
f,
|
||||
"must have named fields for a struct",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// True if this is wrapped a struct with named fields.
|
||||
pub(crate) fn has_named_fields(&self) -> bool {
|
||||
self.fields.is_some()
|
||||
}
|
||||
|
||||
/// Iterator over all named fields.
|
||||
///
|
||||
/// If this is an enum, empty iterator.
|
||||
pub(crate) fn all_entity_fields(&self) -> impl Iterator<Item = &EntityField> {
|
||||
self.fields.iter().flat_map(|v| v.iter())
|
||||
pub(crate) fn all_fields(&self) -> impl Iterator<Item = &SalsaField> {
|
||||
self.fields.iter()
|
||||
}
|
||||
|
||||
/// Names of all fields (id and value).
|
||||
///
|
||||
/// If this is an enum, empty vec.
|
||||
pub(crate) fn all_field_names(&self) -> Vec<&syn::Ident> {
|
||||
self.all_entity_fields().map(|ef| ef.name()).collect()
|
||||
self.all_fields().map(|ef| ef.name()).collect()
|
||||
}
|
||||
|
||||
/// Types of all fields (id and value).
|
||||
///
|
||||
/// If this is an enum, empty vec.
|
||||
pub(crate) fn all_field_tys(&self) -> Vec<&syn::Type> {
|
||||
self.all_entity_fields().map(|ef| ef.ty()).collect()
|
||||
self.all_fields().map(|ef| ef.ty()).collect()
|
||||
}
|
||||
|
||||
/// The name of the "identity" struct (this is the name the user gave, e.g., `Foo`).
|
||||
pub(crate) fn id_ident(&self) -> &syn::Ident {
|
||||
self.data_item.ident()
|
||||
&self.struct_item.ident
|
||||
}
|
||||
|
||||
/// Type of the jar for this struct
|
||||
|
@ -134,19 +134,22 @@ impl EntityLike {
|
|||
pub(crate) fn data_ident(&self) -> syn::Ident {
|
||||
match &self.args.data {
|
||||
Some(d) => d.clone(),
|
||||
None => syn::Ident::new(&format!("{}Data", self.id_ident()), self.id_ident().span()),
|
||||
None => syn::Ident::new(
|
||||
&format!("__{}Data", self.id_ident()),
|
||||
self.id_ident().span(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate `struct Foo(Id)`
|
||||
pub(crate) fn id_struct(&self) -> syn::ItemStruct {
|
||||
let ident = self.id_ident();
|
||||
let visibility = self.data_item.visibility();
|
||||
let visibility = &self.struct_item.vis;
|
||||
|
||||
// Extract the attributes the user gave, but screen out derive, since we are adding our own.
|
||||
let attrs: Vec<_> = self
|
||||
.data_item
|
||||
.attrs()
|
||||
.struct_item
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attr| !attr.path.is_ident("derive"))
|
||||
.collect();
|
||||
|
@ -162,27 +165,79 @@ impl EntityLike {
|
|||
/// This type inherits all the attributes written by the user.
|
||||
///
|
||||
/// When using named fields, we synthesize the struct and field names.
|
||||
/// This intentionally skips
|
||||
///
|
||||
/// When no named fields are available, copy the existing type.
|
||||
pub(crate) fn data_struct(&self) -> DataItem {
|
||||
let mut d = self.data_item.with_ident(self.data_ident());
|
||||
|
||||
// Remove salsa-specific attributes from the fields.
|
||||
if let DataItem::Struct(s) = &mut d {
|
||||
if let syn::Fields::Named(n) = &mut s.fields {
|
||||
for f in &mut n.named {
|
||||
f.attrs.retain(|f| !is_entity_like_field_attribute(f));
|
||||
}
|
||||
pub(crate) fn data_struct(&self) -> syn::ItemStruct {
|
||||
let ident = self.data_ident();
|
||||
let visibility = self.visibility();
|
||||
let all_field_names = self.all_field_names();
|
||||
let all_field_tys = self.all_field_tys();
|
||||
parse_quote! {
|
||||
/// Internal struct used for interned item
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
#visibility struct #ident {
|
||||
#(
|
||||
#all_field_names: #all_field_tys,
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
d
|
||||
}
|
||||
|
||||
/// Returns the visibility of this item
|
||||
pub(crate) fn visibility(&self) -> &syn::Visibility {
|
||||
self.data_item.visibility()
|
||||
&self.struct_item.vis
|
||||
}
|
||||
|
||||
/// For each of the fields passed as an argument,
|
||||
/// generate a struct named `Ident_Field` and an impl
|
||||
/// of `salsa::function::Configuration` for that struct.
|
||||
pub(crate) fn field_config_structs_and_impls<'a>(
|
||||
&self,
|
||||
fields: impl Iterator<Item = &'a SalsaField>,
|
||||
) -> (Vec<syn::ItemStruct>, Vec<syn::ItemImpl>) {
|
||||
let ident = &self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let visibility = self.visibility();
|
||||
fields
|
||||
.map(|ef| {
|
||||
let value_field_name = ef.name();
|
||||
let value_field_ty = ef.ty();
|
||||
let value_field_backdate = ef.is_backdate_field();
|
||||
let config_name = syn::Ident::new(
|
||||
&format!(
|
||||
"__{}",
|
||||
format!("{}_{}", ident, value_field_name).to_camel_case()
|
||||
),
|
||||
value_field_name.span(),
|
||||
);
|
||||
let item_struct: syn::ItemStruct = parse_quote! {
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Debug)]
|
||||
#visibility struct #config_name(std::convert::Infallible);
|
||||
};
|
||||
|
||||
let should_backdate_value_fn = configuration::should_backdate_value_fn(value_field_backdate);
|
||||
let item_impl: syn::ItemImpl = parse_quote! {
|
||||
impl salsa::function::Configuration for #config_name {
|
||||
type Jar = #jar_ty;
|
||||
type Key = #ident;
|
||||
type Value = #value_field_ty;
|
||||
const CYCLE_STRATEGY: salsa::cycle::CycleRecoveryStrategy = salsa::cycle::CycleRecoveryStrategy::Panic;
|
||||
|
||||
#should_backdate_value_fn
|
||||
|
||||
fn execute(db: &salsa::function::DynDb<Self>, key: Self::Key) -> Self::Value {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn recover_from_cycle(db: &salsa::function::DynDb<Self>, cycle: &salsa::Cycle, key: Self::Key) -> Self::Value {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(item_struct, item_impl)
|
||||
})
|
||||
.unzip()
|
||||
}
|
||||
|
||||
/// Generate `impl salsa::AsId for Foo`
|
||||
|
@ -201,15 +256,35 @@ impl EntityLike {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
/// Disallow `#[id]` attributes on the fields of this struct.
|
||||
///
|
||||
/// If an `#[id]` field is found, return an error.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `kind`, the attribute name (e.g., `input` or `interned`)
|
||||
pub(crate) fn disallow_id_fields(&self, kind: &str) -> syn::Result<()> {
|
||||
for ef in self.all_fields() {
|
||||
if ef.has_id_attr {
|
||||
return Err(syn::Error::new(
|
||||
ef.name().span(),
|
||||
format!("`#[id]` cannot be used with `#[salsa::{kind}]`"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const FIELD_OPTION_ATTRIBUTES: &[(&str, fn(&syn::Attribute, &mut EntityField))] = &[
|
||||
pub(crate) const FIELD_OPTION_ATTRIBUTES: &[(&str, fn(&syn::Attribute, &mut SalsaField))] = &[
|
||||
("id", |_, ef| ef.has_id_attr = true),
|
||||
("return_ref", |_, ef| ef.has_ref_attr = true),
|
||||
("no_eq", |_, ef| ef.has_no_eq_attr = true),
|
||||
];
|
||||
|
||||
pub(crate) struct EntityField {
|
||||
pub(crate) struct SalsaField {
|
||||
field: syn::Field,
|
||||
|
||||
pub(crate) has_id_attr: bool,
|
||||
|
@ -217,7 +292,7 @@ pub(crate) struct EntityField {
|
|||
pub(crate) has_no_eq_attr: bool,
|
||||
}
|
||||
|
||||
impl EntityField {
|
||||
impl SalsaField {
|
||||
pub(crate) fn new(field: &syn::Field) -> syn::Result<Self> {
|
||||
let field_name = field.ident.as_ref().unwrap();
|
||||
let field_name_str = field_name.to_string();
|
||||
|
@ -231,7 +306,7 @@ impl EntityField {
|
|||
));
|
||||
}
|
||||
|
||||
let mut result = EntityField {
|
||||
let mut result = SalsaField {
|
||||
field: field.clone(),
|
||||
has_id_attr: false,
|
||||
has_ref_attr: false,
|
18
components/salsa-entity-macros/src/tracked.rs
Normal file
18
components/salsa-entity-macros/src/tracked.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use syn::{spanned::Spanned, Item};
|
||||
|
||||
pub(crate) fn tracked(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let item = syn::parse_macro_input!(input as Item);
|
||||
match item {
|
||||
syn::Item::Struct(item) => crate::tracked_struct::tracked(args, item),
|
||||
syn::Item::Fn(item) => crate::tracked_fn::tracked(args, item),
|
||||
_ => syn::Error::new(
|
||||
item.span(),
|
||||
&format!("tracked can be applied to structs and functions only"),
|
||||
)
|
||||
.into_compile_error()
|
||||
.into(),
|
||||
}
|
||||
}
|
|
@ -1,33 +1,47 @@
|
|||
use proc_macro2::Literal;
|
||||
use proc_macro2::{Literal, TokenStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{ItemFn, ReturnType, Token};
|
||||
use syn::{ReturnType, Token};
|
||||
|
||||
use crate::configuration::{self, Configuration, CycleRecoveryStrategy};
|
||||
use crate::options::Options;
|
||||
|
||||
// #[salsa::memoized(jar = Jar0)]
|
||||
// fn my_func(db: &dyn Jar0Db, input1: u32, input2: u32) -> String {
|
||||
// format!("Hello, world")
|
||||
// }
|
||||
|
||||
pub(crate) fn memoized(
|
||||
pub(crate) fn tracked(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
item_fn: syn::ItemFn,
|
||||
) -> proc_macro::TokenStream {
|
||||
let args = syn::parse_macro_input!(args as Args);
|
||||
let item_fn = syn::parse_macro_input!(input as ItemFn);
|
||||
match tracked_fn(args, item_fn) {
|
||||
Ok(p) => p.into(),
|
||||
Err(e) => return e.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn tracked_fn(args: Args, item_fn: syn::ItemFn) -> syn::Result<TokenStream> {
|
||||
if item_fn.sig.inputs.len() <= 1 {
|
||||
return Err(syn::Error::new(
|
||||
item_fn.sig.ident.span(),
|
||||
"tracked functions must have at least a database and salsa struct argument",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(s) = &args.specify {
|
||||
if requires_interning(&item_fn) {
|
||||
return Err(syn::Error::new(
|
||||
s.span(),
|
||||
"tracked functon takes too many argments to have its value set with `specify`",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let struct_item = configuration_struct(&item_fn);
|
||||
let configuration = fn_configuration(&args, &item_fn);
|
||||
let struct_item_ident = &struct_item.ident;
|
||||
let struct_ty: syn::Type = parse_quote!(#struct_item_ident);
|
||||
let configuration_impl = configuration.to_impl(&struct_ty);
|
||||
let ingredients_for_impl = ingredients_for_impl(&args, &struct_ty);
|
||||
let (getter, setter) = match wrapper_fns(&args, &item_fn, &struct_ty) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return e.into_compile_error().into(),
|
||||
};
|
||||
let config_ty: syn::Type = parse_quote!(#struct_item_ident);
|
||||
let configuration_impl = configuration.to_impl(&config_ty);
|
||||
let ingredients_for_impl = ingredients_for_impl(&args, &item_fn, &config_ty);
|
||||
let (getter, item_impl) = wrapper_fns(&args, &item_fn, &config_ty)?;
|
||||
|
||||
proc_macro::TokenStream::from(quote! {
|
||||
Ok(quote! {
|
||||
#struct_item
|
||||
#configuration_impl
|
||||
#ingredients_for_impl
|
||||
|
@ -36,17 +50,19 @@ pub(crate) fn memoized(
|
|||
// sometimes doesn't like
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
#getter
|
||||
#setter
|
||||
#item_impl
|
||||
})
|
||||
}
|
||||
|
||||
type Args = Options<Memoized>;
|
||||
type Args = Options<TrackedFn>;
|
||||
|
||||
struct Memoized;
|
||||
struct TrackedFn;
|
||||
|
||||
impl crate::options::AllowedOptions for Memoized {
|
||||
impl crate::options::AllowedOptions for TrackedFn {
|
||||
const RETURN_REF: bool = true;
|
||||
|
||||
const SPECIFY: bool = true;
|
||||
|
||||
const NO_EQ: bool = true;
|
||||
|
||||
const JAR: bool = true;
|
||||
|
@ -56,6 +72,8 @@ impl crate::options::AllowedOptions for Memoized {
|
|||
const DB: bool = false;
|
||||
}
|
||||
|
||||
/// Returns the key type for this tracked function.
|
||||
/// This is a tuple of all the argument types (apart from the database).
|
||||
fn key_tuple_ty(item_fn: &syn::ItemFn) -> syn::Type {
|
||||
let arg_tys = item_fn.sig.inputs.iter().skip(1).map(|arg| match arg {
|
||||
syn::FnArg::Receiver(_) => unreachable!(),
|
||||
|
@ -69,19 +87,49 @@ fn key_tuple_ty(item_fn: &syn::ItemFn) -> syn::Type {
|
|||
|
||||
fn configuration_struct(item_fn: &syn::ItemFn) -> syn::ItemStruct {
|
||||
let fn_name = item_fn.sig.ident.clone();
|
||||
let key_tuple_ty = key_tuple_ty(item_fn);
|
||||
let visibility = &item_fn.vis;
|
||||
|
||||
let salsa_struct_ty = salsa_struct_ty(item_fn);
|
||||
let intern_map: syn::Type = if requires_interning(item_fn) {
|
||||
let key_ty = key_tuple_ty(item_fn);
|
||||
parse_quote! { salsa::interned::InternedIngredient<salsa::Id, #key_ty> }
|
||||
} else {
|
||||
parse_quote! { salsa::interned::IdentityInterner<#salsa_struct_ty> }
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct #fn_name {
|
||||
intern_map: salsa::interned::InternedIngredient<salsa::Id, #key_tuple_ty>,
|
||||
#visibility struct #fn_name
|
||||
where
|
||||
#salsa_struct_ty: salsa::AsId, // require that the salsa struct is, well, a salsa struct!
|
||||
{
|
||||
intern_map: #intern_map,
|
||||
function: salsa::function::FunctionIngredient<Self>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True if this fn takes more arguments.
|
||||
fn requires_interning(item_fn: &syn::ItemFn) -> bool {
|
||||
item_fn.sig.inputs.len() > 2
|
||||
}
|
||||
|
||||
/// Every tracked fn takes a salsa struct as its second argument.
|
||||
/// This fn returns the type of that second argument.
|
||||
fn salsa_struct_ty(item_fn: &syn::ItemFn) -> &syn::Type {
|
||||
match &item_fn.sig.inputs[1] {
|
||||
syn::FnArg::Receiver(_) => panic!("receiver not expected"),
|
||||
syn::FnArg::Typed(pat_ty) => &pat_ty.ty,
|
||||
}
|
||||
}
|
||||
|
||||
fn fn_configuration(args: &Args, item_fn: &syn::ItemFn) -> Configuration {
|
||||
let jar_ty = args.jar_ty();
|
||||
let key_ty = parse_quote!(salsa::id::Id);
|
||||
let key_ty = if requires_interning(item_fn) {
|
||||
parse_quote!(salsa::id::Id)
|
||||
} else {
|
||||
salsa_struct_ty(item_fn).clone()
|
||||
};
|
||||
let value_ty = configuration::value_ty(&item_fn.sig);
|
||||
|
||||
// FIXME: these are hardcoded for now
|
||||
|
@ -126,10 +174,33 @@ fn fn_configuration(args: &Args, item_fn: &syn::ItemFn) -> Configuration {
|
|||
}
|
||||
}
|
||||
|
||||
fn ingredients_for_impl(args: &Args, struct_ty: &syn::Type) -> syn::ItemImpl {
|
||||
fn ingredients_for_impl(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::ItemImpl {
|
||||
let jar_ty = args.jar_ty();
|
||||
|
||||
let intern_map: syn::Expr = if requires_interning(item_fn) {
|
||||
parse_quote! {
|
||||
{
|
||||
let index = ingredients.push(|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
|
||||
let ingredients =
|
||||
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
|
||||
&ingredients.intern_map
|
||||
});
|
||||
salsa::interned::InternedIngredient::new(index)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parse_quote! {
|
||||
salsa::interned::IdentityInterner::new()
|
||||
}
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
impl salsa::storage::IngredientsFor for #struct_ty {
|
||||
impl salsa::storage::IngredientsFor for #config_ty {
|
||||
type Ingredients = Self;
|
||||
type Jar = #jar_ty;
|
||||
|
||||
|
@ -138,15 +209,7 @@ fn ingredients_for_impl(args: &Args, struct_ty: &syn::Type) -> syn::ItemImpl {
|
|||
DB: salsa::DbWithJar<Self::Jar> + salsa::storage::JarFromJars<Self::Jar>,
|
||||
{
|
||||
Self {
|
||||
intern_map: {
|
||||
let index = ingredients.push(|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
|
||||
let ingredients =
|
||||
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
|
||||
&ingredients.intern_map
|
||||
});
|
||||
salsa::interned::InternedIngredient::new(index)
|
||||
},
|
||||
intern_map: #intern_map,
|
||||
|
||||
function: {
|
||||
let index = ingredients.push(|jars| {
|
||||
|
@ -166,17 +229,18 @@ fn ingredients_for_impl(args: &Args, struct_ty: &syn::Type) -> syn::ItemImpl {
|
|||
fn wrapper_fns(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::Result<(syn::ItemFn, syn::ItemImpl)> {
|
||||
// The "getter" has same signature as the original:
|
||||
let getter_fn = getter_fn(args, item_fn, struct_ty)?;
|
||||
let getter_fn = getter_fn(args, item_fn, config_ty)?;
|
||||
|
||||
let ref_getter_fn = ref_getter_fn(args, item_fn, struct_ty)?;
|
||||
let accumulated_fn = accumulated_fn(args, item_fn, struct_ty)?;
|
||||
let setter_fn = setter_fn(args, item_fn, struct_ty)?;
|
||||
let ref_getter_fn = ref_getter_fn(args, item_fn, config_ty)?;
|
||||
let accumulated_fn = accumulated_fn(args, item_fn, config_ty)?;
|
||||
let setter_fn = setter_fn(args, item_fn, config_ty)?;
|
||||
let specify_fn = specify_fn(args, item_fn, config_ty)?.map(|f| quote! { #f });
|
||||
|
||||
let setter_impl: syn::ItemImpl = parse_quote! {
|
||||
impl #struct_ty {
|
||||
impl #config_ty {
|
||||
#[allow(dead_code, clippy::needless_lifetimes)]
|
||||
#ref_getter_fn
|
||||
|
||||
|
@ -185,16 +249,19 @@ fn wrapper_fns(
|
|||
|
||||
#[allow(dead_code, clippy::needless_lifetimes)]
|
||||
#accumulated_fn
|
||||
|
||||
#specify_fn
|
||||
}
|
||||
};
|
||||
|
||||
Ok((getter_fn, setter_impl))
|
||||
}
|
||||
|
||||
/// Creates the `get` associated function.
|
||||
fn getter_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ItemFn> {
|
||||
let mut getter_fn = item_fn.clone();
|
||||
let arg_idents: Vec<_> = item_fn
|
||||
|
@ -215,23 +282,27 @@ fn getter_fn(
|
|||
getter_fn = make_fn_return_ref(getter_fn)?;
|
||||
getter_fn.block = Box::new(parse_quote_spanned! {
|
||||
item_fn.block.span() => {
|
||||
#struct_ty::get(#(#arg_idents,)*)
|
||||
#config_ty::get(#(#arg_idents,)*)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getter_fn.block = Box::new(parse_quote_spanned! {
|
||||
item_fn.block.span() => {
|
||||
Clone::clone(#struct_ty::get(#(#arg_idents,)*))
|
||||
Clone::clone(#config_ty::get(#(#arg_idents,)*))
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(getter_fn)
|
||||
}
|
||||
|
||||
/// Creates a `get` associated function that returns `&Value`
|
||||
/// (to be used when `return_ref` is specified).
|
||||
///
|
||||
/// (Helper for `getter_fn`)
|
||||
fn ref_getter_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ItemFn> {
|
||||
let jar_ty = args.jar_ty();
|
||||
let mut ref_getter_fn = item_fn.clone();
|
||||
|
@ -242,8 +313,8 @@ fn ref_getter_fn(
|
|||
ref_getter_fn.block = parse_quote! {
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#struct_ty>>::ingredient(__jar);
|
||||
let __key = __ingredients.intern_map.intern(__runtime, (#(#arg_names,)*));
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#config_ty>>::ingredient(__jar);
|
||||
let __key = __ingredients.intern_map.intern(__runtime, (#(#arg_names),*));
|
||||
__ingredients.function.fetch(#db_var, __key)
|
||||
}
|
||||
};
|
||||
|
@ -251,10 +322,12 @@ fn ref_getter_fn(
|
|||
Ok(ref_getter_fn)
|
||||
}
|
||||
|
||||
/// Creates a `set` associated function that can be used to set (given an `&mut db`)
|
||||
/// the value for this function for some inputs.
|
||||
fn setter_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ImplItemMethod> {
|
||||
// The setter has *always* the same signature as the original:
|
||||
// but it takes a value arg and has no return type.
|
||||
|
@ -284,14 +357,52 @@ fn setter_fn(
|
|||
block: parse_quote! {
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar_mut(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#struct_ty>>::ingredient_mut(__jar);
|
||||
let __key = __ingredients.intern_map.intern(__runtime, (#(#arg_names,)*));
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#config_ty>>::ingredient_mut(__jar);
|
||||
let __key = __ingredients.intern_map.intern(__runtime, (#(#arg_names),*));
|
||||
__ingredients.function.store(__runtime, __key, #value_arg, salsa::Durability::LOW)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn specify_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::Result<Option<syn::ImplItemMethod>> {
|
||||
let specify = match &args.specify {
|
||||
Some(s) => s,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// `specify` has the same signature as the original,
|
||||
// but it takes a value arg and has no return type.
|
||||
let jar_ty = args.jar_ty();
|
||||
let (db_var, arg_names) = fn_args(item_fn)?;
|
||||
let mut setter_sig = item_fn.sig.clone();
|
||||
let value_ty = configuration::value_ty(&item_fn.sig);
|
||||
setter_sig.ident = syn::Ident::new("specify", item_fn.sig.ident.span());
|
||||
let value_arg = syn::Ident::new("__value", item_fn.sig.output.span());
|
||||
setter_sig.inputs.push(parse_quote!(#value_arg: #value_ty));
|
||||
setter_sig.output = ReturnType::Default;
|
||||
Ok(Some(syn::ImplItemMethod {
|
||||
attrs: vec![],
|
||||
vis: item_fn.vis.clone(),
|
||||
defaultness: None,
|
||||
sig: setter_sig,
|
||||
block: parse_quote! {
|
||||
{
|
||||
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#config_ty>>::ingredient(__jar);
|
||||
__ingredients.function.set(#db_var, #(#arg_names,)* #value_arg)
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
/// Given a function def tagged with `#[return_ref]`, modifies `ref_getter_fn`
|
||||
/// so that it returns an `&Value` instead of `Value`. May introduce a name for the
|
||||
/// database lifetime if required.
|
||||
fn make_fn_return_ref(mut ref_getter_fn: syn::ItemFn) -> syn::Result<syn::ItemFn> {
|
||||
// The 0th input should be a `&dyn Foo`. We need to ensure
|
||||
// it has a named lifetime parameter.
|
||||
|
@ -317,6 +428,9 @@ fn make_fn_return_ref(mut ref_getter_fn: syn::ItemFn) -> syn::Result<syn::ItemFn
|
|||
Ok(ref_getter_fn)
|
||||
}
|
||||
|
||||
/// Given an item function, identifies the name given to the `&dyn Db` reference and returns it,
|
||||
/// along with the type of the database. If the database lifetime did not have a name,
|
||||
/// then modifies the item function so that it is called `'__db` and returns that.
|
||||
fn db_lifetime_and_ty(func: &mut syn::ItemFn) -> syn::Result<(syn::Lifetime, &syn::Type)> {
|
||||
match &mut func.sig.inputs[0] {
|
||||
syn::FnArg::Receiver(r) => {
|
||||
|
@ -359,10 +473,13 @@ fn db_lifetime_and_ty(func: &mut syn::ItemFn) -> syn::Result<(syn::Lifetime, &sy
|
|||
}
|
||||
}
|
||||
|
||||
/// Generates the `accumulated` function, which invokes `accumulated`
|
||||
/// on the function ingredient to extract the values pushed (transitively)
|
||||
/// into an accumulator.
|
||||
fn accumulated_fn(
|
||||
args: &Args,
|
||||
item_fn: &syn::ItemFn,
|
||||
struct_ty: &syn::Type,
|
||||
config_ty: &syn::Type,
|
||||
) -> syn::Result<syn::ItemFn> {
|
||||
let jar_ty = args.jar_ty();
|
||||
|
||||
|
@ -388,8 +505,8 @@ fn accumulated_fn(
|
|||
accumulated_fn.block = parse_quote! {
|
||||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(#db_var);
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#struct_ty>>::ingredient(__jar);
|
||||
let __key = __ingredients.intern_map.intern(__runtime, (#(#arg_names,)*));
|
||||
let __ingredients = <_ as salsa::storage::HasIngredientsFor<#config_ty>>::ingredient(__jar);
|
||||
let __key = __ingredients.intern_map.intern(__runtime, (#(#arg_names),*));
|
||||
__ingredients.function.accumulated::<__A>(#db_var, __key)
|
||||
}
|
||||
};
|
||||
|
@ -397,6 +514,10 @@ fn accumulated_fn(
|
|||
Ok(accumulated_fn)
|
||||
}
|
||||
|
||||
/// Examines the function arguments and returns a tuple of:
|
||||
///
|
||||
/// * the name of the database argument
|
||||
/// * the name(s) of the key arguments
|
||||
fn fn_args(item_fn: &syn::ItemFn) -> syn::Result<(proc_macro2::Ident, Vec<proc_macro2::Ident>)> {
|
||||
// Check that we have no receiver and that all argments have names
|
||||
if item_fn.sig.inputs.len() == 0 {
|
|
@ -1,96 +1,60 @@
|
|||
use heck::CamelCase;
|
||||
use proc_macro2::{Literal, TokenStream};
|
||||
|
||||
use crate::configuration;
|
||||
use crate::entity_like::{EntityField, EntityLike};
|
||||
use crate::salsa_struct::{SalsaField, SalsaStruct};
|
||||
|
||||
// #[salsa::Entity(#id_ident in Jar0)]
|
||||
// #[derive(Eq, PartialEq, Hash, Debug, Clone)]
|
||||
// struct EntityData0 {
|
||||
// id: u32
|
||||
// }
|
||||
|
||||
pub(crate) fn entity(
|
||||
/// For an tracked struct `Foo` with fields `f1: T1, ..., fN: TN`, we generate...
|
||||
///
|
||||
/// * the "id struct" `struct Foo(salsa::Id)`
|
||||
/// * the tracked ingredient, which maps the id fields to the `Id`
|
||||
/// * for each value field, a function ingredient
|
||||
pub(crate) fn tracked(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
struct_item: syn::ItemStruct,
|
||||
) -> proc_macro::TokenStream {
|
||||
match EntityLike::new(args, input).and_then(|el| el.generate_entity()) {
|
||||
match SalsaStruct::with_struct(args, struct_item).and_then(|el| el.generate_tracked()) {
|
||||
Ok(s) => s.into(),
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityLike {
|
||||
fn generate_entity(&self) -> syn::Result<TokenStream> {
|
||||
self.validate_entity()?;
|
||||
impl SalsaStruct {
|
||||
fn generate_tracked(&self) -> syn::Result<TokenStream> {
|
||||
self.validate_tracked()?;
|
||||
|
||||
let config_structs = self.config_structs();
|
||||
let (config_structs, config_impls) =
|
||||
self.field_config_structs_and_impls(self.value_fields());
|
||||
|
||||
let id_struct = self.id_struct();
|
||||
let inherent_impl = self.id_inherent_impl();
|
||||
let ingredients_for_impl = self.id_ingredients_for_impl(&config_structs);
|
||||
let entity_in_db_impl = self.entity_in_db_impl();
|
||||
let inherent_impl = self.tracked_inherent_impl();
|
||||
let ingredients_for_impl = self.tracked_struct_ingredients(&config_structs);
|
||||
let tracked_struct_in_db_impl = self.tracked_struct_in_db_impl();
|
||||
let as_id_impl = self.as_id_impl();
|
||||
let config_impls = self.config_impls(&config_structs);
|
||||
|
||||
Ok(quote! {
|
||||
#(#config_structs)*
|
||||
#id_struct
|
||||
#inherent_impl
|
||||
#ingredients_for_impl
|
||||
#entity_in_db_impl
|
||||
#tracked_struct_in_db_impl
|
||||
#as_id_impl
|
||||
#(#config_impls)*
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_entity(&self) -> syn::Result<()> {
|
||||
// Require that entities are structs for now.
|
||||
if !self.has_named_fields() {
|
||||
return Err(syn::Error::new(
|
||||
self.id_ident().span(),
|
||||
"entities must be structs with named fields",
|
||||
));
|
||||
}
|
||||
|
||||
fn validate_tracked(&self) -> syn::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For each of the value fields in the entity,
|
||||
/// we will generate a memoized function that stores its value.
|
||||
/// Generate a struct for the "Configuration" of each of those functions.
|
||||
fn config_structs(&self) -> Vec<syn::ItemStruct> {
|
||||
let ident = &self.id_ident();
|
||||
let visibility = self.visibility();
|
||||
self.value_fields()
|
||||
.map(EntityField::name)
|
||||
.map(|value_field_name| {
|
||||
let config_name = syn::Ident::new(
|
||||
&format!(
|
||||
"__{}",
|
||||
format!("{}_{}", ident, value_field_name).to_camel_case()
|
||||
),
|
||||
value_field_name.span(),
|
||||
);
|
||||
parse_quote! {
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Debug)]
|
||||
#visibility struct #config_name(std::convert::Infallible);
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate an inherent impl with methods on the entity type.
|
||||
fn id_inherent_impl(&self) -> syn::ItemImpl {
|
||||
/// Generate an inherent impl with methods on the tracked type.
|
||||
fn tracked_inherent_impl(&self) -> syn::ItemImpl {
|
||||
let ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let db_dyn_ty = self.db_dyn_ty();
|
||||
let entity_index = self.entity_index();
|
||||
let struct_index = self.tracked_struct_index();
|
||||
|
||||
let id_field_indices: Vec<_> = self.id_field_indices();
|
||||
let id_field_names: Vec<_> = self.id_fields().map(EntityField::name).collect();
|
||||
let id_field_tys: Vec<_> = self.id_fields().map(EntityField::ty).collect();
|
||||
let id_field_clones: Vec<_> = self.id_fields().map(EntityField::is_clone_field).collect();
|
||||
let id_field_names: Vec<_> = self.id_fields().map(SalsaField::name).collect();
|
||||
let id_field_tys: Vec<_> = self.id_fields().map(SalsaField::ty).collect();
|
||||
let id_field_clones: Vec<_> = self.id_fields().map(SalsaField::is_clone_field).collect();
|
||||
let id_field_getters: Vec<syn::ImplItemMethod> = id_field_indices.iter().zip(&id_field_names).zip(&id_field_tys).zip(&id_field_clones).map(|(((field_index, field_name), field_ty), is_clone_field)|
|
||||
if !*is_clone_field {
|
||||
parse_quote! {
|
||||
|
@ -98,7 +62,7 @@ impl EntityLike {
|
|||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar);
|
||||
&__ingredients.#entity_index.entity_data(__runtime, self).#field_index
|
||||
&__ingredients.#struct_index.tracked_struct_data(__runtime, self).#field_index
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -107,7 +71,7 @@ impl EntityLike {
|
|||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar);
|
||||
__ingredients.#entity_index.entity_data(__runtime, self).#field_index.clone()
|
||||
__ingredients.#struct_index.tracked_struct_data(__runtime, self).#field_index.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,11 +79,11 @@ impl EntityLike {
|
|||
.collect();
|
||||
|
||||
let value_field_indices = self.value_field_indices();
|
||||
let value_field_names: Vec<_> = self.value_fields().map(EntityField::name).collect();
|
||||
let value_field_tys: Vec<_> = self.value_fields().map(EntityField::ty).collect();
|
||||
let value_field_names: Vec<_> = self.value_fields().map(SalsaField::name).collect();
|
||||
let value_field_tys: Vec<_> = self.value_fields().map(SalsaField::ty).collect();
|
||||
let value_field_clones: Vec<_> = self
|
||||
.value_fields()
|
||||
.map(EntityField::is_clone_field)
|
||||
.map(SalsaField::is_clone_field)
|
||||
.collect();
|
||||
let value_field_getters: Vec<syn::ImplItemMethod> = value_field_indices.iter().zip(&value_field_names).zip(&value_field_tys).zip(&value_field_clones).map(|(((field_index, field_name), field_ty), is_clone_field)|
|
||||
if !*is_clone_field {
|
||||
|
@ -153,7 +117,7 @@ impl EntityLike {
|
|||
{
|
||||
let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db);
|
||||
let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar);
|
||||
let __id = __ingredients.#entity_index.new_entity(__runtime, (#(#id_field_names,)*));
|
||||
let __id = __ingredients.#struct_index.new_struct(__runtime, (#(#id_field_names,)*));
|
||||
#(
|
||||
__ingredients.#value_field_indices.set(__db, __id, #value_field_names);
|
||||
)*
|
||||
|
@ -167,16 +131,16 @@ impl EntityLike {
|
|||
}
|
||||
}
|
||||
|
||||
/// Generate the `IngredientsFor` impl for this entity.
|
||||
/// Generate the `IngredientsFor` impl for this tracked struct.
|
||||
///
|
||||
/// The entity's ingredients include both the main entity ingredient along with a
|
||||
/// The tracked struct's ingredients include both the main tracked struct ingredient along with a
|
||||
/// function ingredient for each of the value fields.
|
||||
fn id_ingredients_for_impl(&self, config_structs: &[syn::ItemStruct]) -> syn::ItemImpl {
|
||||
fn tracked_struct_ingredients(&self, config_structs: &[syn::ItemStruct]) -> syn::ItemImpl {
|
||||
let ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let id_field_tys: Vec<&syn::Type> = self.id_fields().map(EntityField::ty).collect();
|
||||
let id_field_tys: Vec<&syn::Type> = self.id_fields().map(SalsaField::ty).collect();
|
||||
let value_field_indices: Vec<Literal> = self.value_field_indices();
|
||||
let entity_index: Literal = self.entity_index();
|
||||
let tracked_struct_index: Literal = self.tracked_struct_index();
|
||||
let config_struct_names = config_structs.iter().map(|s| &s.ident);
|
||||
|
||||
parse_quote! {
|
||||
|
@ -186,7 +150,7 @@ impl EntityLike {
|
|||
#(
|
||||
salsa::function::FunctionIngredient<#config_struct_names>,
|
||||
)*
|
||||
salsa::entity::EntityIngredient<#ident, (#(#id_field_tys,)*)>,
|
||||
salsa::tracked_struct::TrackedStructIngredient<#ident, (#(#id_field_tys,)*)>,
|
||||
);
|
||||
|
||||
fn create_ingredients<DB>(
|
||||
|
@ -213,15 +177,15 @@ impl EntityLike {
|
|||
|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
|
||||
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
|
||||
&ingredients.#entity_index
|
||||
&ingredients.#tracked_struct_index
|
||||
},
|
||||
|jars| {
|
||||
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
|
||||
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient_mut(jar);
|
||||
&mut ingredients.#entity_index
|
||||
&mut ingredients.#tracked_struct_index
|
||||
},
|
||||
);
|
||||
salsa::entity::EntityIngredient::new(index)
|
||||
salsa::tracked_struct::TrackedStructIngredient::new(index)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -229,84 +193,48 @@ impl EntityLike {
|
|||
}
|
||||
}
|
||||
|
||||
/// Implementation of `EntityInDb` for this entity.
|
||||
fn entity_in_db_impl(&self) -> syn::ItemImpl {
|
||||
/// Implementation of `TrackedStructInDb`.
|
||||
fn tracked_struct_in_db_impl(&self) -> syn::ItemImpl {
|
||||
let ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let entity_index = self.entity_index();
|
||||
let tracked_struct_index = self.tracked_struct_index();
|
||||
parse_quote! {
|
||||
impl<DB> salsa::entity::EntityInDb<DB> for #ident
|
||||
impl<DB> salsa::tracked_struct::TrackedStructInDb<DB> for #ident
|
||||
where
|
||||
DB: ?Sized + salsa::DbWithJar<#jar_ty>,
|
||||
{
|
||||
fn database_key_index(self, db: &DB) -> salsa::DatabaseKeyIndex {
|
||||
let (jar, _) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
|
||||
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor<#ident>>::ingredient(jar);
|
||||
ingredients.#entity_index.database_key_index(self)
|
||||
ingredients.#tracked_struct_index.database_key_index(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn config_impls(&self, config_structs: &[syn::ItemStruct]) -> Vec<syn::ItemImpl> {
|
||||
let ident = self.id_ident();
|
||||
let jar_ty = self.jar_ty();
|
||||
let value_field_tys = self.value_fields().map(EntityField::ty);
|
||||
let value_field_backdates = self.value_fields().map(EntityField::is_backdate_field);
|
||||
value_field_tys
|
||||
.into_iter()
|
||||
.zip(config_structs.iter().map(|s| &s.ident))
|
||||
.zip(value_field_backdates)
|
||||
.map(|((value_field_ty, config_struct_name), value_field_backdate)| {
|
||||
let should_backdate_value_fn = configuration::should_backdate_value_fn(value_field_backdate);
|
||||
|
||||
parse_quote! {
|
||||
impl salsa::function::Configuration for #config_struct_name {
|
||||
type Jar = #jar_ty;
|
||||
type Key = #ident;
|
||||
type Value = #value_field_ty;
|
||||
const CYCLE_STRATEGY: salsa::cycle::CycleRecoveryStrategy = salsa::cycle::CycleRecoveryStrategy::Panic;
|
||||
|
||||
#should_backdate_value_fn
|
||||
|
||||
fn execute(db: &salsa::function::DynDb<Self>, key: Self::Key) -> Self::Value {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn recover_from_cycle(db: &salsa::function::DynDb<Self>, cycle: &salsa::Cycle, key: Self::Key) -> Self::Value {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// List of id fields (fields that are part of the entity's identity across revisions).
|
||||
/// List of id fields (fields that are part of the tracked struct's identity across revisions).
|
||||
///
|
||||
/// If this is an enum, empty iterator.
|
||||
fn id_fields(&self) -> impl Iterator<Item = &EntityField> {
|
||||
self.all_entity_fields()
|
||||
.filter(|ef| ef.is_entity_id_field())
|
||||
fn id_fields(&self) -> impl Iterator<Item = &SalsaField> {
|
||||
self.all_fields().filter(|ef| ef.is_id_field())
|
||||
}
|
||||
|
||||
/// List of value fields (fields that are not part of the entity's identity across revisions).
|
||||
/// List of value fields (fields that are not part of the tracked struct's identity across revisions).
|
||||
///
|
||||
/// If this is an enum, empty iterator.
|
||||
fn value_fields(&self) -> impl Iterator<Item = &EntityField> {
|
||||
self.all_entity_fields()
|
||||
.filter(|ef| !ef.is_entity_id_field())
|
||||
fn value_fields(&self) -> impl Iterator<Item = &SalsaField> {
|
||||
self.all_fields().filter(|ef| !ef.is_id_field())
|
||||
}
|
||||
|
||||
/// For the entity, we create a tuple that contains the function ingredients
|
||||
/// for each "other" field and the entity ingredient. This is the index of
|
||||
/// For this struct, we create a tuple that contains the function ingredients
|
||||
/// for each "other" field and the tracked-struct ingredient. This is the index of
|
||||
/// the entity ingredient within that tuple.
|
||||
fn entity_index(&self) -> Literal {
|
||||
fn tracked_struct_index(&self) -> Literal {
|
||||
Literal::usize_unsuffixed(self.value_fields().count())
|
||||
}
|
||||
|
||||
/// For the entity, we create a tuple that contains the function ingredients
|
||||
/// for each "other" field and the entity ingredient. These are the indices
|
||||
/// For this struct, we create a tuple that contains the function ingredients
|
||||
/// for each "other" field and the tracked-struct ingredient. These are the indices
|
||||
/// of the function ingredients within that tuple.
|
||||
fn value_field_indices(&self) -> Vec<Literal> {
|
||||
(0..self.value_fields().count())
|
||||
|
@ -322,9 +250,9 @@ impl EntityLike {
|
|||
}
|
||||
}
|
||||
|
||||
impl EntityField {
|
||||
impl SalsaField {
|
||||
/// true if this is an id field
|
||||
fn is_entity_id_field(&self) -> bool {
|
||||
fn is_id_field(&self) -> bool {
|
||||
self.has_id_attr
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
use crossbeam::atomic::AtomicCell;
|
||||
|
||||
use crate::{
|
||||
entity::EntityInDb,
|
||||
key::DependencyIndex,
|
||||
runtime::local_state::{QueryInputs, QueryRevisions},
|
||||
tracked_struct::TrackedStructInDb,
|
||||
Database,
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ where
|
|||
{
|
||||
pub fn set<'db>(&self, db: &'db DynDb<'db, C>, key: C::Key, value: C::Value)
|
||||
where
|
||||
C::Key: EntityInDb<DynDb<'db, C>>,
|
||||
C::Key: TrackedStructInDb<DynDb<'db, C>>,
|
||||
{
|
||||
let runtime = db.salsa_runtime();
|
||||
|
||||
|
|
|
@ -1 +1,64 @@
|
|||
use crate::{
|
||||
cycle::CycleRecoveryStrategy,
|
||||
ingredient::Ingredient,
|
||||
key::{DatabaseKeyIndex, DependencyIndex},
|
||||
runtime::{local_state::QueryInputs, Runtime},
|
||||
AsId, IngredientIndex, Revision,
|
||||
};
|
||||
|
||||
pub trait InputId: AsId {}
|
||||
impl<T: AsId> InputId for T {}
|
||||
|
||||
pub struct InputIngredient<Id>
|
||||
where
|
||||
Id: InputId,
|
||||
{
|
||||
ingredient_index: IngredientIndex,
|
||||
counter: u32,
|
||||
_phantom: std::marker::PhantomData<Id>,
|
||||
}
|
||||
|
||||
impl<Id> InputIngredient<Id>
|
||||
where
|
||||
Id: InputId,
|
||||
{
|
||||
pub fn new(index: IngredientIndex) -> Self {
|
||||
Self {
|
||||
ingredient_index: index,
|
||||
counter: Default::default(),
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn database_key_index(&self, id: Id) -> DatabaseKeyIndex {
|
||||
DatabaseKeyIndex {
|
||||
ingredient_index: self.ingredient_index,
|
||||
key_index: id.as_id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_input(&mut self, _runtime: &mut Runtime) -> Id {
|
||||
let next_id = self.counter;
|
||||
self.counter += 1;
|
||||
Id::from_id(crate::Id::from(next_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB: ?Sized, Id> Ingredient<DB> for InputIngredient<Id>
|
||||
where
|
||||
Id: InputId,
|
||||
{
|
||||
fn maybe_changed_after(&self, _db: &DB, _input: DependencyIndex, _revision: Revision) -> bool {
|
||||
// Input ingredients are just a counter, they store no data, they are immortal.
|
||||
// Their *fields* are stored in function ingredients elsewhere.
|
||||
false
|
||||
}
|
||||
|
||||
fn cycle_recovery_strategy(&self) -> CycleRecoveryStrategy {
|
||||
CycleRecoveryStrategy::Panic
|
||||
}
|
||||
|
||||
fn inputs(&self, _key_index: crate::Id) -> Option<QueryInputs> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam::queue::SegQueue;
|
||||
use std::hash::Hash;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::durability::Durability;
|
||||
use crate::id::AsId;
|
||||
|
@ -21,15 +22,33 @@ impl<T: Eq + Hash + Clone> InternedData for T {}
|
|||
|
||||
#[allow(dead_code)]
|
||||
pub struct InternedIngredient<Id: InternedId, Data: InternedData> {
|
||||
/// Index of this ingredient in the database (used to construct database-ids, etc).
|
||||
ingredient_index: IngredientIndex,
|
||||
|
||||
// Deadlock requirement:
|
||||
//
|
||||
// We access `key_map` while holding lock on `value_map`, but not vice versa.
|
||||
/// Maps from data to the existing interned id for that data.
|
||||
///
|
||||
/// Deadlock requirement: We access `key_map` while holding lock on `value_map`, but not vice versa.
|
||||
key_map: FxDashMap<Data, Id>,
|
||||
|
||||
/// Maps from an interned id to its data.
|
||||
///
|
||||
/// Deadlock requirement: We access `key_map` while holding lock on `value_map`, but not vice versa.
|
||||
value_map: FxDashMap<Id, Box<Data>>,
|
||||
|
||||
/// counter for the next id.
|
||||
counter: AtomicCell<u32>,
|
||||
|
||||
/// Stores the revision when this interned ingredient was last cleared.
|
||||
/// You can clear an interned table at any point, deleting all its entries,
|
||||
/// but that will make anything dependent on those entries dirty and in need
|
||||
/// of being recomputed.
|
||||
reset_at: Revision,
|
||||
|
||||
/// When specific entries are deleted from the interned table, their data is added
|
||||
/// to this vector rather than being immediately freed. This is because we may` have
|
||||
/// references to that data floating about that are tied to the lifetime of some
|
||||
/// `&db` reference. This queue itself is not freed until we have an `&mut db` reference,
|
||||
/// guaranteeing that there are no more references to it.
|
||||
deleted_entries: SegQueue<Box<Data>>,
|
||||
}
|
||||
|
||||
|
@ -180,3 +199,21 @@ where
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdentityInterner<Id: AsId> {
|
||||
data: PhantomData<Id>,
|
||||
}
|
||||
|
||||
impl<Id: AsId> IdentityInterner<Id> {
|
||||
pub fn new() -> Self {
|
||||
IdentityInterner { data: PhantomData }
|
||||
}
|
||||
|
||||
pub fn intern(&self, _runtime: &Runtime, id: Id) -> Id {
|
||||
id
|
||||
}
|
||||
|
||||
pub fn data(&self, _runtime: &Runtime, id: Id) -> (Id,) {
|
||||
(id,)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ pub mod cycle;
|
|||
pub mod database;
|
||||
pub mod debug;
|
||||
pub mod durability;
|
||||
#[doc(hidden)]
|
||||
pub mod entity;
|
||||
pub mod event;
|
||||
pub mod function;
|
||||
pub mod hash;
|
||||
|
@ -20,6 +18,8 @@ pub mod revision;
|
|||
pub mod routes;
|
||||
pub mod runtime;
|
||||
pub mod storage;
|
||||
#[doc(hidden)]
|
||||
pub mod tracked_struct;
|
||||
|
||||
pub use self::cancelled::Cancelled;
|
||||
pub use self::cycle::Cycle;
|
||||
|
@ -29,8 +29,6 @@ pub use self::database::Snapshot;
|
|||
pub use self::debug::DebugWith;
|
||||
pub use self::debug::DebugWithDb;
|
||||
pub use self::durability::Durability;
|
||||
pub use self::entity::EntityData;
|
||||
pub use self::entity::EntityId;
|
||||
pub use self::event::Event;
|
||||
pub use self::event::EventKind;
|
||||
pub use self::id::AsId;
|
||||
|
@ -41,10 +39,11 @@ pub use self::routes::IngredientIndex;
|
|||
pub use self::runtime::Runtime;
|
||||
pub use self::storage::DbWithJar;
|
||||
pub use self::storage::Storage;
|
||||
pub use self::tracked_struct::TrackedStructData;
|
||||
pub use self::tracked_struct::TrackedStructId;
|
||||
pub use salsa_entity_macros::accumulator;
|
||||
pub use salsa_entity_macros::component;
|
||||
pub use salsa_entity_macros::db;
|
||||
pub use salsa_entity_macros::entity;
|
||||
pub use salsa_entity_macros::input;
|
||||
pub use salsa_entity_macros::interned;
|
||||
pub use salsa_entity_macros::jar;
|
||||
pub use salsa_entity_macros::memoized;
|
||||
pub use salsa_entity_macros::tracked;
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
|
||||
use self::{dependency_graph::DependencyGraph, local_state::ActiveQueryGuard};
|
||||
|
||||
use super::{entity::Disambiguator, IngredientIndex};
|
||||
use super::{tracked_struct::Disambiguator, IngredientIndex};
|
||||
|
||||
mod active_query;
|
||||
mod dependency_graph;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::{
|
||||
durability::Durability,
|
||||
entity::Disambiguator,
|
||||
hash::{FxHashSet, FxIndexMap, FxIndexSet},
|
||||
key::{DatabaseKeyIndex, DependencyIndex},
|
||||
tracked_struct::Disambiguator,
|
||||
Cycle, Revision, Runtime,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use log::debug;
|
||||
|
||||
use crate::durability::Durability;
|
||||
use crate::entity::Disambiguator;
|
||||
use crate::key::DatabaseKeyIndex;
|
||||
use crate::key::DependencyIndex;
|
||||
use crate::runtime::Revision;
|
||||
use crate::tracked_struct::Disambiguator;
|
||||
use crate::Cycle;
|
||||
use crate::Runtime;
|
||||
use std::cell::RefCell;
|
||||
|
|
|
@ -7,27 +7,27 @@ use crate::{
|
|||
Database, IngredientIndex, Revision,
|
||||
};
|
||||
|
||||
pub trait EntityId: InternedId {}
|
||||
impl<T: InternedId> EntityId for T {}
|
||||
pub trait TrackedStructId: InternedId {}
|
||||
impl<T: InternedId> TrackedStructId for T {}
|
||||
|
||||
pub trait EntityData: InternedData {}
|
||||
impl<T: InternedData> EntityData for T {}
|
||||
pub trait TrackedStructData: InternedData {}
|
||||
impl<T: InternedData> TrackedStructData for T {}
|
||||
|
||||
pub trait EntityInDb<DB: ?Sized + Database> {
|
||||
pub trait TrackedStructInDb<DB: ?Sized + Database> {
|
||||
fn database_key_index(self, db: &DB) -> DatabaseKeyIndex;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct EntityIngredient<Id, Data>
|
||||
pub struct TrackedStructIngredient<Id, Data>
|
||||
where
|
||||
Id: EntityId,
|
||||
Data: EntityData,
|
||||
Id: TrackedStructId,
|
||||
Data: TrackedStructData,
|
||||
{
|
||||
interned: InternedIngredient<Id, EntityKey<Data>>,
|
||||
interned: InternedIngredient<Id, TrackedStructKey<Data>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Copy, Clone)]
|
||||
struct EntityKey<Data> {
|
||||
struct TrackedStructKey<Data> {
|
||||
query_key: Option<DatabaseKeyIndex>,
|
||||
disambiguator: Disambiguator,
|
||||
data: Data,
|
||||
|
@ -36,10 +36,10 @@ struct EntityKey<Data> {
|
|||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Copy, Clone)]
|
||||
pub struct Disambiguator(pub u32);
|
||||
|
||||
impl<Id, Data> EntityIngredient<Id, Data>
|
||||
impl<Id, Data> TrackedStructIngredient<Id, Data>
|
||||
where
|
||||
Id: EntityId,
|
||||
Data: EntityData,
|
||||
Id: TrackedStructId,
|
||||
Data: TrackedStructData,
|
||||
{
|
||||
pub fn new(index: IngredientIndex) -> Self {
|
||||
Self {
|
||||
|
@ -54,14 +54,14 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_entity(&self, runtime: &Runtime, data: Data) -> Id {
|
||||
pub fn new_struct(&self, runtime: &Runtime, data: Data) -> Id {
|
||||
let data_hash = crate::hash::hash(&data);
|
||||
let (query_key, disambiguator) = runtime.disambiguate_entity(
|
||||
self.interned.ingredient_index(),
|
||||
self.interned.reset_at(),
|
||||
data_hash,
|
||||
);
|
||||
let entity_key = EntityKey {
|
||||
let entity_key = TrackedStructKey {
|
||||
query_key: Some(query_key),
|
||||
disambiguator,
|
||||
data,
|
||||
|
@ -71,7 +71,7 @@ where
|
|||
result
|
||||
}
|
||||
|
||||
pub fn entity_data<'db>(&'db self, runtime: &'db Runtime, id: Id) -> &'db Data {
|
||||
pub fn tracked_struct_data<'db>(&'db self, runtime: &'db Runtime, id: Id) -> &'db Data {
|
||||
&self.interned.data(runtime, id).data
|
||||
}
|
||||
|
||||
|
@ -92,10 +92,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<DB: ?Sized, Id, Data> Ingredient<DB> for EntityIngredient<Id, Data>
|
||||
impl<DB: ?Sized, Id, Data> Ingredient<DB> for TrackedStructIngredient<Id, Data>
|
||||
where
|
||||
Id: EntityId,
|
||||
Data: EntityData,
|
||||
Id: TrackedStructId,
|
||||
Data: TrackedStructData,
|
||||
{
|
||||
fn maybe_changed_after(&self, db: &DB, input: DependencyIndex, revision: Revision) -> bool {
|
||||
self.interned.maybe_changed_after(db, input, revision)
|
||||
|
@ -110,10 +110,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<DB: ?Sized, Id, Data> MutIngredient<DB> for EntityIngredient<Id, Data>
|
||||
impl<DB: ?Sized, Id, Data> MutIngredient<DB> for TrackedStructIngredient<Id, Data>
|
||||
where
|
||||
Id: EntityId,
|
||||
Data: EntityData,
|
||||
Id: TrackedStructId,
|
||||
Data: TrackedStructData,
|
||||
{
|
||||
fn reset_for_new_revision(&mut self) {
|
||||
self.interned.clear_deleted_indices();
|
16
salsa-2022-tests/Cargo.toml
Normal file
16
salsa-2022-tests/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "salsa-2022-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
salsa = { path = "../components/salsa-entity-mock", package = "salsa-entity-mock" }
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test = "1.4.0"
|
||||
|
||||
[[bin]]
|
||||
name = "salsa-2022-tests"
|
||||
path = "main.rs"
|
8
salsa-2022-tests/main.rs
Normal file
8
salsa-2022-tests/main.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
//! This crate has the beginning of various unit tests on salsa 2022
|
||||
//! code.
|
||||
|
||||
mod tracked_fn_on_input;
|
||||
mod tracked_fn_on_tracked;
|
||||
mod tracked_fn_on_tracked_specify;
|
||||
|
||||
fn main() {}
|
38
salsa-2022-tests/tracked_fn_on_input.rs
Normal file
38
salsa-2022-tests/tracked_fn_on_input.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
//! Test that a `tracked` fn on a `salsa::input`
|
||||
//! compiles and executes successfully.
|
||||
|
||||
#[salsa::jar(db = Db)]
|
||||
struct Jar(MyInput, tracked_fn);
|
||||
|
||||
trait Db: salsa::DbWithJar<Jar> {}
|
||||
|
||||
#[salsa::input(jar = Jar)]
|
||||
struct MyInput {
|
||||
field: u32,
|
||||
}
|
||||
|
||||
#[salsa::tracked(jar = Jar)]
|
||||
fn tracked_fn(db: &dyn Db, input: MyInput) -> u32 {
|
||||
input.field(db) * 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute() {
|
||||
#[salsa::db(Jar)]
|
||||
#[derive(Default)]
|
||||
struct Database {
|
||||
storage: salsa::Storage<Self>,
|
||||
}
|
||||
|
||||
impl salsa::Database for Database {
|
||||
fn salsa_runtime(&self) -> &salsa::Runtime {
|
||||
self.storage.runtime()
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for Database {}
|
||||
|
||||
let mut db = Database::default();
|
||||
let input = MyInput::new(&mut db, 22);
|
||||
assert_eq!(tracked_fn(&db, input), 44);
|
||||
}
|
43
salsa-2022-tests/tracked_fn_on_tracked.rs
Normal file
43
salsa-2022-tests/tracked_fn_on_tracked.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
//! Test that a `tracked` fn on a `salsa::input`
|
||||
//! compiles and executes successfully.
|
||||
|
||||
#[salsa::jar(db = Db)]
|
||||
struct Jar(MyInput, MyTracked, tracked_fn);
|
||||
|
||||
trait Db: salsa::DbWithJar<Jar> {}
|
||||
|
||||
#[salsa::input(jar = Jar)]
|
||||
struct MyInput {
|
||||
field: u32,
|
||||
}
|
||||
|
||||
#[salsa::tracked(jar = Jar)]
|
||||
struct MyTracked {
|
||||
field: u32,
|
||||
}
|
||||
|
||||
#[salsa::tracked(jar = Jar)]
|
||||
fn tracked_fn(db: &dyn Db, input: MyInput) -> MyTracked {
|
||||
MyTracked::new(db, input.field(db) * 2)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute() {
|
||||
#[salsa::db(Jar)]
|
||||
#[derive(Default)]
|
||||
struct Database {
|
||||
storage: salsa::Storage<Self>,
|
||||
}
|
||||
|
||||
impl salsa::Database for Database {
|
||||
fn salsa_runtime(&self) -> &salsa::Runtime {
|
||||
self.storage.runtime()
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for Database {}
|
||||
|
||||
let mut db = Database::default();
|
||||
let input = MyInput::new(&mut db, 22);
|
||||
assert_eq!(tracked_fn(&db, input).field(&db), 44);
|
||||
}
|
63
salsa-2022-tests/tracked_fn_on_tracked_specify.rs
Normal file
63
salsa-2022-tests/tracked_fn_on_tracked_specify.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
//! Test that a `tracked` fn on a `salsa::input`
|
||||
//! compiles and executes successfully.
|
||||
|
||||
#[salsa::jar(db = Db)]
|
||||
struct Jar(MyInput, MyTracked, tracked_fn, tracked_fn_extra);
|
||||
|
||||
trait Db: salsa::DbWithJar<Jar> {}
|
||||
|
||||
#[salsa::input(jar = Jar)]
|
||||
struct MyInput {
|
||||
field: u32,
|
||||
}
|
||||
|
||||
#[salsa::tracked(jar = Jar)]
|
||||
struct MyTracked {
|
||||
field: u32,
|
||||
}
|
||||
|
||||
#[salsa::tracked(jar = Jar)]
|
||||
fn tracked_fn(db: &dyn Db, input: MyInput) -> MyTracked {
|
||||
let t = MyTracked::new(db, input.field(db) * 2);
|
||||
if input.field(db) != 0 {
|
||||
tracked_fn_extra::specify(db, t, 2222);
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
#[salsa::tracked(jar = Jar, specify)]
|
||||
fn tracked_fn_extra(_db: &dyn Db, _input: MyTracked) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[salsa::db(Jar)]
|
||||
#[derive(Default)]
|
||||
struct Database {
|
||||
storage: salsa::Storage<Self>,
|
||||
}
|
||||
|
||||
impl salsa::Database for Database {
|
||||
fn salsa_runtime(&self) -> &salsa::Runtime {
|
||||
self.storage.runtime()
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for Database {}
|
||||
|
||||
#[test]
|
||||
fn execute_when_specified() {
|
||||
let mut db = Database::default();
|
||||
let input = MyInput::new(&mut db, 22);
|
||||
let tracked = tracked_fn(&db, input);
|
||||
assert_eq!(tracked.field(&db), 44);
|
||||
assert_eq!(tracked_fn_extra(&db, tracked), 2222);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_when_not_specified() {
|
||||
let mut db = Database::default();
|
||||
let input = MyInput::new(&mut db, 0);
|
||||
let tracked = tracked_fn(&db, input);
|
||||
assert_eq!(tracked.field(&db), 0);
|
||||
assert_eq!(tracked_fn_extra(&db, tracked), 0);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue