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 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 05:08:47 -05:00
parent 6f860a435b
commit 3206aad653
4 changed files with 118 additions and 13 deletions

View File

@@ -227,6 +227,8 @@ pub enum Declaration {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FunctionDecl { pub struct FunctionDecl {
pub visibility: Visibility, pub visibility: Visibility,
/// Documentation comment (from /// doc comments)
pub doc: Option<String>,
pub name: Ident, pub name: Ident,
pub type_params: Vec<Ident>, pub type_params: Vec<Ident>,
pub params: Vec<Parameter>, pub params: Vec<Parameter>,
@@ -251,6 +253,8 @@ pub struct Parameter {
/// Effect declaration /// Effect declaration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EffectDecl { pub struct EffectDecl {
/// Documentation comment
pub doc: Option<String>,
pub name: Ident, pub name: Ident,
pub type_params: Vec<Ident>, pub type_params: Vec<Ident>,
pub operations: Vec<EffectOp>, pub operations: Vec<EffectOp>,
@@ -270,6 +274,8 @@ pub struct EffectOp {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TypeDecl { pub struct TypeDecl {
pub visibility: Visibility, pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident, pub name: Ident,
pub type_params: Vec<Ident>, pub type_params: Vec<Ident>,
/// Optional version annotation: type User @v2 { ... } /// Optional version annotation: type User @v2 { ... }
@@ -342,6 +348,8 @@ pub struct HandlerImpl {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LetDecl { pub struct LetDecl {
pub visibility: Visibility, pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident, pub name: Ident,
pub typ: Option<TypeExpr>, pub typ: Option<TypeExpr>,
pub value: Expr, pub value: Expr,
@@ -352,6 +360,8 @@ pub struct LetDecl {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TraitDecl { pub struct TraitDecl {
pub visibility: Visibility, pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident, pub name: Ident,
/// Type parameters: trait Functor<F> { ... } /// Type parameters: trait Functor<F> { ... }
pub type_params: Vec<Ident>, pub type_params: Vec<Ident>,

View File

@@ -44,6 +44,9 @@ pub enum TokenKind {
Impl, // impl (for trait implementations) Impl, // impl (for trait implementations)
For, // for (in impl Trait for Type) For, // for (in impl Trait for Type)
// Documentation
DocComment(String), // /// doc comment
// Behavioral type keywords // Behavioral type keywords
Is, // is (for behavioral properties) Is, // is (for behavioral properties)
Pure, // pure Pure, // pure
@@ -124,6 +127,7 @@ impl fmt::Display for TokenKind {
TokenKind::Trait => write!(f, "trait"), TokenKind::Trait => write!(f, "trait"),
TokenKind::Impl => write!(f, "impl"), TokenKind::Impl => write!(f, "impl"),
TokenKind::For => write!(f, "for"), TokenKind::For => write!(f, "for"),
TokenKind::DocComment(s) => write!(f, "/// {}", s),
TokenKind::Is => write!(f, "is"), TokenKind::Is => write!(f, "is"),
TokenKind::Pure => write!(f, "pure"), TokenKind::Pure => write!(f, "pure"),
TokenKind::Total => write!(f, "total"), TokenKind::Total => write!(f, "total"),
@@ -268,9 +272,16 @@ impl<'a> Lexer<'a> {
} }
'/' => { '/' => {
if self.peek() == Some('/') { if self.peek() == Some('/') {
// Line comment 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(); self.skip_line_comment();
return self.next_token(); return self.next_token();
}
} else { } else {
TokenKind::Slash 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<TokenKind, LexError> { fn scan_string(&mut self, _start: usize) -> Result<TokenKind, LexError> {
let mut value = String::new(); let mut value = String::new();
loop { loop {

View File

@@ -1419,5 +1419,42 @@ c")"#;
let result = eval(source); let result = eval(source);
assert!(result.is_ok(), "Expected success with explicit effects but got: {:?}", result); 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");
}
} }
} }

View File

@@ -201,6 +201,9 @@ impl Parser {
fn parse_declaration(&mut self) -> Result<Declaration, ParseError> { fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
self.skip_newlines(); self.skip_newlines();
// Collect any doc comments before the declaration
let doc = self.collect_doc_comments();
// Check for visibility modifier // Check for visibility modifier
let visibility = if self.check(TokenKind::Pub) { let visibility = if self.check(TokenKind::Pub) {
self.advance(); self.advance();
@@ -210,19 +213,36 @@ impl Parser {
}; };
match self.peek_kind() { match self.peek_kind() {
TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility)?)), TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)),
TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl()?)), TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)),
TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)), TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)),
TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility)?)), TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)),
TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility)?)), TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility, doc)?)),
TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility)?)), TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility, doc)?)),
TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)), TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)),
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")), _ => 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<String> {
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 /// Parse a function declaration
fn parse_function_decl(&mut self, visibility: Visibility) -> Result<FunctionDecl, ParseError> { fn parse_function_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<FunctionDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::Fn)?; self.expect(TokenKind::Fn)?;
@@ -264,6 +284,7 @@ impl Parser {
let span = start.merge(body.span()); let span = start.merge(body.span());
Ok(FunctionDecl { Ok(FunctionDecl {
visibility, visibility,
doc,
name, name,
type_params, type_params,
params, params,
@@ -277,7 +298,7 @@ impl Parser {
} }
/// Parse effect declaration /// Parse effect declaration
fn parse_effect_decl(&mut self) -> Result<EffectDecl, ParseError> { fn parse_effect_decl(&mut self, doc: Option<String>) -> Result<EffectDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::Effect)?; self.expect(TokenKind::Effect)?;
@@ -307,6 +328,7 @@ impl Parser {
self.expect(TokenKind::RBrace)?; self.expect(TokenKind::RBrace)?;
Ok(EffectDecl { Ok(EffectDecl {
doc,
name, name,
type_params, type_params,
operations, operations,
@@ -418,7 +440,7 @@ impl Parser {
} }
/// Parse type declaration /// Parse type declaration
fn parse_type_decl(&mut self, visibility: Visibility) -> Result<TypeDecl, ParseError> { fn parse_type_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<TypeDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::Type)?; self.expect(TokenKind::Type)?;
@@ -468,6 +490,7 @@ impl Parser {
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
Ok(TypeDecl { Ok(TypeDecl {
visibility, visibility,
doc,
name, name,
type_params, type_params,
version, version,
@@ -478,7 +501,7 @@ impl Parser {
} }
/// Parse let declaration /// Parse let declaration
fn parse_let_decl(&mut self, visibility: Visibility) -> Result<LetDecl, ParseError> { fn parse_let_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<LetDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::Let)?; self.expect(TokenKind::Let)?;
@@ -498,6 +521,7 @@ impl Parser {
let span = start.merge(value.span()); let span = start.merge(value.span());
Ok(LetDecl { Ok(LetDecl {
visibility, visibility,
doc,
name, name,
typ, typ,
value, value,
@@ -506,7 +530,7 @@ impl Parser {
} }
/// Parse trait declaration: trait Show { fn show(self): String } /// Parse trait declaration: trait Show { fn show(self): String }
fn parse_trait_decl(&mut self, visibility: Visibility) -> Result<TraitDecl, ParseError> { fn parse_trait_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<TraitDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::Trait)?; self.expect(TokenKind::Trait)?;
@@ -545,6 +569,7 @@ impl Parser {
Ok(TraitDecl { Ok(TraitDecl {
visibility, visibility,
doc,
name, name,
type_params, type_params,
super_traits, super_traits,