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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions .scratch/value-walk/issues/01-walk.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ visitors over `BinStream::walk`, and the same visitor verifies a repair over `Bi

## Proposed surface

In `ltk_meta::walk`:

```rust
impl Kind {
/// The one question the walk asks of a `Kind`. Sealed: implemented for `Kind` only.
pub trait TreeKind: Copy + sealed::Sealed {
/// Whether a value of this kind is a node: `Struct` or `Embedded`.
pub fn is_node(self) -> bool;
fn is_node(self) -> bool;
}
```
impl TreeKind for Kind {}

In `ltk_meta::walk`:

```rust
/// A value the walk can cross. Sealed: implemented for `&'a PropertyValueEnum<M>` and for
/// `ValueView<'a, M>`, and by nothing else.
pub trait TreeValue<'a>: Copy + sealed::Sealed {
Expand All @@ -37,7 +37,7 @@ pub trait TreeValue<'a>: Copy + sealed::Sealed {

fn kind(&self) -> Kind;
/// Whether entering this value can reach a node: a `Struct` or `Embedded` whose class hash
/// is not 0, or a container, optional or map whose item kind [`Kind::is_node`].
/// is not 0, or a container, optional or map whose item kind [`TreeKind::is_node`].
fn holds_node(&self) -> Result<bool, Error>;
/// This value as a node, if it is a `Struct` or `Embedded` with a class hash that is not 0.
fn as_node(&self) -> Result<Option<Self::Node>, Error>;
Expand All @@ -52,7 +52,8 @@ pub trait TreeValue<'a>: Copy + sealed::Sealed {
}

/// A node the walk can visit: a class and properties. Sealed: implemented for the owned
/// tree's node, for `StructView<'a, M>`, and for `ObjectView<'a, M>` as a root.
/// tree's node and for `StructView<'a, M>`. An object's root is walked as a `StructView`
/// over the same bytes.
pub trait TreeNode<'a>: Copy + sealed::Sealed {
type Value: TreeValue<'a, Node = Self>;
type Properties: Iterator<Item = Result<(BinHash, Self::Value), Error>>;
Expand Down Expand Up @@ -97,7 +98,6 @@ impl<'a, M> TreeValue<'a> for &'a PropertyValueEnum<M> { type Node = OwnedNode<'
impl<'a, M: Default> TreeValue<'a> for ValueView<'a, M> { type Node = StructView<'a, M>; /* ... */ }
impl<'a, M> TreeNode<'a> for OwnedNode<'a, M> { type Value = &'a PropertyValueEnum<M>; /* ... */ }
impl<'a, M: Default> TreeNode<'a> for StructView<'a, M> { type Value = ValueView<'a, M>; /* ... */ }
impl<'a, M: Default> TreeNode<'a> for ObjectView<'a, M> { type Value = ValueView<'a, M>; /* ... */ }

