Skip to content

Commit 942436d

Browse files
rz1989sclaude
andcommitted
feat(#53a): Add AST foundations for module system with visibility support
Foundation work for nested module support (#53). This commit adds the core AST structures and visibility parsing needed for Rust-style module system. **Added:** - `Module`, `UseStatement`, `ModulePath`, `PathSegment` structs to AST - `Visibility` enum (Public/Private) for type definitions - Helper methods for `ModulePath` (is_absolute, final_ident, to_string) - `parse_visibility()` in parser to extract pub keyword from syn **Changed:** - `StructDef`, `EnumDef`, `TypeAlias` now include `visibility` field - `Item` enum extended with `Module` and `Use` variants - Parser updated to extract visibility from all type definitions - Transform skips Module/Use items (to be implemented in #53b) - FileResolver handles Module/Use items in validate_imports **Testing:** - All 120 tests passing (107 core + 13 LSP) - Updated existing test fixtures with visibility field - No breaking changes to existing functionality **Next Steps:** - #53b: Implement module resolution and hierarchical loading - #53c: Update generators for pub mod output - #53d: Add tests and examples Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e6a1cbf commit 942436d

File tree

4 files changed

+212
-8
lines changed

4 files changed

+212
-8
lines changed

packages/core/src/ast.rs

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub struct LumosFile {
1919
pub items: Vec<Item>,
2020
}
2121

22-
/// An import statement
22+
/// An import statement (JavaScript-style, legacy)
2323
#[derive(Debug, Clone, Serialize, Deserialize)]
2424
pub struct Import {
2525
/// Items being imported (e.g., ["UserId", "Timestamp"])
@@ -33,12 +33,77 @@ pub struct Import {
3333
pub span: Option<proc_macro2::Span>,
3434
}
3535

36+
/// A module declaration (Rust-style: `mod name;`)
37+
#[derive(Debug, Clone, Serialize, Deserialize)]
38+
pub struct Module {
39+
/// Module name (e.g., "models")
40+
pub name: String,
41+
42+
/// Visibility (pub or private)
43+
pub visibility: Visibility,
44+
45+
/// Span information for error reporting
46+
#[serde(skip)]
47+
pub span: Option<proc_macro2::Span>,
48+
}
49+
50+
/// A use statement (Rust-style: `use path::Type;`)
51+
#[derive(Debug, Clone, Serialize, Deserialize)]
52+
pub struct UseStatement {
53+
/// Module path (e.g., crate::models::User)
54+
pub path: ModulePath,
55+
56+
/// Optional alias (e.g., `use path::Type as Alias;`)
57+
pub alias: Option<String>,
58+
59+
/// Span information for error reporting
60+
#[serde(skip)]
61+
pub span: Option<proc_macro2::Span>,
62+
}
63+
64+
/// Module path (e.g., crate::models::User)
65+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66+
pub struct ModulePath {
67+
/// Path segments (e.g., [Crate, Ident("models"), Ident("User")])
68+
pub segments: Vec<PathSegment>,
69+
}
70+
71+
/// A segment in a module path
72+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73+
pub enum PathSegment {
74+
/// `crate` keyword
75+
Crate,
76+
77+
/// `super` keyword
78+
Super,
79+
80+
/// `self` keyword
81+
SelfPath,
82+
83+
/// Identifier (module or type name)
84+
Ident(String),
85+
}
86+
87+
/// Visibility modifier
88+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
89+
pub enum Visibility {
90+
/// Private (no `pub` keyword)
91+
#[default]
92+
Private,
93+
94+
/// Public (`pub` keyword)
95+
Public,
96+
}
97+
3698
/// A type alias definition
3799
#[derive(Debug, Clone, Serialize, Deserialize)]
38100
pub struct TypeAlias {
39101
/// Alias name (e.g., "UserId")
40102
pub name: String,
41103

104+
/// Visibility (pub or private)
105+
pub visibility: Visibility,
106+
42107
/// Target type (e.g., PublicKey)
43108
pub target: TypeSpec,
44109

@@ -47,7 +112,7 @@ pub struct TypeAlias {
47112
pub span: Option<proc_macro2::Span>,
48113
}
49114

50-
/// An item in a LUMOS file (struct, enum, or type alias)
115+
/// An item in a LUMOS file (struct, enum, type alias, module, or use statement)
51116
#[derive(Debug, Clone, Serialize, Deserialize)]
52117
pub enum Item {
53118
/// Struct definition
@@ -58,6 +123,12 @@ pub enum Item {
58123

59124
/// Type alias definition
60125
TypeAlias(TypeAlias),
126+
127+
/// Module declaration (Rust-style)
128+
Module(Module),
129+
130+
/// Use statement (Rust-style)
131+
Use(UseStatement),
61132
}
62133

63134
/// A struct definition
@@ -66,6 +137,9 @@ pub struct StructDef {
66137
/// Struct name (e.g., "UserAccount")
67138
pub name: String,
68139

140+
/// Visibility (pub or private)
141+
pub visibility: Visibility,
142+
69143
/// Attributes applied to the struct (e.g., @solana, @account)
70144
pub attributes: Vec<Attribute>,
71145

@@ -86,6 +160,9 @@ pub struct EnumDef {
86160
/// Enum name (e.g., "GameState")
87161
pub name: String,
88162

163+
/// Visibility (pub or private)
164+
pub visibility: Visibility,
165+
89166
/// Attributes applied to the enum (e.g., @solana)
90167
pub attributes: Vec<Attribute>,
91168

@@ -323,6 +400,88 @@ impl Import {
323400
}
324401
}
325402

403+
impl ModulePath {
404+
/// Create a new module path from segments
405+
pub fn new(segments: Vec<PathSegment>) -> Self {
406+
Self { segments }
407+
}
408+
409+
/// Create a simple identifier path (e.g., "models" -> [Ident("models")])
410+
pub fn from_ident(name: String) -> Self {
411+
Self {
412+
segments: vec![PathSegment::Ident(name)],
413+
}
414+
}
415+
416+
/// Check if this path starts with `crate::`
417+
pub fn is_absolute(&self) -> bool {
418+
matches!(self.segments.first(), Some(PathSegment::Crate))
419+
}
420+
421+
/// Check if this path starts with `super::`
422+
pub fn starts_with_super(&self) -> bool {
423+
matches!(self.segments.first(), Some(PathSegment::Super))
424+
}
425+
426+
/// Check if this path starts with `self::`
427+
pub fn starts_with_self(&self) -> bool {
428+
matches!(self.segments.first(), Some(PathSegment::SelfPath))
429+
}
430+
431+
/// Get the final identifier in the path (e.g., "User" from "crate::models::User")
432+
pub fn final_ident(&self) -> Option<&str> {
433+
self.segments.iter().rev().find_map(|seg| {
434+
if let PathSegment::Ident(name) = seg {
435+
Some(name.as_str())
436+
} else {
437+
None
438+
}
439+
})
440+
}
441+
442+
/// Convert to string representation (e.g., "crate::models::User")
443+
pub fn to_string(&self) -> String {
444+
self.segments
445+
.iter()
446+
.map(|seg| match seg {
447+
PathSegment::Crate => "crate".to_string(),
448+
PathSegment::Super => "super".to_string(),
449+
PathSegment::SelfPath => "self".to_string(),
450+
PathSegment::Ident(name) => name.clone(),
451+
})
452+
.collect::<Vec<_>>()
453+
.join("::")
454+
}
455+
}
456+
457+
impl PathSegment {
458+
/// Check if this segment is an identifier
459+
pub fn is_ident(&self) -> bool {
460+
matches!(self, PathSegment::Ident(_))
461+
}
462+
463+
/// Get the identifier name if this is an Ident segment
464+
pub fn as_ident(&self) -> Option<&str> {
465+
if let PathSegment::Ident(name) = self {
466+
Some(name)
467+
} else {
468+
None
469+
}
470+
}
471+
}
472+
473+
impl Visibility {
474+
/// Check if this is public visibility
475+
pub fn is_public(&self) -> bool {
476+
matches!(self, Visibility::Public)
477+
}
478+
479+
/// Check if this is private visibility
480+
pub fn is_private(&self) -> bool {
481+
matches!(self, Visibility::Private)
482+
}
483+
}
484+
326485
#[cfg(test)]
327486
mod tests {
328487
use super::*;
@@ -331,6 +490,7 @@ mod tests {
331490
fn test_struct_has_attribute() {
332491
let struct_def = StructDef {
333492
name: "User".to_string(),
493+
visibility: Visibility::Private,
334494
attributes: vec![
335495
Attribute {
336496
name: "solana".to_string(),
@@ -383,6 +543,7 @@ mod tests {
383543
fn test_enum_has_attribute() {
384544
let enum_def = EnumDef {
385545
name: "GameState".to_string(),
546+
visibility: Visibility::Private,
386547
attributes: vec![Attribute {
387548
name: "solana".to_string(),
388549
value: None,
@@ -401,6 +562,7 @@ mod tests {
401562
fn test_enum_is_unit_only() {
402563
let unit_enum = EnumDef {
403564
name: "GameState".to_string(),
565+
visibility: Visibility::Private,
404566
attributes: vec![],
405567
variants: vec![
406568
EnumVariant::Unit {
@@ -420,6 +582,7 @@ mod tests {
420582

421583
let mixed_enum = EnumDef {
422584
name: "GameEvent".to_string(),
585+
visibility: Visibility::Private,
423586
attributes: vec![],
424587
variants: vec![
425588
EnumVariant::Unit {
@@ -466,6 +629,7 @@ mod tests {
466629
fn test_item_enum() {
467630
let enum_def = EnumDef {
468631
name: "Status".to_string(),
632+
visibility: Visibility::Private,
469633
attributes: vec![],
470634
variants: vec![],
471635
span: None,

packages/core/src/file_resolver.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,17 @@ impl FileResolver {
177177
let mut defined_types = HashSet::new();
178178
for lumos_file in self.loaded_files.values() {
179179
for item in &lumos_file.items {
180-
let name = match item {
181-
crate::ast::Item::Struct(s) => &s.name,
182-
crate::ast::Item::Enum(e) => &e.name,
183-
crate::ast::Item::TypeAlias(a) => &a.name,
180+
let name_opt = match item {
181+
crate::ast::Item::Struct(s) => Some(&s.name),
182+
crate::ast::Item::Enum(e) => Some(&e.name),
183+
crate::ast::Item::TypeAlias(a) => Some(&a.name),
184+
crate::ast::Item::Module(m) => Some(&m.name),
185+
// Use statements don't define types, skip them
186+
crate::ast::Item::Use(_) => None,
184187
};
185-
defined_types.insert(name.clone());
188+
if let Some(name) = name_opt {
189+
defined_types.insert(name.clone());
190+
}
186191
}
187192
}
188193

packages/core/src/parser.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
3636
use crate::ast::{
3737
Attribute, AttributeValue, EnumDef, EnumVariant, FieldDef, Import, Item as AstItem,
38-
LumosFile, StructDef, TypeAlias, TypeSpec,
38+
LumosFile, StructDef, TypeAlias, TypeSpec, Visibility,
3939
};
4040
use crate::error::{LumosError, Result};
4141
use regex::Regex;
@@ -116,6 +116,25 @@ fn extract_imports(input: &str) -> Result<(Vec<Import>, String)> {
116116
Ok((imports, remaining))
117117
}
118118

119+
/// Parse visibility from syn::Visibility
120+
///
121+
/// Converts `pub` keyword to Visibility::Public, absence to Visibility::Private
122+
///
123+
/// # Arguments
124+
///
125+
/// * `vis` - syn Visibility node
126+
///
127+
/// # Returns
128+
///
129+
/// * `Visibility::Public` if `pub` keyword present
130+
/// * `Visibility::Private` otherwise
131+
fn parse_visibility(vis: &syn::Visibility) -> Visibility {
132+
match vis {
133+
syn::Visibility::Public(_) => Visibility::Public,
134+
_ => Visibility::Private,
135+
}
136+
}
137+
119138
/// Parse a type alias definition
120139
///
121140
/// Converts `type UserId = PublicKey;` into a TypeAlias AST node.
@@ -131,12 +150,14 @@ fn extract_imports(input: &str) -> Result<(Vec<Import>, String)> {
131150
fn parse_type_alias(item: syn::ItemType) -> Result<TypeAlias> {
132151
let name = item.ident.to_string();
133152
let span = Some(item.ident.span());
153+
let visibility = parse_visibility(&item.vis);
134154

135155
// Parse the target type
136156
let (target, _optional) = parse_type(&item.ty)?;
137157

138158
Ok(TypeAlias {
139159
name,
160+
visibility,
140161
target,
141162
span,
142163
})
@@ -242,6 +263,7 @@ pub fn parse_lumos_file(input: &str) -> Result<LumosFile> {
242263
fn parse_struct(item: syn::ItemStruct) -> Result<StructDef> {
243264
let name = item.ident.to_string();
244265
let span = Some(item.ident.span());
266+
let visibility = parse_visibility(&item.vis);
245267

246268
// Extract attributes
247269
let attributes = parse_attributes(&item.attrs)?;
@@ -269,6 +291,7 @@ fn parse_struct(item: syn::ItemStruct) -> Result<StructDef> {
269291

270292
Ok(StructDef {
271293
name,
294+
visibility,
272295
attributes,
273296
fields,
274297
version,
@@ -280,6 +303,7 @@ fn parse_struct(item: syn::ItemStruct) -> Result<StructDef> {
280303
fn parse_enum(item: syn::ItemEnum) -> Result<EnumDef> {
281304
let name = item.ident.to_string();
282305
let span = Some(item.ident.span());
306+
let visibility = parse_visibility(&item.vis);
283307

284308
// Extract attributes
285309
let attributes = parse_attributes(&item.attrs)?;
@@ -303,6 +327,7 @@ fn parse_enum(item: syn::ItemEnum) -> Result<EnumDef> {
303327

304328
Ok(EnumDef {
305329
name,
330+
visibility,
306331
attributes,
307332
variants,
308333
version,

packages/core/src/transform.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ pub fn transform_to_ir(file: LumosFile) -> Result<Vec<TypeDefinition>> {
294294
let type_def = transform_type_alias(alias_def, &alias_resolver)?;
295295
type_defs.push(TypeDefinition::TypeAlias(type_def));
296296
}
297+
// Module and Use statements are not yet transformed in this pass
298+
// They will be handled by ModuleResolver in #53b
299+
AstItem::Module(_) | AstItem::Use(_) => {
300+
// Skip for now - module resolution is not yet implemented
301+
}
297302
}
298303
}
299304

@@ -373,6 +378,11 @@ fn transform_to_ir_with_resolver_impl(
373378
let type_def = transform_type_alias(alias_def, resolver)?;
374379
type_defs.push(TypeDefinition::TypeAlias(type_def));
375380
}
381+
// Module and Use statements are not yet transformed
382+
// They will be handled by ModuleResolver in #53b
383+
AstItem::Module(_) | AstItem::Use(_) => {
384+
// Skip for now - module resolution is not yet implemented
385+
}
376386
}
377387
}
378388

0 commit comments

Comments
 (0)