From 589c89b77171853ab0c80e0003fcd6c03905f419 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 29 Dec 2025 14:08:45 -0300 Subject: [PATCH] feat: add type_system module with shared type abstractions Shared type abstractions used by both query and bytecode modules: - Arity: structural type shapes (Optional, Array, Object, Variant) - Kind: JSON value types (Null, Bool, Number, String, Node) - Primitives: source text extraction annotations - Quantifier: repetition modifiers (?, *, +) --- crates/plotnik-lib/src/lib.rs | 1 + crates/plotnik-lib/src/type_system/arity.rs | 56 +++++++ crates/plotnik-lib/src/type_system/kind.rs | 139 ++++++++++++++++++ crates/plotnik-lib/src/type_system/mod.rs | 16 ++ .../plotnik-lib/src/type_system/primitives.rs | 85 +++++++++++ .../plotnik-lib/src/type_system/quantifier.rs | 60 ++++++++ 6 files changed, 357 insertions(+) create mode 100644 crates/plotnik-lib/src/type_system/arity.rs create mode 100644 crates/plotnik-lib/src/type_system/kind.rs create mode 100644 crates/plotnik-lib/src/type_system/mod.rs create mode 100644 crates/plotnik-lib/src/type_system/primitives.rs create mode 100644 crates/plotnik-lib/src/type_system/quantifier.rs diff --git a/crates/plotnik-lib/src/lib.rs b/crates/plotnik-lib/src/lib.rs index 426b770a..a00abd24 100644 --- a/crates/plotnik-lib/src/lib.rs +++ b/crates/plotnik-lib/src/lib.rs @@ -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. /// diff --git a/crates/plotnik-lib/src/type_system/arity.rs b/crates/plotnik-lib/src/type_system/arity.rs new file mode 100644 index 00000000..029ba90a --- /dev/null +++ b/crates/plotnik-lib/src/type_system/arity.rs @@ -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()); + } +} diff --git a/crates/plotnik-lib/src/type_system/kind.rs b/crates/plotnik-lib/src/type_system/kind.rs new file mode 100644 index 00000000..bbf4ec5e --- /dev/null +++ b/crates/plotnik-lib/src/type_system/kind.rs @@ -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 { + 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()); + } +} diff --git a/crates/plotnik-lib/src/type_system/mod.rs b/crates/plotnik-lib/src/type_system/mod.rs new file mode 100644 index 00000000..d64a746b --- /dev/null +++ b/crates/plotnik-lib/src/type_system/mod.rs @@ -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; diff --git a/crates/plotnik-lib/src/type_system/primitives.rs b/crates/plotnik-lib/src/type_system/primitives.rs new file mode 100644 index 00000000..7e5a9fc5 --- /dev/null +++ b/crates/plotnik-lib/src/type_system/primitives.rs @@ -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 { + 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)); + } +} diff --git a/crates/plotnik-lib/src/type_system/quantifier.rs b/crates/plotnik-lib/src/type_system/quantifier.rs new file mode 100644 index 00000000..14652b27 --- /dev/null +++ b/crates/plotnik-lib/src/type_system/quantifier.rs @@ -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()); + } +}