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
1 change: 1 addition & 0 deletions crates/plotnik-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
pub mod diagnostics;
pub mod parser;
pub mod query;
pub mod type_system;

/// Result type for analysis passes that produce both output and diagnostics.
///
Expand Down
56 changes: 56 additions & 0 deletions crates/plotnik-lib/src/type_system/arity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Structural arity definitions.
//!
//! Arity tracks whether an expression matches one or many node positions.

/// Structural arity - whether an expression matches one or many positions.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Arity {
/// Exactly one node position.
One,
/// Multiple sequential positions.
Many,
}

impl Arity {
/// Combine arities: Many wins.
///
/// When combining expressions, if either has Many arity,
/// the result has Many arity.
pub fn combine(self, other: Self) -> Self {
if self == Self::One && other == Self::One {
return Self::One;
}
Self::Many
}

/// Check if this is singular arity.
pub fn is_one(self) -> bool {
self == Self::One
}

/// Check if this is plural arity.
pub fn is_many(self) -> bool {
self == Self::Many
}
}

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

#[test]
fn combine_arities() {
assert_eq!(Arity::One.combine(Arity::One), Arity::One);
assert_eq!(Arity::One.combine(Arity::Many), Arity::Many);
assert_eq!(Arity::Many.combine(Arity::One), Arity::Many);
assert_eq!(Arity::Many.combine(Arity::Many), Arity::Many);
}

#[test]
fn is_one_and_many() {
assert!(Arity::One.is_one());
assert!(!Arity::One.is_many());
assert!(!Arity::Many.is_one());
assert!(Arity::Many.is_many());
}
}
139 changes: 139 additions & 0 deletions crates/plotnik-lib/src/type_system/kind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Canonical type kind definitions.
//!
//! This enum represents the semantic type kinds shared across the system.
//! Different modules may have their own representations that map to/from this.

/// Semantic type kinds.
///
/// This is the canonical enumeration of composite type kinds.
/// Primitive types (Void, Node, String) are handled via reserved indices,
/// not as variants here.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[repr(u8)]
pub enum TypeKind {
/// `T?` - optional wrapper, contains zero or one value.
Optional = 0,
/// `T*` - array of zero or more values.
ArrayZeroOrMore = 1,
/// `T+` - array of one or more values (non-empty).
ArrayOneOrMore = 2,
/// Record with named fields.
Struct = 3,
/// Discriminated union with tagged variants.
Enum = 4,
/// Named reference to another type (e.g., `type Foo = Bar`).
Alias = 5,
}

impl TypeKind {
/// Convert from raw discriminant.
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Optional),
1 => Some(Self::ArrayZeroOrMore),
2 => Some(Self::ArrayOneOrMore),
3 => Some(Self::Struct),
4 => Some(Self::Enum),
5 => Some(Self::Alias),
_ => None,
}
}

/// Whether this is a wrapper type (Optional, ArrayZeroOrMore, ArrayOneOrMore).
///
/// Wrapper types contain a single inner type.
/// Composite types (Struct, Enum) have named members.
pub fn is_wrapper(self) -> bool {
matches!(
self,
Self::Optional | Self::ArrayZeroOrMore | Self::ArrayOneOrMore
)
}

/// Whether this is a composite type (Struct, Enum).
pub fn is_composite(self) -> bool {
matches!(self, Self::Struct | Self::Enum)
}

/// Whether this is an array type.
pub fn is_array(self) -> bool {
matches!(self, Self::ArrayZeroOrMore | Self::ArrayOneOrMore)
}

/// For array types, whether the array is non-empty.
pub fn array_is_non_empty(self) -> bool {
matches!(self, Self::ArrayOneOrMore)
}

/// Whether this is an alias type.
pub fn is_alias(self) -> bool {
matches!(self, Self::Alias)
}
}

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

#[test]
fn from_u8_valid() {
assert_eq!(TypeKind::from_u8(0), Some(TypeKind::Optional));
assert_eq!(TypeKind::from_u8(1), Some(TypeKind::ArrayZeroOrMore));
assert_eq!(TypeKind::from_u8(2), Some(TypeKind::ArrayOneOrMore));
assert_eq!(TypeKind::from_u8(3), Some(TypeKind::Struct));
assert_eq!(TypeKind::from_u8(4), Some(TypeKind::Enum));
assert_eq!(TypeKind::from_u8(5), Some(TypeKind::Alias));
}

#[test]
fn from_u8_invalid() {
assert_eq!(TypeKind::from_u8(6), None);
assert_eq!(TypeKind::from_u8(255), None);
}

#[test]
fn 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());
assert!(!TypeKind::Alias.is_wrapper());
}

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

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

#[test]
fn array_is_non_empty() {
assert!(!TypeKind::ArrayZeroOrMore.array_is_non_empty());
assert!(TypeKind::ArrayOneOrMore.array_is_non_empty());
}

