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
110 changes: 110 additions & 0 deletions crates/plotnik-lib/src/bytecode/effects.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! Effect operations for bytecode.

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum EffectOpcode {
Node = 0,
A = 1,
Push = 2,
EndA = 3,
S = 4,
EndS = 5,
Set = 6,
E = 7,
EndE = 8,
Text = 9,
Clear = 10,
Null = 11,
}

impl EffectOpcode {
fn from_u8(v: u8) -> Self {
match v {
0 => Self::Node,
1 => Self::A,
2 => Self::Push,
3 => Self::EndA,
4 => Self::S,
5 => Self::EndS,
6 => Self::Set,
7 => Self::E,
8 => Self::EndE,
9 => Self::Text,
10 => Self::Clear,
11 => Self::Null,
_ => panic!("invalid effect opcode: {v}"),
}
}
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct EffectOp {
pub opcode: EffectOpcode,
pub payload: usize,
}

impl EffectOp {
pub fn from_bytes(bytes: [u8; 2]) -> Self {
let raw = u16::from_le_bytes(bytes);
let opcode = EffectOpcode::from_u8((raw >> 10) as u8);
let payload = (raw & 0x3FF) as usize;
Self { opcode, payload }
}

pub fn to_bytes(self) -> [u8; 2] {
assert!(
self.payload <= 0x3FF,
"effect payload exceeds 10-bit limit: {}",
self.payload
);
let raw = ((self.opcode as u16) << 10) | ((self.payload as u16) & 0x3FF);
raw.to_le_bytes()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn roundtrip_with_payload() {
let op = EffectOp {
opcode: EffectOpcode::Set,
payload: 42,
};
let bytes = op.to_bytes();
let decoded = EffectOp::from_bytes(bytes);
assert_eq!(decoded.opcode, EffectOpcode::Set);
assert_eq!(decoded.payload, 42);
}

#[test]
fn roundtrip_no_payload() {
let op = EffectOp {
opcode: EffectOpcode::Node,
payload: 0,
};
let bytes = op.to_bytes();
let decoded = EffectOp::from_bytes(bytes);
assert_eq!(decoded.opcode, EffectOpcode::Node);
assert_eq!(decoded.payload, 0);
}

#[test]
fn max_payload() {
let op = EffectOp {
opcode: EffectOpcode::E,
payload: 1023,
};
let bytes = op.to_bytes();
let decoded = EffectOp::from_bytes(bytes);
assert_eq!(decoded.payload, 1023);
}

#[test]
#[should_panic(expected = "invalid effect opcode")]
fn invalid_opcode_panics() {
let bytes = [0xFF, 0xFF]; // opcode would be 63, which is invalid
EffectOp::from_bytes(bytes);
}
}
28 changes: 28 additions & 0 deletions crates/plotnik-lib/src/bytecode/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Entrypoint section types.

use super::{QTypeId, StepId, StringId};

/// Named query definition entry point (8 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct Entrypoint {
/// Definition name.
pub name: StringId,
/// Starting instruction (StepId).
pub target: StepId,
/// Result type.
pub result_type: QTypeId,
pub(crate) _pad: u16,
}

const _: () = assert!(std::mem::size_of::<Entrypoint>() == 8);

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn entrypoint_size() {
assert_eq!(std::mem::size_of::<Entrypoint>(), 8);
}
}
9 changes: 9 additions & 0 deletions crates/plotnik-lib/src/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
//! Implements the binary format specified in `docs/binary-format/`.

mod constants;
mod effects;
mod entrypoint;
mod header;
mod ids;
mod nav;
mod sections;
mod type_meta;

pub use constants::{
MAGIC, SECTION_ALIGN, STEP_ACCEPT, STEP_SIZE, TYPE_CUSTOM_START, TYPE_NODE, TYPE_STRING,
Expand All @@ -20,3 +23,9 @@ pub use header::Header;
pub use nav::Nav;

pub use sections::{FieldSymbol, NodeSymbol, Slice, TriviaEntry};

pub use effects::{EffectOp, EffectOpcode};

pub use entrypoint::Entrypoint;

pub use type_meta::{TypeDef, TypeKind, TypeMember, TypeMetaHeader, TypeName};
202 changes: 202 additions & 0 deletions crates/plotnik-lib/src/bytecode/type_meta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//! Type metadata definitions for bytecode format.

use super::{QTypeId, StringId};

// Re-export the shared TypeKind
pub use crate::type_system::TypeKind;

/// Convenience aliases for bytecode-specific naming (ArrayStar/ArrayPlus).
impl TypeKind {
/// Alias for `ArrayZeroOrMore` (T*).
pub const ARRAY_STAR: Self = Self::ArrayZeroOrMore;
/// Alias for `ArrayOneOrMore` (T+).
pub const ARRAY_PLUS: Self = Self::ArrayOneOrMore;
}

/// TypeMeta section header (8 bytes).
///
/// Contains counts for the three sub-sections. Located at `type_meta_offset`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct TypeMetaHeader {
/// Number of TypeDef entries.
pub type_defs_count: u16,
/// Number of TypeMember entries.
pub type_members_count: u16,
/// Number of TypeName entries.
pub type_names_count: u16,
/// Padding for alignment.
pub(crate) _pad: u16,
}

