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:
@@ -0,0 +1,258 @@
|
||||
use crate::ast::nodes::{NodeKind, SyntaxNode};
|
||||
use crate::ast::types::{CommentLine, Value};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Emits a [`SyntaxNode`] back to Myc source code, preserving leading comments.
|
||||
///
|
||||
/// Round-trip guarantee: `emit(parse(emit(parse(source)))) == emit(parse(source))`.
|
||||
/// In other words, the emitter is idempotent after the first pass.
|
||||
pub fn emit(node: &SyntaxNode) -> String {
|
||||
let mut out = String::new();
|
||||
// The top-level node is always at block level (owns its own line).
|
||||
emit_block(node, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
// ── Block-level emission ────────────────────────────────────────────────────
|
||||
|
||||
/// Emits a node in **block-level** context: writes leading comments and
|
||||
/// the appropriate indentation prefix, then the code.
|
||||
///
|
||||
/// Callers must only write a `\n` separator before calling this — never `\n + pad`.
|
||||
fn emit_block(node: &SyntaxNode, indent: usize, out: &mut String) {
|
||||
emit_comments(&node.comments, indent, out);
|
||||
out.push_str(&" ".repeat(indent));
|
||||
emit_kind(node, indent, out);
|
||||
}
|
||||
|
||||
/// Emits a node in **inline** context: no indentation, no comments.
|
||||
/// Used for sub-expressions that are part of a larger expression on the same line.
|
||||
fn emit_inline(node: &SyntaxNode, indent: usize, out: &mut String) {
|
||||
emit_kind(node, indent, out);
|
||||
}
|
||||
|
||||
// ── Comment emission ────────────────────────────────────────────────────────
|
||||
|
||||
fn emit_comments(comments: &Rc<[CommentLine]>, indent: usize, out: &mut String) {
|
||||
if comments.is_empty() {
|
||||
return;
|
||||
}
|
||||
let pad = " ".repeat(indent);
|
||||
for line in comments.iter() {
|
||||
match line {
|
||||
CommentLine::Comment(text) => {
|
||||
out.push_str(&pad);
|
||||
out.push(';');
|
||||
if !text.is_empty() {
|
||||
out.push(' ');
|
||||
out.push_str(text);
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
CommentLine::Doc(text) => {
|
||||
out.push_str(&pad);
|
||||
out.push_str(";;");
|
||||
if !text.is_empty() {
|
||||
out.push(' ');
|
||||
out.push_str(text);
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
CommentLine::Blank => {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kind emission ───────────────────────────────────────────────────────────
|
||||
|
||||
fn emit_kind(node: &SyntaxNode, indent: usize, out: &mut String) {
|
||||
match &node.kind {
|
||||
NodeKind::Nop => out.push_str("..."),
|
||||
|
||||
NodeKind::Constant(val) => emit_value(val, out),
|
||||
|
||||
NodeKind::Identifier { symbol, .. } => out.push_str(&symbol.name),
|
||||
|
||||
NodeKind::FieldAccessor(k) => {
|
||||
out.push('.');
|
||||
out.push_str(&k.name());
|
||||
}
|
||||
|
||||
NodeKind::If { cond, then_br, else_br } => {
|
||||
out.push_str("(if ");
|
||||
emit_inline(cond, indent + 1, out);
|
||||
out.push('\n');
|
||||
emit_block(then_br, indent + 1, out);
|
||||
if let Some(else_node) = else_br {
|
||||
out.push('\n');
|
||||
emit_block(else_node, indent + 1, out);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Def { pattern, value, .. } => {
|
||||
out.push_str("(def ");
|
||||
emit_inline(pattern, indent, out);
|
||||
out.push('\n');
|
||||
emit_block(value, indent + 1, out);
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Assign { target, value, .. } => {
|
||||
out.push_str("(assign ");
|
||||
emit_inline(target, indent, out);
|
||||
out.push(' ');
|
||||
emit_inline(value, indent + 1, out);
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Lambda { params, body, .. } => {
|
||||
out.push_str("(fn ");
|
||||
emit_inline(params, indent, out);
|
||||
out.push('\n');
|
||||
emit_block(body, indent + 1, out);
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Call { callee, args } => {
|
||||
out.push('(');
|
||||
emit_inline(callee, indent, out);
|
||||
if let NodeKind::Tuple { elements } = &args.kind {
|
||||
for arg in elements {
|
||||
out.push(' ');
|
||||
emit_inline(arg, indent + 1, out);
|
||||
}
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Again { args } => {
|
||||
out.push_str("(again");
|
||||
if let NodeKind::Tuple { elements } = &args.kind {
|
||||
for arg in elements {
|
||||
out.push(' ');
|
||||
emit_inline(arg, indent + 1, out);
|
||||
}
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Block { exprs } => {
|
||||
out.push_str("(do");
|
||||
for expr in exprs {
|
||||
out.push('\n');
|
||||
emit_block(expr, indent + 1, out);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Tuple { elements } => {
|
||||
out.push('[');
|
||||
for (i, el) in elements.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(' ');
|
||||
}
|
||||
emit_inline(el, indent, out);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
|
||||
NodeKind::Record { fields, .. } => {
|
||||
if fields.is_empty() {
|
||||
out.push_str("{}");
|
||||
return;
|
||||
}
|
||||
// Always multi-line so that any leading comments on keys are valid.
|
||||
out.push('{');
|
||||
for (key, val) in fields.iter() {
|
||||
out.push('\n');
|
||||
emit_block(key, indent + 1, out);
|
||||
out.push(' ');
|
||||
emit_inline(val, indent + 1, out);
|
||||
}
|
||||
out.push('\n');
|
||||
out.push_str(&" ".repeat(indent));
|
||||
out.push('}');
|
||||
}
|
||||
|
||||
NodeKind::MacroDecl { name, params, body } => {
|
||||
out.push_str("(macro ");
|
||||
out.push_str(&name.name);
|
||||
out.push(' ');
|
||||
emit_inline(params, indent, out);
|
||||
out.push('\n');
|
||||
emit_block(body, indent + 1, out);
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Template(inner) => {
|
||||
out.push('`');
|
||||
emit_inline(inner, indent, out);
|
||||
}
|
||||
|
||||
NodeKind::Placeholder(inner) => {
|
||||
out.push('~');
|
||||
emit_inline(inner, indent, out);
|
||||
}
|
||||
|
||||
NodeKind::Splice(inner) => {
|
||||
out.push_str("~@");
|
||||
emit_inline(inner, indent, out);
|
||||
}
|
||||
|
||||
NodeKind::GetField { rec, field } => {
|
||||
out.push_str("(.");
|
||||
out.push_str(&field.name());
|
||||
out.push(' ');
|
||||
emit_inline(rec, indent, out);
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
NodeKind::Expansion { original_call, .. } => {
|
||||
// Round-trip uses the original pre-expansion form.
|
||||
emit_inline(original_call, indent, out);
|
||||
}
|
||||
|
||||
NodeKind::Error | NodeKind::Extension(_) => {
|
||||
out.push_str("<error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_value(val: &Value, out: &mut String) {
|
||||
match val {
|
||||
Value::Int(n) => out.push_str(&n.to_string()),
|
||||
Value::Float(f) => {
|
||||
let s = format!("{}", f);
|
||||
// Ensure a decimal point is present so it round-trips as a float.
|
||||
if s.contains('.') || s.contains('e') {
|
||||
out.push_str(&s);
|
||||
} else {
|
||||
out.push_str(&s);
|
||||
out.push_str(".0");
|
||||
}
|
||||
}
|
||||
Value::Text(t) => {
|
||||
out.push('"');
|
||||
for c in t.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
Value::Keyword(k) => {
|
||||
out.push(':');
|
||||
out.push_str(&k.name());
|
||||
}
|
||||
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
||||
other => out.push_str(&other.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -624,7 +624,7 @@ mod tests {
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::nodes::{Address, VirtualId};
|
||||
use crate::ast::parser::Parser;
|
||||
use crate::ast::types::{NodeIdentity, Object, Purity, SourceLocation, StaticType, Value};
|
||||
use crate::ast::types::{NodeIdentity, Purity, SourceLocation, StaticType, Value};
|
||||
|
||||
struct SimpleEvaluator;
|
||||
impl MacroEvaluator for SimpleEvaluator {
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod analyzer;
|
||||
pub mod binder;
|
||||
pub mod captures;
|
||||
pub mod dumper;
|
||||
pub mod emitter;
|
||||
pub mod lambda_collector;
|
||||
pub mod macros;
|
||||
pub mod optimizer;
|
||||
|
||||
Reference in New Issue
Block a user