Skip to content
Merged
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
2 changes: 1 addition & 1 deletion crates/plotnik-cli/src/commands/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub fn run(args: DumpArgs) {
query.emit().expect("bytecode emission failed")
};

let module = Module::from_bytes(bytecode).expect("module loading failed");
let module = Module::load(&bytecode).expect("module loading failed");
let colors = Colors::new(args.color);
print!("{}", dump(&module, colors));
}
2 changes: 1 addition & 1 deletion crates/plotnik-cli/src/commands/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub fn run(args: InferArgs) {
}
query.emit().expect("bytecode emission failed")
};
let module = Module::from_bytes(bytecode).expect("module loading failed");
let module = Module::load(&bytecode).expect("module loading failed");

// Emit TypeScript types
let void_type = match args.void_type.as_deref() {
Expand Down
2 changes: 1 addition & 1 deletion crates/plotnik-cli/src/commands/run_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ pub fn prepare_query(input: QueryInput) -> PreparedQuery {
}

let bytecode = emit_linked(&query).expect("emit failed");
let module = Module::from_bytes(bytecode).expect("module load failed");
let module = Module::load(&bytecode).expect("module load failed");

let entrypoint = resolve_entrypoint(&module, input.entry);
let tree = lang.parse(&source_code);
Expand Down
108 changes: 108 additions & 0 deletions crates/plotnik-lib/src/bytecode/aligned_vec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! 64-byte aligned storage for bytecode.
//!
//! Bytecode sections are 64-byte aligned internally. For this alignment to be
//! meaningful at runtime, the buffer itself must start at a 64-byte boundary.
//! Standard `Vec<u8>` provides no alignment guarantees for `u8`.

use std::ops::Deref;

/// Alignment for bytecode buffers (matches `SECTION_ALIGN`).
pub const ALIGN: usize = 64;

/// 64-byte aligned block for bytecode storage.
#[repr(C, align(64))]
#[derive(Clone, Copy)]
struct Block([u8; 64]);

/// Immutable 64-byte aligned byte storage.
///
/// Uses `Vec<Block>` internally — Vec guarantees element alignment,
/// so the data starts at a 64-byte boundary. No custom allocator needed.
pub struct AlignedVec {
blocks: Vec<Block>,
len: usize,
}

impl AlignedVec {
/// Copy bytes into aligned storage.
pub fn copy_from_slice(bytes: &[u8]) -> Self {
if bytes.is_empty() {
return Self {
blocks: Vec::new(),
len: 0,
};
}

let num_blocks = bytes.len().div_ceil(64);
let mut blocks = vec![Block([0; 64]); num_blocks];

// Copy block by block to stay safe
for (i, chunk) in bytes.chunks(64).enumerate() {
blocks[i].0[..chunk.len()].copy_from_slice(chunk);
}

Self {
blocks,
len: bytes.len(),
}
}

/// Read a file into aligned storage.
pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let bytes = std::fs::read(path)?;
Ok(Self::copy_from_slice(&bytes))
}

/// Number of bytes stored.
pub fn len(&self) -> usize {
self.len
}

/// Check if empty.
pub fn is_empty(&self) -> bool {
self.len == 0
}

/// View as byte slice.
pub fn as_slice(&self) -> &[u8] {
if self.blocks.is_empty() {
return &[];
}
if self.len > self.blocks.len() * 64 {
panic!(
"AlignedVec invariant violated: len {} exceeds capacity {}",
self.len,
self.blocks.len() * 64
);
}
// SAFETY: Block is repr(C) with only [u8; 64], so pointer cast is valid.
// We only expose `len` bytes, which were initialized in copy_from_slice.
unsafe { std::slice::from_raw_parts(self.blocks.as_ptr() as *const u8, self.len) }
}
}

impl Deref for AlignedVec {
type Target = [u8];

fn deref(&self) -> &[u8] {
self.as_slice()
}
}

impl Clone for AlignedVec {
fn clone(&self) -> Self {
Self {
blocks: self.blocks.clone(),
len: self.len,
}
}
}

