From 3206aad65315e7201188702e1c9acb8966410c82 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 05:08:47 -0500 Subject: [PATCH] feat: implement documentation comments Add support for doc comments (/// syntax) that can be attached to declarations for documentation purposes. The implementation: - Adds DocComment token kind to lexer - Recognizes /// as doc comment syntax (distinct from // regular comments) - Parses consecutive doc comments and combines them into a single string - Adds doc field to FunctionDecl, TypeDecl, LetDecl, EffectDecl, TraitDecl - Passes doc comments through parser to declarations - Multiple consecutive doc comment lines are joined with newlines This enables documentation extraction and could be used for generating API docs, IDE hover information, and REPL help. Co-Authored-By: Claude Opus 4.5 --- src/ast.rs | 10 ++++++++++ src/lexer.rs | 39 ++++++++++++++++++++++++++++++++++++--- src/main.rs | 37 +++++++++++++++++++++++++++++++++++++ src/parser.rs | 45 +++++++++++++++++++++++++++++++++++---------- 4 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 63af786..d1e0146 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -227,6 +227,8 @@ pub enum Declaration { #[derive(Debug, Clone)] pub struct FunctionDecl { pub visibility: Visibility, + /// Documentation comment (from /// doc comments) + pub doc: Option, pub name: Ident, pub type_params: Vec, pub params: Vec, @@ -251,6 +253,8 @@ pub struct Parameter { /// Effect declaration #[derive(Debug, Clone)] pub struct EffectDecl { + /// Documentation comment + pub doc: Option, pub name: Ident, pub type_params: Vec, pub operations: Vec, @@ -270,6 +274,8 @@ pub struct EffectOp { #[derive(Debug, Clone)] pub struct TypeDecl { pub visibility: Visibility, + /// Documentation comment + pub doc: Option, pub name: Ident, pub type_params: Vec, /// Optional version annotation: type User @v2 { ... } @@ -342,6 +348,8 @@ pub struct HandlerImpl { #[derive(Debug, Clone)] pub struct LetDecl { pub visibility: Visibility, + /// Documentation comment + pub doc: Option, pub name: Ident, pub typ: Option, pub value: Expr, @@ -352,6 +360,8 @@ pub struct LetDecl { #[derive(Debug, Clone)] pub struct TraitDecl { pub visibility: Visibility, + /// Documentation comment + pub doc: Option, pub name: Ident, /// Type parameters: trait Functor { ... } pub type_params: Vec, diff --git a/src/lexer.rs b/src/lexer.rs index d8ed7d4..48952b2 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -44,6 +44,9 @@ pub enum TokenKind { Impl, // impl (for trait implementations) For, // for (in impl Trait for Type) + // Documentation + DocComment(String), // /// doc comment + // Behavioral type keywords Is, // is (for behavioral properties) Pure, // pure @@ -124,6 +127,7 @@ impl fmt::Display for TokenKind { TokenKind::Trait => write!(f, "trait"), TokenKind::Impl => write!(f, "impl"), TokenKind::For => write!(f, "for"), + TokenKind::DocComment(s) => write!(f, "/// {}", s), TokenKind::Is => write!(f, "is"), TokenKind::Pure => write!(f, "pure"), TokenKind::Total => write!(f, "total"), @@ -268,9 +272,16 @@ impl<'a> Lexer<'a> { } '/' => { if self.peek() == Some('/') { - // Line comment - self.skip_line_comment(); - return self.next_token(); + self.advance(); // consume second '/' + // Check if this is a doc comment (///) + if self.peek() == Some('/') { + self.advance(); // consume third '/' + return Ok(self.scan_doc_comment(start)); + } else { + // Regular line comment + self.skip_line_comment(); + return self.next_token(); + } } else { TokenKind::Slash } @@ -411,6 +422,28 @@ impl<'a> Lexer<'a> { } } + fn scan_doc_comment(&mut self, start: usize) -> Token { + // Skip leading whitespace after /// + while self.peek() == Some(' ') || self.peek() == Some('\t') { + self.advance(); + } + + // Collect the rest of the line + let mut content = String::new(); + while let Some(c) = self.peek() { + if c == '\n' { + break; + } + content.push(c); + self.advance(); + } + + Token::new( + TokenKind::DocComment(content.trim_end().to_string()), + Span::new(start, self.pos), + ) + } + fn scan_string(&mut self, _start: usize) -> Result { let mut value = String::new(); loop { diff --git a/src/main.rs b/src/main.rs index 80517e1..9e6b40c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1419,5 +1419,42 @@ c")"#; let result = eval(source); assert!(result.is_ok(), "Expected success with explicit effects but got: {:?}", result); } + + #[test] + fn test_doc_comments_on_function() { + // Test that doc comments are parsed and attached to functions + let source = r#" + /// Adds two numbers together. + /// Returns the sum. + fn add(a: Int, b: Int): Int = a + b + let result = add(1, 2) + "#; + assert_eq!(eval(source).unwrap(), "3"); + } + + #[test] + fn test_doc_comments_on_type() { + // Test that doc comments are parsed and attached to types + let source = r#" + /// A point in 2D space. + type Point { x: Int, y: Int } + let p = { x: 1, y: 2 } + let result = p.x + p.y + "#; + assert_eq!(eval(source).unwrap(), "3"); + } + + #[test] + fn test_doc_comments_multiline() { + // Test that multiple doc comment lines are combined + let source = r#" + /// First line of documentation. + /// Second line of documentation. + /// Third line of documentation. + fn documented(): Int = 42 + let result = documented() + "#; + assert_eq!(eval(source).unwrap(), "42"); + } } } diff --git a/src/parser.rs b/src/parser.rs index 075b1a8..716dcc8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -201,6 +201,9 @@ impl Parser { fn parse_declaration(&mut self) -> Result { self.skip_newlines(); + // Collect any doc comments before the declaration + let doc = self.collect_doc_comments(); + // Check for visibility modifier let visibility = if self.check(TokenKind::Pub) { self.advance(); @@ -210,19 +213,36 @@ impl Parser { }; match self.peek_kind() { - TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility)?)), - TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl()?)), + TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)), + TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)), TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)), - TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility)?)), - TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility)?)), - TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility)?)), + TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)), + TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility, doc)?)), + TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility, doc)?)), TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)), _ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")), } } + /// Collect consecutive doc comments into a single string + fn collect_doc_comments(&mut self) -> Option { + let mut docs = Vec::new(); + + while let TokenKind::DocComment(content) = self.peek_kind() { + docs.push(content.clone()); + self.advance(); + self.skip_newlines(); + } + + if docs.is_empty() { + None + } else { + Some(docs.join("\n")) + } + } + /// Parse a function declaration - fn parse_function_decl(&mut self, visibility: Visibility) -> Result { + fn parse_function_decl(&mut self, visibility: Visibility, doc: Option) -> Result { let start = self.current_span(); self.expect(TokenKind::Fn)?; @@ -264,6 +284,7 @@ impl Parser { let span = start.merge(body.span()); Ok(FunctionDecl { visibility, + doc, name, type_params, params, @@ -277,7 +298,7 @@ impl Parser { } /// Parse effect declaration - fn parse_effect_decl(&mut self) -> Result { + fn parse_effect_decl(&mut self, doc: Option) -> Result { let start = self.current_span(); self.expect(TokenKind::Effect)?; @@ -307,6 +328,7 @@ impl Parser { self.expect(TokenKind::RBrace)?; Ok(EffectDecl { + doc, name, type_params, operations, @@ -418,7 +440,7 @@ impl Parser { } /// Parse type declaration - fn parse_type_decl(&mut self, visibility: Visibility) -> Result { + fn parse_type_decl(&mut self, visibility: Visibility, doc: Option) -> Result { let start = self.current_span(); self.expect(TokenKind::Type)?; @@ -468,6 +490,7 @@ impl Parser { let span = start.merge(self.previous_span()); Ok(TypeDecl { visibility, + doc, name, type_params, version, @@ -478,7 +501,7 @@ impl Parser { } /// Parse let declaration - fn parse_let_decl(&mut self, visibility: Visibility) -> Result { + fn parse_let_decl(&mut self, visibility: Visibility, doc: Option) -> Result { let start = self.current_span(); self.expect(TokenKind::Let)?; @@ -498,6 +521,7 @@ impl Parser { let span = start.merge(value.span()); Ok(LetDecl { visibility, + doc, name, typ, value, @@ -506,7 +530,7 @@ impl Parser { } /// Parse trait declaration: trait Show { fn show(self): String } - fn parse_trait_decl(&mut self, visibility: Visibility) -> Result { + fn parse_trait_decl(&mut self, visibility: Visibility, doc: Option) -> Result { let start = self.current_span(); self.expect(TokenKind::Trait)?; @@ -545,6 +569,7 @@ impl Parser { Ok(TraitDecl { visibility, + doc, name, type_params, super_traits,