const _: () = assert!(std::mem::size_of::<TypeMetaHeader>() == 8);

impl TypeMetaHeader {
/// Decode from 8 bytes.
pub fn from_bytes(bytes: &[u8]) -> Self {
assert!(bytes.len() >= 8, "TypeMetaHeader too short");
Self {
type_defs_count: u16::from_le_bytes([bytes[0], bytes[1]]),
type_members_count: u16::from_le_bytes([bytes[2], bytes[3]]),
type_names_count: u16::from_le_bytes([bytes[4], bytes[5]]),
_pad: u16::from_le_bytes([bytes[6], bytes[7]]),
}
}

/// Encode to 8 bytes.
pub fn to_bytes(&self) -> [u8; 8] {
let mut bytes = [0u8; 8];
bytes[0..2].copy_from_slice(&self.type_defs_count.to_le_bytes());
bytes[2..4].copy_from_slice(&self.type_members_count.to_le_bytes());
bytes[4..6].copy_from_slice(&self.type_names_count.to_le_bytes());
bytes[6..8].copy_from_slice(&self._pad.to_le_bytes());
bytes
}
}

/// Type definition entry (4 bytes).
///
/// Semantics of `data` and `count` depend on `kind`:
/// - Wrappers (Optional, ArrayStar, ArrayPlus): `data` = inner TypeId, `count` = 0
/// - Struct/Enum: `data` = member index, `count` = member count
/// - Alias: `data` = target TypeId, `count` = 0
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct TypeDef {
/// For wrappers/alias: inner/target TypeId.
/// For Struct/Enum: index into TypeMembers section.
pub data: u16,
/// Member count (0 for wrappers/alias, field/variant count for composites).
pub count: u8,
/// TypeKind discriminant.
pub kind: u8,
}

const _: () = assert!(std::mem::size_of::<TypeDef>() == 4);

impl TypeDef {
/// For wrapper types, get the inner type.
#[inline]
pub fn inner_type(&self) -> Option<QTypeId> {
TypeKind::from_u8(self.kind)
.filter(|k| k.is_wrapper())
.map(|_| QTypeId(self.data))
}

/// Get the TypeKind for this definition.
#[inline]
pub fn type_kind(&self) -> Option<TypeKind> {
TypeKind::from_u8(self.kind)
}

/// Whether this is an alias type.
#[inline]
pub fn is_alias(&self) -> bool {
TypeKind::from_u8(self.kind).is_some_and(|k| k.is_alias())
}

/// For alias types, get the target type.
#[inline]
pub fn alias_target(&self) -> Option<QTypeId> {
TypeKind::from_u8(self.kind)
.filter(|k| k.is_alias())
.map(|_| QTypeId(self.data))
}

/// For Struct/Enum types, get the member index.
#[inline]
pub fn member_index(&self) -> Option<u16> {
TypeKind::from_u8(self.kind)
.filter(|k| k.is_composite())
.map(|_| self.data)
}

/// For Struct/Enum types, get the member count.
#[inline]
pub fn member_count(&self) -> Option<u8> {
TypeKind::from_u8(self.kind)
.filter(|k| k.is_composite())
.map(|_| self.count)
}
}

/// Maps a name to a type (4 bytes).
///
/// Only named types (definitions, aliases) have entries here.
/// Entries are sorted lexicographically by name for binary search.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct TypeName {
/// StringId of the type name.
pub name: StringId,
/// TypeId this name refers to.
pub type_id: QTypeId,
}

const _: () = assert!(std::mem::size_of::<TypeName>() == 4);

/// Field or variant entry (4 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct TypeMember {
/// Field/variant name.
pub name: StringId,
/// Type of this field/variant.
pub type_id: QTypeId,
}

const _: () = assert!(std::mem::size_of::<TypeMember>() == 4);

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn type_meta_header_size() {
assert_eq!(std::mem::size_of::<TypeMetaHeader>(), 8);
}

#[test]
fn type_meta_header_roundtrip() {
let header = TypeMetaHeader {
type_defs_count: 42,
type_members_count: 100,
type_names_count: 5,
..Default::default()
};
let bytes = header.to_bytes();
let decoded = TypeMetaHeader::from_bytes(&bytes);
assert_eq!(decoded, header);
}

#[test]
fn type_def_size() {
assert_eq!(std::mem::size_of::<TypeDef>(), 4);
}

#[test]
fn type_member_size() {
assert_eq!(std::mem::size_of::<TypeMember>(), 4);
}

#[test]
fn type_name_size() {
assert_eq!(std::mem::size_of::<TypeName>(), 4);
}

#[test]
fn type_kind_is_wrapper() {
assert!(TypeKind::Optional.is_wrapper());
assert!(TypeKind::ArrayZeroOrMore.is_wrapper());
assert!(TypeKind::ArrayOneOrMore.is_wrapper());
assert!(!TypeKind::Struct.is_wrapper());
assert!(!TypeKind::Enum.is_wrapper());
}

#[test]
fn type_kind_aliases() {
// Test bytecode-friendly aliases
assert_eq!(TypeKind::ARRAY_STAR, TypeKind::ArrayZeroOrMore);
assert_eq!(TypeKind::ARRAY_PLUS, TypeKind::ArrayOneOrMore);
}
}
Loading