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
45 changes: 45 additions & 0 deletions crates/plotnik-lib/src/ir/effect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Effect operations for the query IR.
//!
//! Effects are recorded during transition execution and replayed
//! during materialization to construct the output value.

use super::ids::{DataFieldId, VariantTagId};

/// Effect operation in the IR effect stream.
///
/// Effects are executed sequentially after a successful match.
/// They manipulate a value stack to construct structured output.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C, u16)]
pub enum EffectOp {
/// Store matched node as current value.
/// Only valid on transitions with Node/Anonymous/Wildcard matcher.
CaptureNode,

/// Push empty array onto stack.
StartArray,

/// Move current value into top array.
PushElement,

/// Pop array from stack into current.
EndArray,

/// Push empty object onto stack.
StartObject,

/// Pop object from stack into current.
EndObject,

/// Move current value into top object at field.
Field(DataFieldId),

/// Push variant container with tag onto stack.
StartVariant(VariantTagId),

/// Pop variant, wrap current, set as current.
EndVariant,

/// Replace current Node with its source text.
ToString,
}
22 changes: 22 additions & 0 deletions crates/plotnik-lib/src/ir/effect_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use super::*;

#[test]
fn effect_op_size_and_align() {
assert_eq!(size_of::<EffectOp>(), 4);
assert_eq!(align_of::<EffectOp>(), 2);
}

#[test]
fn effect_op_variants() {
// Ensure all variants exist and are constructible
let _ = EffectOp::CaptureNode;
let _ = EffectOp::StartArray;
let _ = EffectOp::PushElement;
let _ = EffectOp::EndArray;
let _ = EffectOp::StartObject;
let _ = EffectOp::EndObject;
let _ = EffectOp::Field(0);
let _ = EffectOp::StartVariant(0);
let _ = EffectOp::EndVariant;
let _ = EffectOp::ToString;
}
56 changes: 56 additions & 0 deletions crates/plotnik-lib/src/ir/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Named entrypoints for multi-definition queries.
//!
//! Entrypoints provide named exports for definitions. The default entrypoint
//! is always Transition 0; this table enables accessing other definitions by name.

use super::ids::{StringId, TransitionId, TypeId};

/// Named entrypoint into the query graph.
///
/// Layout: 12 bytes, align 4.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Entrypoint {
/// String ID for the entrypoint name.
name_id: StringId,
_pad: u16,
/// Target transition (definition entry point).
target: TransitionId,
/// Result type of this definition (see ADR-0007).
result_type: TypeId,
_pad2: u16,
}

const _: () = assert!(size_of::<Entrypoint>() == 12);
const _: () = assert!(align_of::<Entrypoint>() == 4);

impl Entrypoint {
/// Creates a new entrypoint.
pub const fn new(name_id: StringId, target: TransitionId, result_type: TypeId) -> Self {
Self {
name_id,
_pad: 0,
target,
result_type,
_pad2: 0,
}
}

/// Returns the string ID of the entrypoint name.
#[inline]
pub const fn name_id(&self) -> StringId {
self.name_id
}

/// Returns the target transition ID.
#[inline]
pub const fn target(&self) -> TransitionId {
self.target
}

/// Returns the result type ID.
#[inline]
pub const fn result_type(&self) -> TypeId {
self.result_type
}
}
37 changes: 37 additions & 0 deletions crates/plotnik-lib/src/ir/ids.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! ID types for the compiled query IR.
//!
//! These are lightweight wrappers/aliases for indices and identifiers
//! used throughout the IR. They provide type safety without runtime cost.

use std::num::NonZeroU16;

/// Index into the transitions segment.
pub type TransitionId = u32;

/// Node type ID from tree-sitter. Do not change the underlying type.
pub type NodeTypeId = u16;

/// Node field ID from tree-sitter. Uses `NonZeroU16` so `Option<NodeFieldId>`
/// is the same size as `NodeFieldId` (niche optimization with 0 = None).
pub type NodeFieldId = NonZeroU16;

/// Index into the string_refs segment.
pub type StringId = u16;

/// Field name in effects (alias for type safety).
pub type DataFieldId = StringId;

/// Variant tag in effects (alias for type safety).
pub type VariantTagId = StringId;

/// Index for definition references (Enter/Exit).
pub type RefId = u16;

/// Index into type_defs segment (with reserved primitives 0-2).
pub type TypeId = u16;

// TypeId reserved constants
pub const TYPE_VOID: TypeId = 0;
pub const TYPE_NODE: TypeId = 1;
pub const TYPE_STR: TypeId = 2;
pub const TYPE_INVALID: TypeId = 0xFFFF;
89 changes: 89 additions & 0 deletions crates/plotnik-lib/src/ir/matcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! Node matchers for transition graph.
//!
//! Matchers are purely for node matching - navigation is handled by `Nav`.

use super::{NodeFieldId, NodeTypeId, Slice};