impl std::fmt::Debug for AlignedVec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AlignedVec")
.field("len", &self.len)
.field("aligned", &(self.blocks.as_ptr() as usize).is_multiple_of(ALIGN))
.finish()
}
}
76 changes: 76 additions & 0 deletions crates/plotnik-lib/src/bytecode/aligned_vec_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use super::aligned_vec::{AlignedVec, ALIGN};

fn is_aligned(ptr: *const u8) -> bool {
(ptr as usize).is_multiple_of(ALIGN)
}

#[test]
fn alignment_guarantee() {
let data: Vec<u8> = (0..100).collect();
let vec = AlignedVec::copy_from_slice(&data);
assert!(is_aligned(vec.as_ptr()));
}

#[test]
fn copy_from_slice() {
let data = [1u8, 2, 3, 4, 5];
let vec = AlignedVec::copy_from_slice(&data);

assert!(is_aligned(vec.as_ptr()));
assert_eq!(&*vec, &data);
}

#[test]
fn empty_slice() {
let vec = AlignedVec::copy_from_slice(&[]);
assert!(vec.is_empty());
assert_eq!(vec.len(), 0);
assert_eq!(vec.as_slice(), &[] as &[u8]);
}

#[test]
fn clone_preserves_alignment() {
let data: Vec<u8> = (0..100).collect();
let vec = AlignedVec::copy_from_slice(&data);
let cloned = vec.clone();

assert!(is_aligned(cloned.as_ptr()));
assert_eq!(&*cloned, &*vec);
}

#[test]
fn deref_to_slice() {
let vec = AlignedVec::copy_from_slice(&[10, 20, 30]);

let slice: &[u8] = &vec;
assert_eq!(slice, &[10, 20, 30]);
assert_eq!(vec[0], 10);
assert_eq!(vec[2], 30);
}

#[test]
fn large_data() {
let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
let vec = AlignedVec::copy_from_slice(&data);

assert!(is_aligned(vec.as_ptr()));
assert_eq!(&*vec, &data[..]);
}

#[test]
fn partial_block() {
let data: Vec<u8> = (0..37).collect();
let vec = AlignedVec::copy_from_slice(&data);

assert_eq!(vec.len(), 37);
assert_eq!(&*vec, &data[..]);
}

#[test]
fn exact_block_boundary() {
let data: Vec<u8> = (0..128).map(|i| i as u8).collect();
let vec = AlignedVec::copy_from_slice(&data);

assert_eq!(vec.len(), 128);
assert_eq!(&*vec, &data[..]);
}
5 changes: 5 additions & 0 deletions crates/plotnik-lib/src/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! Implements the binary format specified in `docs/binary-format/`.

mod aligned_vec;
mod constants;
mod dump;
mod effects;
Expand All @@ -16,6 +17,8 @@ mod nav;
mod sections;
mod type_meta;

pub use aligned_vec::AlignedVec;

pub use constants::{
MAGIC, MAX_MATCH_PAYLOAD_SLOTS, MAX_PRE_EFFECTS, SECTION_ALIGN, STEP_SIZE, VERSION,
};
Expand Down Expand Up @@ -56,6 +59,8 @@ pub use ir::{
TrampolineIR,
};

#[cfg(test)]
mod aligned_vec_tests;
#[cfg(test)]
mod effects_tests;
#[cfg(test)]
Expand Down
103 changes: 88 additions & 15 deletions crates/plotnik-lib/src/bytecode/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::io;
use std::ops::Deref;
use std::path::Path;

use super::aligned_vec::AlignedVec;
use super::header::{Header, SectionOffsets};
use super::ids::{StringId, TypeId};
use super::instructions::{Call, Match, Opcode, Return, Trampoline};
Expand All @@ -31,28 +32,69 @@ fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
])
}

