- Write clean, readable, and maintainable code
- Follow standard Rust idioms and best practices
- Prioritize safety and correctness
- Prefer zero-cost abstractions where possible
- Minimize use of
unsafeto cases where it's truly necessary (i.e., typically when the performance benefits are large)
- Use
rustfmtwith the project's custom configuration:- Condense wildcard suffixes
- Format macro matchers and bodies
- Group imports by StdExternalCrate
- Use field init shorthand
- Group imports at the module level
- Use 2024 edition style
- Run
cargo +nightly fmtbefore submitting code
- Every public API definition MUST have a doc comment
- Module-level documentation using
//!comments for context and purpose - Function-level documentation using
///comments - Examples in documentation are encouraged but not strictly required
- Use
#![deny(missing_docs)]in crates to enforce documentation standards
- Follow standard Rust naming conventions:
CamelCasefor types, traits, and enumssnake_casefor functions, methods, and variablesSCREAMING_SNAKE_CASEfor constants and statics
- Use descriptive names that clearly convey purpose
- Prefer explicit names over overly terse abbreviations
- Prefer strongly typed APIs when possible
- Use Rust's type system to prevent bugs at compile time
- Implement appropriate traits for custom types
- Prefer
impl AsRef<T>to&Tfor public APIs (e.g.impl AsRef<Path>) - Use type aliases to improve code readability and maintenance
- Use the custom
VortexErrortype for errors - Propagate errors using the
?operator - Use the following error macros consistently:
vortex_err!for creating errorsvortex_bail!for returning errorsvortex_panic!for handling invariant violations
- Add context to errors using
.with_context() - Include backtraces for better debugging
- Use
VortexExpecttrait when unwrapping is appropriate with proper error context.
- Maintain a clear separation between logical and physical types
- Keep functions focused and reasonably sized
- Separate public API from internal implementation details
- Prefer one public entrypoint for each piece of functionality; keep helper APIs crate-private unless callers need them independently.
- Use modules to organize related functionality
- Place tests in a
testsmodule or separate test files
- Avoid using
HashMapandHashSetfrom the standard library (prefer the alternatives invortex-array::aliases) - Prefer specialized collections when appropriate
- Be mindful of performance implications when choosing data structures
- Avoid
unsafecode unless strictly necessary for optimal performance - Document all uses of
unsafewith detailed safety comments - Encapsulate
unsafecode within safe abstractions
- Write comprehensive unit tests for new functionality
- Include integration tests for complex features
- Use property-based testing for appropriate scenarios
- Follow test naming conventions:
test_<function_name>_<scenario> - In tests only:
dbg!usage is allowedexpect()andunwrap()are acceptable- More relaxed clippy rules apply
- Be conservative with adding new dependencies
- Follow dependency management guidelines in
deny.toml - Prefer using crates from the workspace when possible
- Optimize for readability & performance (choose two)
- Use benchmarks to measure performance improvements
- Prefer algorithmic improvements over micro-optimizations
- Document performance-critical sections
Avoid Hidden-Cost Accessors in Hot Loops
Do not call a per-element accessor that hides non-trivial work inside an O(n) loop. Each call can
repay work that is constant or amortizable across the chunk, turning the loop into O(n * k).
Watch for these accessors inside for i in 0..n { ... }:
| Per-element accessor | Hidden cost | Bulk replacement |
|---|---|---|
Validity::is_valid(i) / is_null(i) |
Array-backed validity allocates an ExecutionCtx and runs a scalar lookup per call. |
Call validity.execute_mask(len, ctx)? once, then read the materialized mask. |
array.scalar_at(i) / array.execute_scalar(i, ctx) |
Executes through the compute stack per element. | Canonicalize once with execute::<PrimitiveArray> or as_slice, then index. |
BitBuffer::value(i) / Mask::value(i) accumulated into a count |
Recomputes the byte address and defeats popcount. | Use true_count(), BitBuffer::count_range(start, end), or set_indices(). |
BitIterator::next() accumulated into a rank or prefix count |
Processes one bit at a time. | Use count_range over each gap. |
Re-deriving a value such as self.validity()? |
Repeats the derivation for every element. | Hoist the derivation above the loop. |
Choose the replacement based on the access pattern:
- For sequential or contiguous access, materialize once and iterate or index the chunk.
- For a gather over arbitrary indices, materialize the backing buffer once, then use cheap random reads. The decode itself may not be amortizable.
- Leave genuinely
O(1)accessors alone. Bulk materialization does not help an already materialized mask, slice, or native bitmap.
After materializing a Mask or BitBuffer, avoid calling value(i) for every element merely to act
on set bits. Use BitBuffer::for_each_set_index, which iterates words with all-set and all-unset fast
paths. Use cached indices() or slices() representations when they will be reused.
Back changes to these loops with an appropriate benchmark:
vortex-array/benches/validity_is_valid.rsfor validity access.vortex-mask/benches/valid_counts.rsfor popcount.vortex-mask/benches/mask_iteration.rsfor set-bit iteration.
- Run
cargo clippy --all-targets --all-featuresbefore submitting code - Resolve all clippy warnings
- Follow custom clippy configuration:
- Single character binding names threshold of 2
- Avoid disallowed types like
HashMapandHashSet