mirror of
https://github.com/salsa-rs/salsa.git
synced 2025-01-12 08:30:51 +00:00
improve docs
This commit is contained in:
parent
2ddc8032ee
commit
0a2a871d98
4 changed files with 305 additions and 64 deletions
32
FAQ.md
Normal file
32
FAQ.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Frequently asked questions
|
||||||
|
|
||||||
|
## Why is it called salsa?
|
||||||
|
|
||||||
|
I like salsa! Don't you?! Well, ok, there's a bit more to it. The
|
||||||
|
underlying algorithm for figuring out which bits of code need to be
|
||||||
|
re-executed after any given change is based on the algorithm used in
|
||||||
|
rustc. Michael Woerister and I first described the rustc algorithm in
|
||||||
|
terms of two colors, red and green, and hence we called it the
|
||||||
|
"red-green algorithm". This made me think of the New Mexico State
|
||||||
|
Question --- "Red or green?" --- which refers to salsa. Although this
|
||||||
|
version no longer uses colors (we borrowed revision counters from
|
||||||
|
Glimmer, instead), I still like the name.
|
||||||
|
|
||||||
|
## What is the relationship between salsa and an Entity-Component System (ECS)?
|
||||||
|
|
||||||
|
You may have noticed that Salsa "feels" a lot like an ECS in some
|
||||||
|
ways. That's true -- Salsa's queries are a bit like *components* (and
|
||||||
|
the keys to the queries are a bit like *entitites*). But there is one
|
||||||
|
big difference: **ECS is -- at its heart -- a mutable system**. You
|
||||||
|
can get or set a component of some entity whenever you like. In
|
||||||
|
contrast, salsa's queries define **define "derived values" via pure
|
||||||
|
computations**.
|
||||||
|
|
||||||
|
Partly as a consequence, ECS doesn't handle incremental updates for
|
||||||
|
you. When you update some component of some entity, you have to ensure
|
||||||
|
that other entities' components are upated appropriately.
|
||||||
|
|
||||||
|
Finally, ECS offers interesting metadata and "aspect-like" facilities,
|
||||||
|
such as iterating over all entities that share certain components.
|
||||||
|
Salsa has no analogue to that.
|
||||||
|
|
80
README.md
80
README.md
|
@ -15,15 +15,16 @@ Yehuda Katz, and Michael Woerister.
|
||||||
|
|
||||||
## Key idea
|
## Key idea
|
||||||
|
|
||||||
The key idea of `salsa` is that you define two things:
|
The key idea of `salsa` is that you define your program as a set
|
||||||
|
of **queries**. Queries come in two basic varieties:
|
||||||
|
|
||||||
- **Inputs**: the base inputs to your system. You can change these
|
- **Inputs**: the base inputs to your system. You can change these
|
||||||
whenever you like.
|
whenever you like.
|
||||||
- **Queries**: values derived from those inputs. These are defined via
|
- **Functions**: pure functions (no side effects) that transform your
|
||||||
"pure functions" (no side effects). The results of queries can be
|
inputs into other values. The results of queries is memoized to
|
||||||
memoized to avoid recomputing them a lot. When you make changes to
|
avoid recomputing them a lot. When you make changes to the inputs,
|
||||||
the inputs, we'll figure out (fairly intelligently) when we can
|
we'll figure out (fairly intelligently) when we can re-use these
|
||||||
re-use these memoized values and when we have to recompute them.
|
memoized values and when we have to recompute them.
|
||||||
|
|
||||||
## How to use Salsa in three easy steps
|
## How to use Salsa in three easy steps
|
||||||
|
|
||||||
|
@ -34,63 +35,14 @@ Using salsa is as easy as 1, 2, 3...
|
||||||
later on you can use more than one to break up your system into
|
later on you can use more than one to break up your system into
|
||||||
components (or spread your code across crates).
|
components (or spread your code across crates).
|
||||||
2. **Implement the queries** using the `query_definition!` macro.
|
2. **Implement the queries** using the `query_definition!` macro.
|
||||||
3. Create the **query context implementation**, which contains a full
|
3. **Implement the query context trait** for your query context
|
||||||
listing of all the inputs/queries you will be using. The query
|
struct, which contains a full listing of all the inputs/queries you
|
||||||
content implementation will contain the storage for all of the
|
will be using. The query struct will contain the storage for all of
|
||||||
inputs/queries and may also contain anything else that your code
|
the inputs/queries and may also contain anything else that your
|
||||||
needs (e.g., configuration data).
|
code needs (e.g., configuration data).
|
||||||
|
|
||||||
Let's walk through an example! This is [the `hello_world`
|
To see an example of this in action, check out [the `hello_world`
|
||||||
example](examples/hello_world) from the repository.
|
example](examples/hello_world/main.rs), which has a number of comments
|
||||||
|
explaining how things work. The [`hello_world`
|
||||||
|
README](examples/hello_world/README.md) has a more detailed writeup.
|
||||||
|
|
||||||
### Step 1: Define a query context trait
|
|
||||||
|
|
||||||
The "query context" is the central struct that holds all the state for
|
|
||||||
your application. It has the current values of all your inputs, the
|
|
||||||
values of any memoized queries you have executed thus far, and
|
|
||||||
dependency information between them.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub trait HelloWorldContext: salsa::QueryContext {
|
|
||||||
salsa::query_prototype! {
|
|
||||||
/// The fundamental **input** to the system: contains a
|
|
||||||
/// complete list of files.
|
|
||||||
fn all_files() for AllFiles;
|
|
||||||
|
|
||||||
/// A **derived value**: filtered list of paths representing
|
|
||||||
/// jpegs.
|
|
||||||
fn jpegs() for Jpegs;
|
|
||||||
|
|
||||||
/// A **derived value**: the size of the biggest image. To
|
|
||||||
/// avoid doing actual image manipulating, we'll use the silly
|
|
||||||
/// metric of the longest file name. =)
|
|
||||||
fn largest() for Largest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
Let's make a very simple, hello-world sort of example. We'll make two inputs,
|
|
||||||
each of whihc is
|
|
||||||
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
It tries to hit a few goals:
|
|
||||||
|
|
||||||
- No need for a base crate that declares the "complete set of queries"
|
|
||||||
- Each query can define its own storage and doesn't have to be memoized
|
|
||||||
- Each module only has to know about the queries that it depends on
|
|
||||||
and that it provides (but no others)
|
|
||||||
- Compiles to fast code, with no allocation, dynamic dispatch, etc on
|
|
||||||
the "memoized hit" fast path
|
|
||||||
- Can recover from cycles gracefully (though I didn't really show
|
|
||||||
that)
|
|
||||||
- Should support arenas and other lifetime-based things without requiring
|
|
||||||
lifetimes everywhere when you're not using them (untested)
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
There is a working `hello_world` example which is probably the best documentation.
|
|
||||||
More to come when I expand out a few more patterns.
|
|
||||||
|
|
172
examples/hello_world/README.md
Normal file
172
examples/hello_world/README.md
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
The `hello_world` example is intended to walk through the very basics
|
||||||
|
of a salsa setup. Here is a more detailed writeup.
|
||||||
|
|
||||||
|
### Step 1: Define the query context trait
|
||||||
|
|
||||||
|
The **query context** is the central struct that holds all the state
|
||||||
|
for your application. It has the current values of all your inputs,
|
||||||
|
the values of any memoized queries you have executed thus far, and
|
||||||
|
dependency information between them.
|
||||||
|
|
||||||
|
In your program, however, you rarely interact with the **actual**
|
||||||
|
query context struct. Instead, you interact with query context
|
||||||
|
**traits** that you define. These traits define the set of queries
|
||||||
|
that you need for any given piece of code. You define them using
|
||||||
|
the `salsa::query_prototype!` macro.
|
||||||
|
|
||||||
|
Here is a simple example of a query context trait from the
|
||||||
|
`hello_world` example. It defines exactly two queries: `input_string`
|
||||||
|
and `length`. You see that the `query_prototype!` macro just lists out
|
||||||
|
the names of the queries as methods (e.g., `input_string()`) and also a
|
||||||
|
path to a type that will define the query (`InputString`). It doesn't
|
||||||
|
give many other details: those are specified in the query definition
|
||||||
|
that comes later.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
salsa::query_prototype! {
|
||||||
|
trait HelloWorldContext: salsa::QueryContext {
|
||||||
|
fn input_string() for InputString;
|
||||||
|
fn length() for Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Define the queries
|
||||||
|
|
||||||
|
The actual query definitions are made using the
|
||||||
|
`salsa::query_definition` macro. For an **input query**, such as
|
||||||
|
`input_string`, these resemble a variable definition:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
salsa::query_definition! {
|
||||||
|
InputString: Map<(), Arc<String>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the `Map` is actually a keyword -- you have to write it. The
|
||||||
|
idea is that each query isn't defining a single value: they are always
|
||||||
|
a mapping from some **key** to some **value** -- in this case, though,
|
||||||
|
the type of the key is just the unit type `()` (so in a sense this
|
||||||
|
*is* a single value). The value type would be `Arc<String>`.
|
||||||
|
|
||||||
|
Note that both keys and values are cloned with relative frequency, so
|
||||||
|
it's a good idea to pick types that can be cheaply cloned. Also, for
|
||||||
|
the incremental system to work, keys and value types must not employ
|
||||||
|
"interior mutability" (no `Mutex` or `AtomicUsize` etc).
|
||||||
|
|
||||||
|
Next let's define the `length` query, which is a function query:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
salsa::query_definition! {
|
||||||
|
Length(context: &impl HelloWorldContext, _key: ()) -> usize {
|
||||||
|
// Read the input string:
|
||||||
|
let input_string = context.input_string().get(());
|
||||||
|
|
||||||
|
// Return its length:
|
||||||
|
input_string.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Like the `InputString` query, `Length` has a **key** and a **value**
|
||||||
|
-- but this time the type of the key is specified as the type of the
|
||||||
|
second argument (`_key`), and the type of the value is specified from
|
||||||
|
the return type (`usize`).
|
||||||
|
|
||||||
|
You can also see that functions take a first argument, the `context`,
|
||||||
|
which always has the form `&impl <SomeContextTrait>`. This `context`
|
||||||
|
value gives access to all the other queries that are listed in the
|
||||||
|
context trait that you specify.
|
||||||
|
|
||||||
|
In the first line of the function we see how we invoke a query:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let input_string = context.input_string().get(());
|
||||||
|
```
|
||||||
|
|
||||||
|
When you invoke `context.input_string()`, what you get back is called
|
||||||
|
a `QueryTable` -- it offers a few methods that let you interact with
|
||||||
|
the query. The main method you will use though is `get(key)` which --
|
||||||
|
given a `key` -- computes and returns the up-to-date value. In the
|
||||||
|
case of an input query like `input_string`, this just returns whatever
|
||||||
|
value has been set by the user (if no value has been set yet, it
|
||||||
|
returns the `Default::default()` value; all query inputs must
|
||||||
|
implement `Default`).
|
||||||
|
|
||||||
|
### Step 3: Implement the query context trait
|
||||||
|
|
||||||
|
The final step is to create the **query context struct** which will
|
||||||
|
implement your query context trait(s). This struct combines all the
|
||||||
|
parts of your system into one whole; it can also add custom state of
|
||||||
|
your own (such as an interner or configuration). In our simple example
|
||||||
|
though we won't do any of that. The only field that you **actually**
|
||||||
|
need is a reference to the **salsa runtime**; then you must also
|
||||||
|
implement the `QueryContext` trait to tell salsa where to find this
|
||||||
|
runtime:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Default)]
|
||||||
|
struct QueryContextStruct {
|
||||||
|
runtime: salsa::runtime::Runtime<QueryContextStruct>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl salsa::QueryContext for QueryContextImpl {
|
||||||
|
fn salsa_runtime(&self) -> &salsa::runtime::Runtime<QueryContextStruct> {
|
||||||
|
&self.runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you must use the `query_context_storage!` to define the "storage
|
||||||
|
struct" for your type. This storage struct contains all the hashmaps
|
||||||
|
and other things that salsa uses to store the values for your
|
||||||
|
queries. You won't need to interact with it directly. To use the
|
||||||
|
macro, you basically list out all the traits and each of the queries
|
||||||
|
within those traits:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
query_context_storage! {
|
||||||
|
struct QueryContextStorage for QueryContextStruct {
|
||||||
|
// ^^^^^^^^^^^^^^^^^^^ ------------------
|
||||||
|
// name of the type the name of your context type
|
||||||
|
// we will make
|
||||||
|
impl HelloWorldContext {
|
||||||
|
fn input_string() for InputString;
|
||||||
|
fn length() for Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `query_context_storage` macro will also implement the
|
||||||
|
`HelloWorldContext` trait for your query context type.
|
||||||
|
|
||||||
|
Now that we've defined our query context, we can start using it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
let context = QueryContextStruct::default();
|
||||||
|
|
||||||
|
println!("Initially, the length is {}.", context.length().get(()));
|
||||||
|
|
||||||
|
context
|
||||||
|
.input_string()
|
||||||
|
.set((), Arc::new(format!("Hello, world")));
|
||||||
|
|
||||||
|
println!("Now, the length is {}.", context.length().get(()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And if we run this code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> cargo run --example hello_world
|
||||||
|
Compiling salsa v0.2.0 (/Users/nmatsakis/versioned/salsa)
|
||||||
|
Finished dev [unoptimized + debuginfo] target(s) in 0.94s
|
||||||
|
Running `target/debug/examples/hello_world`
|
||||||
|
Initially, the length is 0.
|
||||||
|
Now, the length is 12.
|
||||||
|
```
|
||||||
|
|
||||||
|
Amazing.
|
||||||
|
|
85
examples/hello_world/main.rs
Normal file
85
examples/hello_world/main.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Step 1. Define the query context trait
|
||||||
|
|
||||||
|
// Define a **query context trait** listing out all the prototypes
|
||||||
|
// that are defined in this section of the code (in real applications
|
||||||
|
// you would have many of these). For each query, we just give the
|
||||||
|
// name of the accessor method (`input_string`) and link that to a
|
||||||
|
// query type (`InputString`) that will be defined later.
|
||||||
|
salsa::query_prototype! {
|
||||||
|
trait HelloWorldContext: salsa::QueryContext {
|
||||||
|
fn input_string() for InputString;
|
||||||
|
fn length() for Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Step 2. Define the queries.
|
||||||
|
|
||||||
|
// Define an **input query**. Like all queries, it is a map from a key
|
||||||
|
// (of type `()`) to a value (of type `Arc<String>`). All values begin
|
||||||
|
// as `Default::default` but you can assign them new values.
|
||||||
|
salsa::query_definition! {
|
||||||
|
InputString: Map<(), Arc<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a **function query**. It too has a key and value type, but
|
||||||
|
// it is defined with a function that -- given the key -- computes the
|
||||||
|
// value. This function is supplied with a context (an `&impl
|
||||||
|
// HelloWorldContext`) that gives access to other queries. The runtime
|
||||||
|
// will track which queries you use so that we can incrementally
|
||||||
|
// update memoized results.
|
||||||
|
salsa::query_definition! {
|
||||||
|
Length(context: &impl HelloWorldContext, _key: ()) -> usize {
|
||||||
|
// Read the input string:
|
||||||
|
let input_string = context.input_string().get(());
|
||||||
|
|
||||||
|
// Return its length:
|
||||||
|
input_string.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Step 3. Implement the query context trait.
|
||||||
|
|
||||||
|
// Define the actual query context struct. This must contain a salsa
|
||||||
|
// runtime but can also contain anything else you need.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct QueryContextStruct {
|
||||||
|
runtime: salsa::runtime::Runtime<QueryContextStruct>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell salsa where to find the runtime in your context.
|
||||||
|
impl salsa::QueryContext for QueryContextStruct {
|
||||||
|
fn salsa_runtime(&self) -> &salsa::runtime::Runtime<QueryContextStruct> {
|
||||||
|
&self.runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the full set of queries that your context needs. This would
|
||||||
|
// in general combine (and implement) all the query context traits in
|
||||||
|
// your application into one place, allocating storage for all of
|
||||||
|
// them.
|
||||||
|
salsa::query_context_storage! {
|
||||||
|
pub struct QueryContextStorage for QueryContextStruct {
|
||||||
|
impl HelloWorldContext {
|
||||||
|
fn input_string() for InputString;
|
||||||
|
fn length() for Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shows how to use a query.
|
||||||
|
fn main() {
|
||||||
|
let context = QueryContextStruct::default();
|
||||||
|
|
||||||
|
println!("Initially, the length is {}.", context.length().get(()));
|
||||||
|
|
||||||
|
context
|
||||||
|
.input_string()
|
||||||
|
.set((), Arc::new(format!("Hello, world")));
|
||||||
|
|
||||||
|
println!("Now, the length is {}.", context.length().get(()));
|
||||||
|
}
|
Loading…
Reference in a new issue