/// Storage for bytecode bytes.
#[derive(Debug)]
pub struct ByteStorage(Vec<u8>);
/// Storage for bytecode bytes with guaranteed 64-byte alignment.
///
/// All bytecode must be 64-byte aligned for DFA deserialization and cache
/// efficiency. This enum ensures alignment through two paths:
/// - `Static`: Pre-aligned via `include_query_aligned!` macro
/// - `Aligned`: Allocated with 64-byte alignment via `AlignedVec`
pub enum ByteStorage {
/// Static bytes from `include_query_aligned!` (zero-copy, pre-aligned).
Static(&'static [u8]),
/// Owned bytes with guaranteed 64-byte alignment.
Aligned(AlignedVec),
}

impl Deref for ByteStorage {
type Target = [u8];

fn deref(&self) -> &Self::Target {
&self.0
match self {
ByteStorage::Static(s) => s,
ByteStorage::Aligned(v) => v,
}
}
}

impl std::fmt::Debug for ByteStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ByteStorage::Static(s) => f.debug_tuple("Static").field(&s.len()).finish(),
ByteStorage::Aligned(v) => f.debug_tuple("Aligned").field(&v.len()).finish(),
}
}
}

impl ByteStorage {
/// Create from owned bytes.
pub fn from_vec(bytes: Vec<u8>) -> Self {
Self(bytes)
/// Create from static bytes (zero-copy).
///
/// The bytes must be 64-byte aligned. Use `include_query_aligned!` macro.
///
/// # Panics
/// Panics if bytes are not 64-byte aligned.
pub fn from_static(bytes: &'static [u8]) -> Self {
assert!(
(bytes.as_ptr() as usize).is_multiple_of(64),
"static bytes must be 64-byte aligned; use include_query_aligned! macro"
);
Self::Static(bytes)
}

/// Create from an aligned vector (from compiler or file read).
pub fn from_aligned(vec: AlignedVec) -> Self {
Self::Aligned(vec)
}

/// Create by copying bytes into aligned storage.
///
/// Use this when receiving bytes from unknown sources (e.g., network).
pub fn copy_from_slice(bytes: &[u8]) -> Self {
Self::Aligned(AlignedVec::copy_from_slice(bytes))
}

/// Read a file into memory.
/// Read a file into aligned storage.
pub fn from_file(path: impl AsRef<Path>) -> io::Result<Self> {
let bytes = std::fs::read(path)?;
Ok(Self(bytes))
Ok(Self::Aligned(AlignedVec::from_file(path)?))
}
}

Expand Down Expand Up @@ -118,15 +160,46 @@ pub struct Module {
}

impl Module {
/// Load a module from owned bytes.
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, ModuleError> {
Self::from_storage(ByteStorage::from_vec(bytes))
/// Load a module from an aligned vector (compiler output).
///
/// This is the primary constructor for bytecode produced by the compiler.
pub fn from_aligned(vec: AlignedVec) -> Result<Self, ModuleError> {
Self::from_storage(ByteStorage::from_aligned(vec))
}

/// Load a module from static bytes (zero-copy).
///
/// Use with `include_query_aligned!` to embed aligned bytecode:
/// ```ignore
/// use plotnik_lib::include_query_aligned;
///
/// let module = Module::from_static(include_query_aligned!("query.ptk.bin"))?;
/// ```
///
/// # Panics
/// Panics if bytes are not 64-byte aligned.
pub fn from_static(bytes: &'static [u8]) -> Result<Self, ModuleError> {
Self::from_storage(ByteStorage::from_static(bytes))
}

/// Load a module from a file path.
///
/// Reads the file into 64-byte aligned storage.
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, ModuleError> {
let storage = ByteStorage::from_file(&path)?;
Self::from_storage(storage)
Self::from_storage(ByteStorage::from_file(&path)?)
}

/// Load a module from arbitrary bytes (copies into aligned storage).
///
/// Use this for bytes from unknown sources (network, etc.). Always copies.
pub fn load(bytes: &[u8]) -> Result<Self, ModuleError> {
Self::from_storage(ByteStorage::copy_from_slice(bytes))
}

/// Load a module from owned bytes (copies into aligned storage).
#[deprecated(since = "0.1.0", note = "use `Module::from_aligned` for AlignedVec or `Module::load` for copying")]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, ModuleError> {
Self::load(&bytes)
}

/// Load a module from storage.
Expand Down
Loading