#[test]
fn is_alias() {
assert!(!TypeKind::Optional.is_alias());
assert!(!TypeKind::ArrayZeroOrMore.is_alias());
assert!(!TypeKind::ArrayOneOrMore.is_alias());
assert!(!TypeKind::Struct.is_alias());
assert!(!TypeKind::Enum.is_alias());
assert!(TypeKind::Alias.is_alias());
}
}
16 changes: 16 additions & 0 deletions crates/plotnik-lib/src/type_system/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! Core type system definitions shared between analysis and bytecode.
//!
//! This module provides the canonical type model used across:
//! - Type checking/inference (`query::type_check`)
//! - Bytecode emission and runtime (`bytecode`)
//! - TypeScript code generation

mod arity;
mod kind;
mod primitives;
mod quantifier;

pub use arity::Arity;
pub use kind::TypeKind;
pub use primitives::{PrimitiveType, TYPE_CUSTOM_START, TYPE_NODE, TYPE_STRING, TYPE_VOID};
pub use quantifier::QuantifierKind;
85 changes: 85 additions & 0 deletions crates/plotnik-lib/src/type_system/primitives.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Primitive (builtin) type definitions.
//!
//! These are the fundamental types that exist in every query,
//! with fixed indices 0, 1, 2 reserved across both analysis and bytecode.

/// Index for the Void type (produces nothing).
pub const TYPE_VOID: u16 = 0;

/// Index for the Node type (tree-sitter AST node reference).
pub const TYPE_NODE: u16 = 1;

/// Index for the String type (extracted source text).
pub const TYPE_STRING: u16 = 2;

/// First index available for user-defined/composite types.
pub const TYPE_CUSTOM_START: u16 = 3;

/// Primitive type enumeration.
///
/// These are the builtin scalar types that don't require
/// additional metadata in the type table.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[repr(u16)]
pub enum PrimitiveType {
/// Produces nothing, transparent to parent scope.
Void = TYPE_VOID,
/// A tree-sitter AST node reference.
Node = TYPE_NODE,
/// Extracted text from a node.
String = TYPE_STRING,
}

impl PrimitiveType {
/// Try to convert a type index to a primitive type.
#[inline]
pub fn from_index(index: u16) -> Option<Self> {
match index {
TYPE_VOID => Some(Self::Void),
TYPE_NODE => Some(Self::Node),
TYPE_STRING => Some(Self::String),
_ => None,
}
}

/// Get the type index for this primitive.
#[inline]
pub const fn index(self) -> u16 {
self as u16
}

/// Check if a type index is a builtin primitive.
#[inline]
pub fn is_builtin(index: u16) -> bool {
index <= TYPE_STRING
}
}

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

#[test]
fn primitive_indices() {
assert_eq!(PrimitiveType::Void.index(), 0);
assert_eq!(PrimitiveType::Node.index(), 1);
assert_eq!(PrimitiveType::String.index(), 2);
}

#[test]
fn from_index() {
assert_eq!(PrimitiveType::from_index(0), Some(PrimitiveType::Void));
assert_eq!(PrimitiveType::from_index(1), Some(PrimitiveType::Node));
assert_eq!(PrimitiveType::from_index(2), Some(PrimitiveType::String));
assert_eq!(PrimitiveType::from_index(3), None);
}

#[test]
fn is_builtin() {
assert!(PrimitiveType::is_builtin(0));
assert!(PrimitiveType::is_builtin(1));
assert!(PrimitiveType::is_builtin(2));
assert!(!PrimitiveType::is_builtin(3));
assert!(!PrimitiveType::is_builtin(100));
}
}
60 changes: 60 additions & 0 deletions crates/plotnik-lib/src/type_system/quantifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Quantifier kinds for type inference.
//!
//! Quantifiers determine cardinality: how many times a pattern can match.

/// Quantifier kind for pattern matching.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum QuantifierKind {
/// `?` or `??` - zero or one.
Optional,
/// `*` or `*?` - zero or more.
ZeroOrMore,
/// `+` or `+?` - one or more.
OneOrMore,
}

impl QuantifierKind {
/// Whether this quantifier requires strict dimensionality (row capture).
///
/// `*` and `+` produce arrays, so internal captures need explicit row structure.
/// `?` produces at most one value, so no dimensionality issue.
pub fn requires_row_capture(self) -> bool {
matches!(self, Self::ZeroOrMore | Self::OneOrMore)
}

/// Whether this quantifier guarantees at least one match.
pub fn is_non_empty(self) -> bool {
matches!(self, Self::OneOrMore)
}

/// Whether this quantifier can match zero times.
pub fn can_be_empty(self) -> bool {
matches!(self, Self::Optional | Self::ZeroOrMore)
}
}

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

#[test]
fn requires_row_capture() {
assert!(!QuantifierKind::Optional.requires_row_capture());
assert!(QuantifierKind::ZeroOrMore.requires_row_capture());
assert!(QuantifierKind::OneOrMore.requires_row_capture());
}

#[test]
fn is_non_empty() {
assert!(!QuantifierKind::Optional.is_non_empty());
assert!(!QuantifierKind::ZeroOrMore.is_non_empty());
assert!(QuantifierKind::OneOrMore.is_non_empty());
}

#[test]
fn can_be_empty() {
assert!(QuantifierKind::Optional.can_be_empty());
assert!(QuantifierKind::ZeroOrMore.can_be_empty());
assert!(!QuantifierKind::OneOrMore.can_be_empty());
}
}