/// What a callback answers. `ltk_ritobin::cst::visitor::Visit`'s shape (W21).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -132,7 +132,8 @@ pub trait Visitor<'a, V: TreeValue<'a>> {
/// descended on `Continue`.
fn enter_property(&mut self, field: BinHash, value: V, node: &Node<'_, 'a, V>)
-> Result<Visit, Self::Error> { Ok(Visit::Continue) }
/// Once per property descended. Not called for a leaf. Never after an `Abort`.
/// Once per property that holds a node and was entered. Not called for a leaf. Never
/// after an `Abort`.
fn exit_property(&mut self, field: BinHash, value: V, node: &Node<'_, 'a, V>)
-> Result<Visit, Self::Error> { Ok(Visit::Continue) }
}
Expand Down Expand Up @@ -208,14 +209,14 @@ impl<'a, M: Default> ObjectView<'a, M> {
}
impl<R: io::Read + io::Seek, M: Default> ObjectStream<'_, R, M> {
/// `view()?` then `walk`.
pub fn walk<W>(&mut self, visitor: &mut W) -> Result<WalkOutcome, W::Error>
where W: for<'a> Visitor<'a, ValueView<'a, M>>;
pub fn walk<E, W>(&mut self, visitor: &mut W) -> Result<WalkOutcome, E>
where E: From<Error>, W: for<'a> Visitor<'a, ValueView<'a, M>, Error = E>;
}
impl<R: io::Read + io::Seek, M: Default> BinStream<R, M> {
/// Walks every object in file order, one buffered object at a time. Holds one object's
/// bytes at any moment and nothing of the tree.
pub fn walk<W>(&mut self, visitor: &mut W) -> Result<WalkOutcome, W::Error>
where W: for<'a> Visitor<'a, ValueView<'a, M>>;
pub fn walk<E, W>(&mut self, visitor: &mut W) -> Result<WalkOutcome, E>
where E: From<Error>, W: for<'a> Visitor<'a, ValueView<'a, M>, Error = E>;
}
```

Expand Down Expand Up @@ -252,7 +253,7 @@ to decode and a visitor reading a leaf can too; the tree's errors convert throug
`Stop` is an outcome, not an error.

Blocked by #219: `Node::value_path`, `Trail::to_value_path` and `MapKey` are its types.
`Kind::is_node`, the traits, `Leaf`, `Visitor`, `Node`, `Trail` and every entry point do not
`TreeKind`, the tree traits, `Leaf`, `Visitor`, `Node`, `Trail` and every entry point do not
depend on it and can land first behind those methods.

- [ ] The fixture tree of `value-walk.md` [section 7](https://github.com/LeagueToolkit/league-toolkit/blob/main/docs/design/value-walk.md#s7) is walked twice, owned and as an `ObjectView` of its bytes, through one generic visitor, and a
Expand Down
78 changes: 78 additions & 0 deletions crates/ltk_meta/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,84 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

## Walking a bin

`ltk_meta::walk` is one traversal for both trees. A `Visitor` is called once per node, in pre-order and file order, and answers a `Visit` that continues, prunes a property, stops or aborts. The walk visits every node of an object once, in pre-order and file order, and asks the visitor before entering each property. The visitor is generic over the tree: the same `Census` runs over an owned `Bin` and over a `BinStream`, where nothing is materialised.

```rust
use ltk_hash::BinHash;
use ltk_meta::{
walk::{Node, TreeValue, Visit, Visitor},
Error,
};

/// Counts nodes and records the address of every `Struct` of one class.
#[derive(Default)]
struct Census {
nodes: usize,
hits: Vec<(BinHash, String)>,
}

impl<'a, V: TreeValue<'a>> Visitor<'a, V> for Census {
type Error = Error;

fn enter_node(&mut self, node: &Node<'_, 'a, V>) -> Result<Visit, Error> {
self.nodes += 1;
if *node.class_hash() == 0x1e6b_a0c4 {
self.hits.push((node.object_hash(), node.trail().to_string()));
}
Ok(Visit::Continue)
}
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
let bin = ltk_meta::Bin::from_reader(&mut std::fs::File::open("data.bin")?)?;
let mut census = Census::default();
bin.walk(&mut census)?;
println!("{} nodes, {} hits", census.nodes, census.hits.len());

let mut stream = ltk_meta::concrete::BinStream::mount(std::fs::File::open("data.bin")?)?;
let mut census = Census::default();
stream.walk(&mut census)?;
Ok(())
}
```

**In parallel.** The walk over one object is sequential by contract: one visitor, pre-order, `Stop` and `Skip` as ordered decisions. Objects are independent of one another, and every view, node and trail type is `Send`. A sweep parallelises across objects with one visitor instance per worker and a reduce at the end. Nothing in the crate schedules this; the split is the caller's.

```rust
let objects: Vec<_> = bin.objects.values().collect();
let workers = std::thread::available_parallelism().map_or(1, |n| n.get());
let per_worker = objects.len().div_ceil(workers).max(1);

let counted = std::thread::scope(|scope| {
let workers: Vec<_> = objects
.chunks(per_worker)
.map(|chunk| {
scope.spawn(move || {
let mut census = Census::default();
for object in chunk {
object.walk(&mut census)?;
}
Ok::<_, Error>(census)
})
})
.collect();

let mut all = Census::default();
for worker in workers {
let census = worker.join().expect("a worker panicked")?;
all.nodes += census.nodes;
all.hits.extend(census.hits);
}
Ok::<_, Error>(all)
})?;

println!("{} nodes, {} hits", counted.nodes, counted.hits.len());
```

Across many files the same shape applies one level up: one task per file, each mounting its own `BinStream` and walking it sequentially. The per-object walk is microseconds; decompression and I/O are where a sweep spends its time.

## Creating one programmatically

The `concrete` module pins the metadata parameter, which is what you want unless you are attaching per-node metadata of your own:
Expand Down
93 changes: 93 additions & 0 deletions crates/ltk_meta/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,97 @@ while let Some(mut object) = objects.next()? {
[`Bin::from_reader`] is itself `BinStream::mount` plus [`BinStream::into_bin`], so the eager
tree and the streaming surface are one parser and cannot drift.

### Walking a bin

[`walk`] is one traversal for both trees. A [`Visitor`](walk::Visitor) is called once per
node, in pre-order and file order, and answers a [`Visit`](walk::Visit) that continues, prunes
a property, stops or aborts. The same visitor runs over an owned [`Bin`] and over a
[`BinStream`], where nothing is materialised.

```
use ltk_hash::BinHash;
use ltk_meta::{
walk::{Node, TreeValue, Visit, Visitor},
Error,
};

