Skip to content

Commit 1ca408d

Browse files
authored
feat(lib): add type_system module with shared type abstractions (#167)
1 parent 1ce51c8 commit 1ca408d

File tree

6 files changed

+357
-0
lines changed

6 files changed

+357
-0
lines changed

crates/plotnik-lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
pub mod diagnostics;
2020
pub mod parser;
2121
pub mod query;
22+
pub mod type_system;
2223

2324
/// Result type for analysis passes that produce both output and diagnostics.
2425
///
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//! Structural arity definitions.
2+
//!
3+
//! Arity tracks whether an expression matches one or many node positions.
4+
5+
/// Structural arity - whether an expression matches one or many positions.
6+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
7+
pub enum Arity {
8+
/// Exactly one node position.
9+
One,
10+
/// Multiple sequential positions.
11+
Many,
12+
}
13+
14+
impl Arity {
15+
/// Combine arities: Many wins.
16+
///
17+
/// When combining expressions, if either has Many arity,
18+
/// the result has Many arity.
19+
pub fn combine(self, other: Self) -> Self {
20+
if self == Self::One && other == Self::One {
21+
return Self::One;
22+
}
23+
Self::Many
24+
}
25+
26+
/// Check if this is singular arity.
27+
pub fn is_one(self) -> bool {
28+
self == Self::One
29+
}
30+
31+
/// Check if this is plural arity.
32+
pub fn is_many(self) -> bool {
33+
self == Self::Many
34+
}
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use super::*;
40+
41+
#[test]
42+
fn combine_arities() {
43+
assert_eq!(Arity::One.combine(Arity::One), Arity::One);
44+
assert_eq!(Arity::One.combine(Arity::Many), Arity::Many);
45+
assert_eq!(Arity::Many.combine(Arity::One), Arity::Many);
46+
assert_eq!(Arity::Many.combine(Arity::Many), Arity::Many);
47+
}
48+
49+
#[test]
50+
fn is_one_and_many() {
51+
assert!(Arity::One.is_one());
52+
assert!(!Arity::One.is_many());
53+
assert!(!Arity::Many.is_one());
54+
assert!(Arity::Many.is_many());
55+
}
56+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//! Canonical type kind definitions.
2+
//!
3+
//! This enum represents the semantic type kinds shared across the system.
4+
//! Different modules may have their own representations that map to/from this.
5+
6+
/// Semantic type kinds.
7+
///
8+
/// This is the canonical enumeration of composite type kinds.
9+
/// Primitive types (Void, Node, String) are handled via reserved indices,
10+
/// not as variants here.
11+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
12+
#[repr(u8)]
13+
pub enum TypeKind {
14+
/// `T?` - optional wrapper, contains zero or one value.
15+
Optional = 0,
16+
/// `T*` - array of zero or more values.
17+
ArrayZeroOrMore = 1,
18+
/// `T+` - array of one or more values (non-empty).
19+
ArrayOneOrMore = 2,
20+
/// Record with named fields.
21+
Struct = 3,
22+
/// Discriminated union with tagged variants.
23+
Enum = 4,
24+
/// Named reference to another type (e.g., `type Foo = Bar`).
25+
Alias = 5,
26+
}
27+
28+
impl TypeKind {
29+
/// Convert from raw discriminant.
30+
pub fn from_u8(v: u8) -> Option<Self> {
31+
match v {
32+
0 => Some(Self::Optional),
33+
1 => Some(Self::ArrayZeroOrMore),
34+
2 => Some(Self::ArrayOneOrMore),
35+
3 => Some(Self::Struct),
36+
4 => Some(Self::Enum),
37+
5 => Some(Self::Alias),
38+
_ => None,
39+
}
40+
}
41+
42+
/// Whether this is a wrapper type (Optional, ArrayZeroOrMore, ArrayOneOrMore).
43+
///
44+
/// Wrapper types contain a single inner type.
45+
/// Composite types (Struct, Enum) have named members.
46+
pub fn is_wrapper(self) -> bool {
47+
matches!(
48+
self,
49+
Self::Optional | Self::ArrayZeroOrMore | Self::ArrayOneOrMore
50+
)
51+
}
52+
53+
/// Whether this is a composite type (Struct, Enum).
54+
pub fn is_composite(self) -> bool {
55+
matches!(self, Self::Struct | Self::Enum)
56+
}
57+
58+
/// Whether this is an array type.
59+
pub fn is_array(self) -> bool {
60+
matches!(self, Self::ArrayZeroOrMore | Self::ArrayOneOrMore)
61+
}
62+
63+
/// For array types, whether the array is non-empty.
64+
pub fn array_is_non_empty(self) -> bool {
65+
matches!(self, Self::ArrayOneOrMore)
66+
}
67+
68+
/// Whether this is an alias type.
69+
pub fn is_alias(self) -> bool {
70+
matches!(self, Self::Alias)
71+
}
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
78+
#[test]
79+
fn from_u8_valid() {
80+
assert_eq!(TypeKind::from_u8(0), Some(TypeKind::Optional));
81+
assert_eq!(TypeKind::from_u8(1), Some(TypeKind::ArrayZeroOrMore));
82+
assert_eq!(TypeKind::from_u8(2), Some(TypeKind::ArrayOneOrMore));
83+
assert_eq!(TypeKind::from_u8(3), Some(TypeKind::Struct));
84+
assert_eq!(TypeKind::from_u8(4), Some(TypeKind::Enum));
85+
assert_eq!(TypeKind::from_u8(5), Some(TypeKind::Alias));
86+
}
87+
88+
#[test]
89+
fn from_u8_invalid() {
90+
assert_eq!(TypeKind::from_u8(6), None);
91+
assert_eq!(TypeKind::from_u8(255), None);
92+
}
93+
94+
#[test]
95+
fn is_wrapper() {
96+
assert!(TypeKind::Optional.is_wrapper());
97+
assert!(TypeKind::ArrayZeroOrMore.is_wrapper());
98+
assert!(TypeKind::ArrayOneOrMore.is_wrapper());
99+
assert!(!TypeKind::Struct.is_wrapper());
100+
assert!(!TypeKind::Enum.is_wrapper());
101+
assert!(!TypeKind::Alias.is_wrapper());
102+
}
103+
104+
#[test]
105+
fn is_composite() {
106+
assert!(!TypeKind::Optional.is_composite());
107+
assert!(!TypeKind::ArrayZeroOrMore.is_composite());
108+
assert!(!TypeKind::ArrayOneOrMore.is_composite());
109+
assert!(TypeKind::Struct.is_composite());
110+
assert!(TypeKind::Enum.is_composite());
111+
assert!(!TypeKind::Alias.is_composite());
112+
}
113+
114+
#[test]
115+
fn is_array() {
116+
assert!(!TypeKind::Optional.is_array());
117+
assert!(TypeKind::ArrayZeroOrMore.is_array());
118+
assert!(TypeKind::ArrayOneOrMore.is_array());
119+
assert!(!TypeKind::Struct.is_array());
120+
assert!(!TypeKind::Enum.is_array());
121+
assert!(!TypeKind::Alias.is_array());
122+
}
123+
124+
#[test]
125+
fn array_is_non_empty() {
126+
assert!(!TypeKind::ArrayZeroOrMore.array_is_non_empty());
127+
assert!(TypeKind::ArrayOneOrMore.array_is_non_empty());
128+
}
129+
130+
#[test]
131+
fn is_alias() {
132+
assert!(!TypeKind::Optional.is_alias());
133+
assert!(!TypeKind::ArrayZeroOrMore.is_alias());
134+
assert!(!TypeKind::ArrayOneOrMore.is_alias());
135+
assert!(!TypeKind::Struct.is_alias());
136+
assert!(!TypeKind::Enum.is_alias());
137+
assert!(TypeKind::Alias.is_alias());
138+
}
139+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Core type system definitions shared between analysis and bytecode.
2+
//!
3+
//! This module provides the canonical type model used across:
4+
//! - Type checking/inference (`query::type_check`)
5+
//! - Bytecode emission and runtime (`bytecode`)
6+
//! - TypeScript code generation
7+
8+
mod arity;
9+
mod kind;
10+
mod primitives;
11+
mod quantifier;
12+
13+
pub use arity::Arity;
14+
pub use kind::TypeKind;
15+
pub use primitives::{PrimitiveType, TYPE_CUSTOM_START, TYPE_NODE, TYPE_STRING, TYPE_VOID};
16+
pub use quantifier::QuantifierKind;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//! Primitive (builtin) type definitions.
2+
//!
3+
//! These are the fundamental types that exist in every query,
4+
//! with fixed indices 0, 1, 2 reserved across both analysis and bytecode.
5+
6+
/// Index for the Void type (produces nothing).
7+
pub const TYPE_VOID: u16 = 0;
8+
9+
/// Index for the Node type (tree-sitter AST node reference).
10+
pub const TYPE_NODE: u16 = 1;
11+
12+
/// Index for the String type (extracted source text).
13+
pub const TYPE_STRING: u16 = 2;
14+
15+
/// First index available for user-defined/composite types.
16+
pub const TYPE_CUSTOM_START: u16 = 3;
17+
18+
/// Primitive type enumeration.
19+
///
20+
/// These are the builtin scalar types that don't require
21+
/// additional metadata in the type table.
22+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
23+
#[repr(u16)]
24+
pub enum PrimitiveType {
25+
/// Produces nothing, transparent to parent scope.
26+
Void = TYPE_VOID,
27+
/// A tree-sitter AST node reference.
28+
Node = TYPE_NODE,
29+
/// Extracted text from a node.
30+
String = TYPE_STRING,
31+
}
32+
33+
impl PrimitiveType {
34+
/// Try to convert a type index to a primitive type.
35+
#[inline]
36+
pub fn from_index(index: u16) -> Option<Self> {
37+
match index {
38+
TYPE_VOID => Some(Self::Void),
39+
TYPE_NODE => Some(Self::Node),
40+
TYPE_STRING => Some(Self::String),
41+
_ => None,
42+
}
43+
}
44+
45+
/// Get the type index for this primitive.
46+
#[inline]
47+
pub const fn index(self) -> u16 {
48+
self as u16
49+
}
50+
51+
/// Check if a type index is a builtin primitive.
52+
#[inline]
53+
pub fn is_builtin(index: u16) -> bool {
54+
index <= TYPE_STRING
55+
}
56+
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
62+
#[test]
63+
fn primitive_indices() {
64+
assert_eq!(PrimitiveType::Void.index(), 0);
65+
assert_eq!(PrimitiveType::Node.index(), 1);
66+
assert_eq!(PrimitiveType::String.index(), 2);
67+
}
68+
69+
#[test]
70+
fn from_index() {
71+
assert_eq!(PrimitiveType::from_index(0), Some(PrimitiveType::Void));
72+
assert_eq!(PrimitiveType::from_index(1), Some(PrimitiveType::Node));
73+
assert_eq!(PrimitiveType::from_index(2), Some(PrimitiveType::String));
74+
assert_eq!(PrimitiveType::from_index(3), None);
75+
}
76+
77+
#[test]
78+
fn is_builtin() {
79+
assert!(PrimitiveType::is_builtin(0));
80+
assert!(PrimitiveType::is_builtin(1));
81+
assert!(PrimitiveType::is_builtin(2));
82+
assert!(!PrimitiveType::is_builtin(3));
83+
assert!(!PrimitiveType::is_builtin(100));
84+
}
85+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//! Quantifier kinds for type inference.
2+
//!
3+
//! Quantifiers determine cardinality: how many times a pattern can match.
4+
5+
/// Quantifier kind for pattern matching.
6+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
7+
pub enum QuantifierKind {
8+
/// `?` or `??` - zero or one.
9+
Optional,
10+
/// `*` or `*?` - zero or more.
11+
ZeroOrMore,
12+
/// `+` or `+?` - one or more.
13+
OneOrMore,
14+
}
15+
16+
impl QuantifierKind {
17+
/// Whether this quantifier requires strict dimensionality (row capture).
18+
///
19+
/// `*` and `+` produce arrays, so internal captures need explicit row structure.
20+
/// `?` produces at most one value, so no dimensionality issue.
21+
pub fn requires_row_capture(self) -> bool {
22+
matches!(self, Self::ZeroOrMore | Self::OneOrMore)
23+
}
24+
25+
/// Whether this quantifier guarantees at least one match.
26+
pub fn is_non_empty(self) -> bool {
27+
matches!(self, Self::OneOrMore)
28+
}
29+
30+
/// Whether this quantifier can match zero times.
31+
pub fn can_be_empty(self) -> bool {
32+
matches!(self, Self::Optional | Self::ZeroOrMore)
33+
}
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use super::*;
39+
40+
#[test]
41+
fn requires_row_capture() {
42+
assert!(!QuantifierKind::Optional.requires_row_capture());
43+
assert!(QuantifierKind::ZeroOrMore.requires_row_capture());
44+
assert!(QuantifierKind::OneOrMore.requires_row_capture());
45+
}
46+
47+
#[test]
48+
fn is_non_empty() {
49+
assert!(!QuantifierKind::Optional.is_non_empty());
50+
assert!(!QuantifierKind::ZeroOrMore.is_non_empty());
51+
assert!(QuantifierKind::OneOrMore.is_non_empty());
52+
}
53+
54+
#[test]
55+
fn can_be_empty() {
56+
assert!(QuantifierKind::Optional.can_be_empty());
57+
assert!(QuantifierKind::ZeroOrMore.can_be_empty());
58+
assert!(!QuantifierKind::OneOrMore.can_be_empty());
59+
}
60+
}

0 commit comments

Comments
 (0)