mod codegen;
mod error_fmt;
mod ir;
mod lexer;
mod parser;
mod syntax;
#[cfg(test)]
mod tests;
use codegen::{Codegen, CodegenConfig};
use parser::Parser;
use proc_macro2::{Span, TokenStream, TokenTree};
use quote::quote;
#[derive(Clone, Copy)]
struct SpanEntry {
line: usize,
col_start: usize,
col_end: usize,
span: Span,
}
struct SpanMap {
entries: Vec<SpanEntry>,
fallback: Span,
}
impl SpanMap {
fn from_token_stream(stream: &TokenStream) -> Self {
let mut entries = Vec::new();
let fallback = stream
.clone()
.into_iter()
.next()
.map(|t| t.span())
.unwrap_or_else(Span::call_site);
Self::collect_spans(stream, &mut entries);
entries.sort_by(|a, b| a.line.cmp(&b.line).then(a.col_start.cmp(&b.col_start)));
Self { entries, fallback }
}
fn collect_spans(stream: &TokenStream, entries: &mut Vec<SpanEntry>) {
for token in stream.clone() {
let span = token.span();
let start = span.start();
let end = span.end();
entries.push(SpanEntry {
line: start.line,
col_start: start.column,
col_end: if end.line == start.line {
end.column
} else {
usize::MAX
},
span,
});
if let TokenTree::Group(group) = token {
let open_span = group.span_open();
let open_start = open_span.start();
entries.push(SpanEntry {
line: open_start.line,
col_start: open_start.column,
col_end: open_start.column + 1,
span: open_span,
});
Self::collect_spans(&group.stream(), entries);
let close_span = group.span_close();
let close_start = close_span.start();
entries.push(SpanEntry {
line: close_start.line,
col_start: close_start.column,
col_end: close_start.column + 1,
span: close_span,
});
}
}
}
fn span_at(&self, line: usize, column: usize) -> Span {
let col = column.saturating_sub(1);
let line_start = self.entries.partition_point(|e| e.line < line);
let line_end = self.entries.partition_point(|e| e.line <= line);
if line_start >= line_end {
if line_start > 0 {
return self.entries[line_start - 1].span;
}
return self.fallback;
}
let line_entries = &self.entries[line_start..line_end];
for entry in line_entries {
if col >= entry.col_start && col < entry.col_end {
return entry.span;
}
}
let mut best = None;
for entry in line_entries {
if entry.col_start <= col {
best = Some(entry.span);
}
}
best.unwrap_or_else(|| {
line_entries
.first()
.map(|e| e.span)
.unwrap_or(self.fallback)
})
}
fn span_for_line(&self, line: usize) -> Span {
self.span_at(line, 1)
}
}
pub fn compile_template_tokens(input: TokenStream) -> syn::Result<TokenStream> {
let parsed = parse_position(input)?;
let span_map = SpanMap::from_token_stream(&parsed.body);
let has_source_text = parsed.source_text.is_some();
let (template_str, line_offset) = if let Some(ref source) = parsed.source_text {
let trimmed = source.trim();
let inner = if trimmed.starts_with('{') && trimmed.ends_with('}') {
trimmed[1..trimmed.len() - 1].to_string()
} else {
trimmed.to_string()
};
(inner, parsed.brace_line.saturating_sub(1))
} else {
let first_token_line = parsed
.body
.clone()
.into_iter()
.next()
.map(|t| t.span().start().line)
.unwrap_or(1);
(parsed.body.to_string(), first_token_line.saturating_sub(1))
};
#[cfg(debug_assertions)]
if std::env::var("MF_DEBUG_LINE").is_ok() {
eprintln!("[MF_DEBUG] Brace line: {}", parsed.brace_line);
eprintln!("[MF_DEBUG] Line offset: {}", line_offset);
eprintln!("[MF_DEBUG] Has source_text: {}", has_source_text);
}
let position = parsed.position;
#[cfg(debug_assertions)]
if std::env::var("MF_DEBUG_TEMPLATE").is_ok() {
eprintln!(
"[MF_DEBUG] Template string ({} chars): {:?}",
template_str.len(),
template_str
);
}
let template_str = if position == Some("Within") {
format!("class __MF_DUMMY__ {{ {} }}", template_str)
} else {
template_str
};
compile_template_with_spans(&template_str, position, line_offset, &span_map)
}
struct ParsedInput {
position: Option<&'static str>,
body: TokenStream,
brace_line: usize,
source_text: Option<String>,
}
fn parse_position(input: TokenStream) -> syn::Result<ParsedInput> {
let mut iter = input.clone().into_iter().peekable();
if let Some(proc_macro2::TokenTree::Ident(ident)) = iter.peek() {
let pos = match ident.to_string().as_str() {
"Top" => Some("Top"),
"Above" => Some("Above"),
"Within" => Some("Within"),
"Below" => Some("Below"),
"Bottom" => Some("Bottom"),
_ => None,
};
if pos.is_some() {
iter.next();
let remaining: TokenStream = iter.collect();
let mut remaining_iter = remaining.into_iter();
if let Some(proc_macro2::TokenTree::Group(group)) = remaining_iter.next()
&& group.delimiter() == proc_macro2::Delimiter::Brace
{
let brace_line = group.span_open().start().line;
let source_text = group.span().source_text();
return Ok(ParsedInput {
position: pos,
body: group.stream(),
brace_line,
source_text,
});
}
return Err(syn::Error::new_spanned(
input,
"expected `{` after position keyword (e.g., `ts_template!(Within { ... })`)",
));
}
}
let (brace_line, source_text) = input
.clone()
.into_iter()
.find_map(|t| {
if let proc_macro2::TokenTree::Group(g) = t {
Some((g.span_open().start().line, g.span().source_text()))
} else {
None
}
})
.unwrap_or((0, None));
Ok(ParsedInput {
position: None,
body: input,
brace_line,
source_text,
})
}
pub fn compile_template(
template: &str,
position: Option<&str>,
line_offset: usize,
) -> syn::Result<TokenStream> {
let dummy_span_map = SpanMap {
entries: Vec::new(),
fallback: Span::call_site(),
};
compile_template_with_spans(template, position, line_offset, &dummy_span_map)
}
fn compile_template_with_spans(
template: &str,
position: Option<&str>,
line_offset: usize,
span_map: &SpanMap,
) -> syn::Result<TokenStream> {
use crate::compiler::parser::SourceLocation;
if template.trim().is_empty() {
let insert_pos = position_to_tokens(position);
return Ok(quote! {
macroforge_ts::ts_syn::TsStream::with_insert_pos(String::new(), #insert_pos)
});
}
let parser = Parser::try_new(template).map_err(|e| {
let loc = SourceLocation::from_offset(template, e.position);
let absolute_line = loc.line + line_offset;
let span = span_map.span_at(absolute_line, loc.column);
syn::Error::new(
span,
e.format_with_source_and_file(template, "template", line_offset),
)
})?;
let ir = parser.parse().map_err(|e| {
let loc = SourceLocation::from_offset(template, e.position);
let absolute_line = loc.line + line_offset;
let span = span_map.span_at(absolute_line, loc.column);
syn::Error::new(
span,
e.format_with_source_and_file(template, "template", line_offset),
)
})?;
let config = CodegenConfig::default();
let stmts_code = Codegen::with_config(config).generate(&ir).map_err(|e| {
let span = if let Some(ir_span) = e.span {
span_map.span_at(
SourceLocation::from_offset(template, ir_span.start).line + line_offset,
SourceLocation::from_offset(template, ir_span.start).column,
)
} else {
span_map.fallback
};
syn::Error::new(
span,
e.format_with_source_and_file(template, "template", line_offset),
)
})?;
let insert_pos = position_to_tokens(position);
let is_within = position == Some("Within");
if is_within {
Ok(quote! {
{
let __stmts: Vec<macroforge_ts::swc_core::ecma::ast::ModuleItem> = #stmts_code;
let __comments = macroforge_ts::swc_core::common::comments::SingleThreadedComments::default();
let __full_source = macroforge_ts::ts_syn::emit_module_items(&__stmts, &__comments);
let __body_source = {
let marker = "class __MF_DUMMY__";
if let Some(pos) = __full_source.find(marker) {
let after = &__full_source[pos + marker.len()..];
let after_trimmed = after.trim_start();
if after_trimmed.starts_with('{') {
let brace_offset = after.len() - after_trimmed.len();
let after_brace = &after[brace_offset + 1..];
if let Some(end) = after_brace.rfind('}') {
after_brace[..end].trim().to_string()
} else {
after_brace.trim().to_string()
}
} else {
after.trim().to_string()
}
} else {
__full_source.clone()
}
};
let __source = format!("/* @macroforge:body */{}", __body_source);
macroforge_ts::ts_syn::TsStream::with_insert_pos(__source, #insert_pos)
}
})
} else {
Ok(quote! {
{
let __stmts: Vec<macroforge_ts::swc_core::ecma::ast::ModuleItem> = #stmts_code;
let __comments = macroforge_ts::swc_core::common::comments::SingleThreadedComments::default();
let __source = macroforge_ts::ts_syn::emit_module_items(&__stmts, &__comments);
macroforge_ts::ts_syn::TsStream::with_insert_pos(__source, #insert_pos)
}
})
}
}
fn position_to_tokens(position: Option<&str>) -> TokenStream {
match position {
Some("Top") => quote! { macroforge_ts::ts_syn::InsertPos::Top },
Some("Above") => quote! { macroforge_ts::ts_syn::InsertPos::Above },
Some("Within") => quote! { macroforge_ts::ts_syn::InsertPos::Within },
Some("Bottom") => quote! { macroforge_ts::ts_syn::InsertPos::Bottom },
_ => quote! { macroforge_ts::ts_syn::InsertPos::Below },
}
}