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)]
|
||||
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>,
|
||||
|
||||
39
src/lexer.rs
39
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<TokenKind, LexError> {
|
||||
let mut value = String::new();
|
||||
loop {
|
||||
|
||||
37
src/main.rs
37
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user