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:
10
src/ast.rs
10
src/ast.rs
@@ -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>,
|
||||||
|
|||||||
35
src/lexer.rs
35
src/lexer.rs
@@ -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 {
|
||||||
|
|||||||
37
src/main.rs
37
src/main.rs
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user