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)]
pub struct FunctionDecl {
pub visibility: Visibility,
/// Documentation comment (from /// doc comments)
pub doc: Option<String>,
pub name: Ident,
pub type_params: Vec<Ident>,
pub params: Vec<Parameter>,
@@ -251,6 +253,8 @@ pub struct Parameter {
/// Effect declaration
#[derive(Debug, Clone)]
pub struct EffectDecl {
/// Documentation comment
pub doc: Option<String>,
pub name: Ident,
pub type_params: Vec<Ident>,
pub operations: Vec<EffectOp>,
@@ -270,6 +274,8 @@ pub struct EffectOp {
#[derive(Debug, Clone)]
pub struct TypeDecl {
pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident,
pub type_params: Vec<Ident>,
/// 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<String>,
pub name: Ident,
pub typ: Option<TypeExpr>,
pub value: Expr,
@@ -352,6 +360,8 @@ pub struct LetDecl {
#[derive(Debug, Clone)]
pub struct TraitDecl {
pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident,
/// Type parameters: trait Functor<F> { ... }
pub type_params: Vec<Ident>,

View File

@@ -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.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<TokenKind, LexError> {
let mut value = String::new();
loop {

View File

@@ -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");
}
}
}

View File

@@ -201,6 +201,9 @@ impl Parser {
fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
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<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
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();
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<EffectDecl, ParseError> {
fn parse_effect_decl(&mut self, doc: Option<String>) -> Result<EffectDecl, ParseError> {
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<TypeDecl, ParseError> {
fn parse_type_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<TypeDecl, ParseError> {
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<LetDecl, ParseError> {
fn parse_let_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<LetDecl, ParseError> {
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<TraitDecl, ParseError> {
fn parse_trait_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<TraitDecl, ParseError> {
let start = self.current_span();
self.expect(TokenKind::Trait)?;
@@ -545,6 +569,7 @@ impl Parser {
Ok(TraitDecl {
visibility,
doc,
name,
type_params,
super_traits,