/// Counts nodes and records the address of every `Struct` of one class.
#[derive(Default)]
struct Census {
nodes: usize,
hits: Vec<(BinHash, String)>,
}

impl<'a, V: TreeValue<'a>> Visitor<'a, V> for Census {
type Error = Error;

fn enter_node(&mut self, node: &Node<'_, 'a, V>) -> Result<Visit, Error> {
self.nodes += 1;
if *node.class_hash() == 0x1e6b_a0c4 {
self.hits.push((node.object_hash(), node.trail().to_string()));
}
Ok(Visit::Continue)
}
}

# let bin = ltk_meta::concrete::Bin::builder().build();
let mut census = Census::default();
bin.walk(&mut census)?;
# Ok::<(), Error>(())
```

The walk over one object is sequential by contract. Objects are independent, and every view,
node and trail type is `Send`. A sweep parallelises across objects: one visitor instance per
worker, reduced at the end. The split is the caller's; the crate schedules nothing.

```
# use ltk_hash::BinHash;
# use ltk_meta::{walk::{Node, TreeValue, Visit, Visitor}, Error};
# #[derive(Default)]
# struct Census { nodes: usize, hits: Vec<(BinHash, String)> }
# impl<'a, V: TreeValue<'a>> Visitor<'a, V> for Census {
# type Error = Error;
# fn enter_node(&mut self, node: &Node<'_, 'a, V>) -> Result<Visit, Error> {
# self.nodes += 1;
# Ok(Visit::Continue)
# }
# }
# let bin = ltk_meta::concrete::Bin::builder().build();
let objects: Vec<_> = bin.objects.values().collect();
let workers = std::thread::available_parallelism().map_or(1, |n| n.get());
let per_worker = objects.len().div_ceil(workers).max(1);

let counted = std::thread::scope(|scope| {
let workers: Vec<_> = objects
.chunks(per_worker)
.map(|chunk| {
scope.spawn(move || {
let mut census = Census::default();
for object in chunk {
object.walk(&mut census)?;
}
Ok::<_, Error>(census)
})
})
.collect();

let mut all = Census::default();
for worker in workers {
let census = worker.join().expect("a worker panicked")?;
all.nodes += census.nodes;
all.hits.extend(census.hits);
}
Ok::<_, Error>(all)
})?;

println!("{} nodes, {} hits", counted.nodes, counted.hits.len());
# Ok::<(), Error>(())
```

Across many files the same shape applies one level up: one task per file, each mounting its
own [`BinStream`] and walking it sequentially. The per-object walk is microseconds;
decompression and I/O are where a sweep spends its time.

### Modifying a bin file

```no_run
Expand Down Expand Up @@ -234,3 +325,5 @@ mod error;
pub use error::*;

pub mod traits;

pub mod walk;
5 changes: 5 additions & 0 deletions crates/ltk_meta/src/stream/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ impl<'a, M> ObjectView<'a, M> {
find_property(self.properties(), name_hash.into())
}

/// The object as a struct: its class hash and its properties, the path hash left behind.
pub(crate) fn as_struct(&self) -> StructView<'a, M> {
StructView::from_parts(self.class_hash, self.property_count, self.properties)
}

/// The object's raw bytes — its whole declared range, size field included.
///
/// This is the range a byte-exact copy of the object covers, which is what the delta
Expand Down
11 changes: 11 additions & 0 deletions crates/ltk_meta/src/stream/view/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,17 @@ impl<'a, M> StructView<'a, M> {
})
}

/// A view over `count` properties at `properties`, carrying `class_hash`. An object's
/// root as a struct.
pub(crate) fn from_parts(class_hash: BinHash, count: u16, properties: Cursor<'a>) -> Self {
Self {
class_hash,
property_count: count,
properties,
meta: PhantomData,
}
}

/// The class this is an instance of, or `0` for a null pointer.
#[must_use]
pub fn class_hash(&self) -> BinHash {
Expand Down
Loading
Loading