feat: Add AST emitter and comment support

Introduces a new AST emitter module responsible for serializing AST
nodes back into Myc source code. This emitter preserves leading comments
and whitespace, enabling a round-trip guarantee for the parser and
emitter.

The documentation in `docs/BNF.md` has been updated to reflect the new
syntax rules for comments, including single-line comments (`;`) and
documentation comments (`;;`). The rules clarify that comments must be
whole lines and that inline comments are disallowed.

The lexer and AST node structures have been updated to accommodate
`CommentLine` variants, allowing for different types of comments
(regular, documentation, and blank lines) to be stored and processed.

A new test suite `tests/comment_roundtrip.rs` has been added to verify
the round-trip property of the parser and emitter across all example
`.myc` files. This test ensures that parsing source code, emitting it,
and then parsing the emitted code again results in the same output
string.

The `ast.rs` binary now includes a `--emit` flag that uses the new
emitter to output the parsed source code, facilitating debugging and
testing of the emitter itself.
This commit is contained in:
2026-03-25 19:15:40 +01:00
parent ee89d2330d
commit e757b453a4
10 changed files with 501 additions and 32 deletions
+80 -13
View File
@@ -22,7 +22,7 @@ use crate::ast::compiler::optimizer::Optimizer;
use crate::ast::compiler::specializer::{FunctionRegistry, MonoCache, RtlLookupFunc, Specializer};
use crate::ast::rtl::{self, intrinsics};
use crate::ast::types::{
NativeFunction, NodeIdentity, Purity, SourceLocation, StaticType, Value,
CommentLine, NativeFunction, NodeIdentity, Purity, SourceLocation, StaticType, Value,
};
use crate::ast::diagnostics::{DiagnosticLevel, Diagnostics};
@@ -148,6 +148,9 @@ pub struct Environment {
pub search_paths: Rc<RefCell<Vec<PathBuf>>>,
pub loaded_modules: Rc<RefCell<HashSet<PathBuf>>>,
pub rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
/// Documentation for symbols defined in Myc source (via `;;` comments before `def`/`macro`).
/// Key: symbol name. Value: concatenated doc lines joined by newlines.
pub myc_docs: Rc<RefCell<HashMap<String, String>>>,
}
struct EnvFunctionRegistry {
@@ -235,6 +238,7 @@ impl Environment {
search_paths: Rc::new(RefCell::new(Vec::new())),
loaded_modules: Rc::new(RefCell::new(HashSet::new())),
rtl_docs: Rc::new(RefCell::new(Vec::new())),
myc_docs: Rc::new(RefCell::new(HashMap::new())),
};
rtl::register(&env);
@@ -561,7 +565,9 @@ impl Environment {
comments: Rc::from([]),
}
};
Some(checker.check(&wrapped_ast, &[], diagnostics))
let typed = checker.check(&wrapped_ast, &[], diagnostics);
self.collect_doc_comments(&typed);
Some(typed)
}
fn compile_syntax(&self, syntax_ast: SyntaxNode) -> Result<TypedNode, String> {
@@ -719,6 +725,17 @@ impl Environment {
out
}).collect();
// Include Myc-defined symbol docs (from `;;` comments in source)
let myc_docs = self.myc_docs.borrow();
for (name, doc_text) in myc_docs.iter() {
let sig = scope
.locals
.get(&Symbol::from(name.as_str()))
.map(|info| info._ty.to_doc_string())
.unwrap_or_else(|| "unknown".to_string());
entries.push(format!("{} : {}\n {}", name, sig, doc_text));
}
entries.sort_by_key(|a| a.to_lowercase());
entries
}
@@ -727,9 +744,6 @@ impl Environment {
/// or `None` if the symbol has no doc entry.
pub fn get_rtl_doc(&self, name: &str) -> Option<String> {
use crate::ast::nodes::Symbol;
let docs = self.rtl_docs.borrow();
let entry = docs.iter().find(|e| e.name == name)?;
let root_scopes = self.root_scopes.borrow();
let sig = root_scopes[0]
.locals
@@ -737,17 +751,70 @@ impl Environment {
.map(|info| info._ty.to_doc_string())
.unwrap_or_else(|| "unknown".to_string());
let mut out = format!("{} : {}\n {}", entry.name, sig, entry.one_liner);
if let Some(desc) = entry.description {
out.push_str(&format!("\n {}", desc));
// Check RTL docs first
let docs = self.rtl_docs.borrow();
if let Some(entry) = docs.iter().find(|e| e.name == name) {
let mut out = format!("{} : {}\n {}", entry.name, sig, entry.one_liner);
if let Some(desc) = entry.description {
out.push_str(&format!("\n {}", desc));
}
if let Some(examples) = entry.examples {
out.push_str("\n Examples:");
for ex in examples.iter() {
out.push_str(&format!("\n {}", ex));
}
}
return Some(out);
}
if let Some(examples) = entry.examples {
out.push_str("\n Examples:");
for ex in examples.iter() {
out.push_str(&format!("\n {}", ex));
// Fall back to Myc-source docs
let myc_docs = self.myc_docs.borrow();
myc_docs
.get(name)
.map(|doc_text| format!("{} : {}\n {}", name, sig, doc_text))
}
/// Walks a typed AST and registers `;;` doc comments on `def`/`macro` nodes
/// whose target is a plain identifier into the `myc_docs` registry.
fn collect_doc_comments(&self, node: &TypedNode) {
// Extract the symbol name this node defines, if any.
let sym_name: Option<Rc<str>> = match &node.kind {
NodeKind::Def { pattern, .. } => {
if let NodeKind::Identifier { symbol, .. } = &pattern.kind {
Some(symbol.name.clone())
} else {
None
}
}
NodeKind::MacroDecl { name, .. } => Some(name.name.clone()),
NodeKind::Block { exprs } => {
for expr in exprs {
self.collect_doc_comments(expr);
}
return;
}
NodeKind::Lambda { body, .. } => {
self.collect_doc_comments(body);
return;
}
_ => None,
};
if let Some(name) = sym_name {
let doc_text: String = node
.comments
.iter()
.filter_map(|line| match line {
CommentLine::Doc(text) => Some(text.as_ref()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
if !doc_text.is_empty() {
self.myc_docs.borrow_mut().insert(name.to_string(), doc_text);
}
}
Some(out)
}
pub fn dump_ast(&self, source: &str) -> Result<String, String> {