/// Discriminant for matcher variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatcherKind {
Epsilon,
Node,
Anonymous,
Wildcard,
}

/// Matcher determines what node satisfies a transition.
///
/// Navigation (descend/ascend) is handled by `Nav`, not matchers.
#[repr(C, u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Matcher {
/// Matches without consuming input. Used for control flow transitions.
Epsilon,

/// Matches a named node by kind, optionally constrained by field.
Node {
kind: NodeTypeId,
field: Option<NodeFieldId>,
negated_fields: Slice<NodeFieldId>,
},

/// Matches an anonymous node by kind, optionally constrained by field.
Anonymous {
kind: NodeTypeId,
field: Option<NodeFieldId>,
negated_fields: Slice<NodeFieldId>,
},

/// Matches any node (named or anonymous).
Wildcard,
}

impl Matcher {
/// Returns true if this matcher consumes a node.
#[inline]
pub fn consumes_node(&self) -> bool {
!matches!(self, Matcher::Epsilon)
}

/// Returns the discriminant kind.
#[inline]
pub fn kind(&self) -> MatcherKind {
match self {
Matcher::Epsilon => MatcherKind::Epsilon,
Matcher::Node { .. } => MatcherKind::Node,
Matcher::Anonymous { .. } => MatcherKind::Anonymous,
Matcher::Wildcard => MatcherKind::Wildcard,
}
}

/// Returns the node type ID for Node/Anonymous variants, `None` otherwise.
#[inline]
pub fn node_kind(&self) -> Option<NodeTypeId> {
match self {
Matcher::Node { kind, .. } | Matcher::Anonymous { kind, .. } => Some(*kind),
_ => None,
}
}

/// Returns the field constraint, if any.
#[inline]
pub fn field(&self) -> Option<NodeFieldId> {
match self {
Matcher::Node { field, .. } | Matcher::Anonymous { field, .. } => *field,
_ => None,
}
}

/// Returns the negated fields slice. Empty for Epsilon/Wildcard.
#[inline]
pub fn negated_fields(&self) -> Slice<NodeFieldId> {
match self {
Matcher::Node { negated_fields, .. } | Matcher::Anonymous { negated_fields, .. } => {
*negated_fields
}
_ => Slice::empty(),
}
}
}
27 changes: 27 additions & 0 deletions crates/plotnik-lib/src/ir/matcher_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use super::*;

#[test]
fn matcher_size_and_alignment() {
assert_eq!(size_of::<Matcher>(), 16);
assert_eq!(align_of::<Matcher>(), 4);
}

#[test]
fn consumes_node() {
assert!(!Matcher::Epsilon.consumes_node());
assert!(Matcher::Wildcard.consumes_node());

let node_matcher = Matcher::Node {
kind: 42,
field: None,
negated_fields: Slice::empty(),
};
assert!(node_matcher.consumes_node());

let anon_matcher = Matcher::Anonymous {
kind: 1,
field: None,
negated_fields: Slice::empty(),
};
assert!(anon_matcher.consumes_node());
}
67 changes: 67 additions & 0 deletions crates/plotnik-lib/src/ir/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Intermediate Representation (IR) for compiled queries.
//!
//! This module contains the in-memory representation of compiled queries
//! as defined in ADR-0004 through ADR-0008. The IR is designed for:
//! - Cache-efficient execution (64-byte aligned transitions)
//! - Zero-copy access patterns
//! - WASM compatibility
//!
//! Note: This module contains only type definitions. Query execution
//! lives elsewhere.

mod effect;
mod entrypoint;
mod ids;
mod matcher;
mod nav;
mod ref_transition;
mod slice;
mod string_ref;
mod transition;
mod type_metadata;

#[cfg(test)]
mod effect_tests;
#[cfg(test)]
mod matcher_tests;
#[cfg(test)]
mod ref_transition_tests;
#[cfg(test)]
mod slice_tests;
#[cfg(test)]
mod string_ref_tests;

// Re-export ID types
pub use ids::{
DataFieldId, NodeFieldId, NodeTypeId, RefId, StringId, TransitionId, TypeId, VariantTagId,
};

// Re-export TypeId constants
pub use ids::{TYPE_INVALID, TYPE_NODE, TYPE_STR, TYPE_VOID};

// Re-export Slice
pub use slice::Slice;

// Re-export navigation
pub use nav::{Nav, NavKind};

// Re-export matcher
pub use matcher::{Matcher, MatcherKind};

// Re-export effects
pub use effect::EffectOp;

// Re-export ref transition
pub use ref_transition::RefTransition;

// Re-export transition
pub use transition::{MAX_INLINE_SUCCESSORS, Transition};

// Re-export type metadata
pub use type_metadata::{TYPE_COMPOSITE_START, TypeDef, TypeKind, TypeMember};

// Re-export string ref
pub use string_ref::StringRef;

// Re-export entrypoint
pub use entrypoint::Entrypoint;
Loading