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:
+80
-13
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user