#rust #vortex
In [Vortex](https://github.com/vortex-data/vortex), we have a type hierarchy that looks something like this:
- `DType`
- `Struct(Vec<DType>)`
- `Primitive(PType)`
- where `PType` is broken down into
- Signed Int
- `I8`, `I16`, `I32`, `I64`
- Unsigned Int
- `U8`, `U16`, `U32`, `U64`
- Float
- `F16`, `F32`, `F64`
- `Utf8`
- ...
You get the picture. Pretty often with the primitive types, we'll get a `PrimitiveArray` and we don't know exactly what's inside of it. To make it easier to work with a block of data generically, we have a set of higher-order declarative macros which let us expand and take action generically, substituting the type parameter in where necessary:
```rust
let range: usize = match_each_integer_ptype!(array.ptype(), |P| {
// Use type parameter P to read the array's value and cast it
// to a buffer of `P`'s
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
})
```
Note that the macro actually accepts a closure-like syntax where the first argument is not a value, but a type-parameter that is bound in the inner function.
Here's how we write the macro:
```rust
macro_rules! match_each_integer_ptype {
($self:expr, | $tname:ident | $body:block) => {{
use $crate::PType;
match $self {
PType::I8 => {
type $tname = i8;
$body
}
PType::I16 => {
type $tname = i16;
$body
}
PType::I32 => {
type $tname = i32;
$body
}
PType::I64 => {
type $tname = i64;
$body
}
PType::U8 => {
type $tname = u8;
$body
}
PType::U16 => {
type $tname = u16;
$body
}
PType::U32 => {
type $tname = u32;
$body
}
PType::U64 => {
type $tname = u64;
$body
}
other => panic!("Unsupported ptype {other}", other = other),
}
}};
}
```
What's going on here? Let's make like an LLM and think step-by-step:
```rust
($self:expr, | $tname:ident | $body:block)
```
This block sets up the binder for the closure-like syntax from the example above. The first parameter to the macro is some `PType` value, which is the reified runtime enum that describes all of Vortex's primitive types.
The next token (after whitespace) must be the pipe `|` character, followed by some identifier we'll bind to `$tname`, followed by another `|`.
The next item must be a block, i.e. a valid code block bound by a pair of curly braces `{}`.
```rust
match $self {
```
Sets up the match, so we can switch our logic and bind to each possible `PType` variant.
```rust
PType::I8 => {
type $tname = i8;
$body
}
```
This is the juicy part. Ok so what's happening?
Inside of each match arm, we create a _new scope_. Inside of the scope, we bind a new type alias `$tname = i8`. This is because inside of the `I8` match arm, we want to treat whatever is behind our variable as an `i8`. We then insert the block. The block should be referencing the type parameter that we called `$tname` inside of the closure-style `|` pipes.
So if we put it all together, we can see that our original example gets expanded to this larger block:
```rust
use ::vortex_dtype::PType;
match (array.ptype()) {
PType::I8 => {
type P = i8;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::I16 => {
type P = i16;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::I32 => {
type P = i32;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::I64 => {
type P = i64;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::U8 => {
type P = u8;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::U16 => {
type P = u16;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::U32 => {
type P = u32;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
PType::U64 => {
type P = u64;
{
let xs = array.buffer::<P>();
let max: P = xs.iter().copied().min().unwrap_or_default();
let min: P = xs.iter().copied().max().unwrap_or_default();
(max - min) as usize
}
}
other => panic!("Unsupported ptype {other}", other = other),
}
}
```