mod actions;
mod config;
mod connections;
mod directives;
mod expressions;
mod instructions;
mod language;
mod primitives;
mod reasoning;
mod system;
#[cfg(not(test))]
mod tests;
mod topics;
mod variables;
use crate::ast::AgentFile;
use crate::lexer;
pub use primitives::Span;
fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
for (i, ch) in source.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
fn get_line_content(source: &str, line_num: usize) -> &str {
source.lines().nth(line_num.saturating_sub(1)).unwrap_or("")
}
fn format_parse_error<'tokens, 'src>(
source: &str,
error: &Rich<'tokens, crate::lexer::Token<'src>, primitives::Span>,
) -> String {
let span = error.span();
let (line, col) = offset_to_line_col(source, span.start);
let line_content = get_line_content(source, line);
let expected: Vec<String> = error.expected().map(|e| format!("{}", e)).collect();
let expected_str = if expected.is_empty() {
String::new()
} else if expected.len() == 1 {
format!(", expected {}", expected[0])
} else {
format!(", expected one of: {}", expected.join(", "))
};
let found_str = match error.found() {
Some(tok) => format!("found '{}'", tok),
None => "found end of input".to_string(),
};
let contexts: Vec<_> = error.contexts().collect();
let context_str = if contexts.is_empty() {
String::new()
} else {
let ctx_labels: Vec<String> = contexts
.iter()
.map(|(label, ctx_span)| {
let (ctx_line, _) = offset_to_line_col(source, ctx_span.start);
format!("{} (line {})", label, ctx_line)
})
.collect();
format!("\n while parsing: {}", ctx_labels.join(" > "))
};
format!(
"Error at line {}, column {}: {}{}{}\n |\n{:>3} | {}\n | {}{}",
line,
col,
found_str,
expected_str,
context_str,
line,
line_content,
" ".repeat(col.saturating_sub(1)),
"^".repeat(
(span.end - span.start)
.max(1)
.min(line_content.len().saturating_sub(col - 1).max(1))
)
)
}
fn format_lexer_error(
source: &str,
error: &impl std::fmt::Debug,
span_start: usize,
span_end: usize,
) -> String {
let (line, col) = offset_to_line_col(source, span_start);
let line_content = get_line_content(source, line);
format!(
"Lexer error at line {}, column {}: {:?}\n |\n{:>3} | {}\n | {}{}",
line,
col,
error,
line,
line_content,
" ".repeat(col.saturating_sub(1)),
"^".repeat(
(span_end - span_start)
.max(1)
.min(line_content.len().saturating_sub(col - 1).max(1))
)
)
}
use primitives::{skip_toplevel_noise, ParserInput};
use chumsky::input::Input as _;
use chumsky::prelude::*;
use chumsky::recovery::skip_then_retry_until;
use config::config_block;
use connections::{connection_block, connections_wrapper_block};
use language::language_block;
use system::system_block;
use topics::{start_agent_block, topic_block};
use variables::variables_block;
pub fn parse(source: &str) -> Result<AgentFile, Vec<String>> {
let (result, errors) = parse_with_errors(source);
if errors.is_empty() {
result.ok_or_else(|| vec!["Unknown parse error".to_string()])
} else {
Err(errors)
}
}
pub fn parse_with_errors(source: &str) -> (Option<AgentFile>, Vec<String>) {
let tokens = match lexer::lex_with_indentation(source) {
Ok(tokens) => tokens,
Err(errs) => {
let errors: Vec<String> = errs
.iter()
.map(|e| {
let span = e.span();
format_lexer_error(source, &e.reason(), span.start, span.end)
})
.collect();
return (None, errors);
}
};
let eoi_span = primitives::Span::new((), source.len()..source.len());
let token_stream = tokens.as_slice().split_token_span(eoi_span);
let (result, errs) = agent_file_parser().parse(token_stream).into_output_errors();
let errors: Vec<String> = errs.iter().map(|e| format_parse_error(source, e)).collect();
(result, errors)
}
pub fn parse_with_structured_errors(
source: &str,
) -> Result<AgentFile, Vec<crate::error::ParseErrorInfo>> {
let (result, errors) = parse_with_structured_errors_all(source);
if errors.is_empty() {
result.ok_or_else(|| {
vec![crate::error::ParseErrorInfo {
message: "Unknown parse error".to_string(),
span: None,
expected: vec![],
found: None,
contexts: vec![],
}]
})
} else {
Err(errors)
}
}
pub fn parse_with_structured_errors_all(
source: &str,
) -> (Option<AgentFile>, Vec<crate::error::ParseErrorInfo>) {
use crate::error::ParseErrorInfo;
let tokens = match lexer::lex_with_indentation(source) {
Ok(tokens) => tokens,
Err(errs) => {
let errors: Vec<ParseErrorInfo> = errs
.iter()
.map(|e| {
let span = e.span();
let (line, col) = offset_to_line_col(source, span.start);
ParseErrorInfo {
message: format!(
"Lexer error at line {}, column {}: {}",
line,
col,
e.reason()
),
span: Some(span.start..span.end),
expected: vec![],
found: None,
contexts: vec![],
}
})
.collect();
return (None, errors);
}
};
let eoi_span = primitives::Span::new((), source.len()..source.len());
let token_stream = tokens.as_slice().split_token_span(eoi_span);
let (result, errs) = agent_file_parser().parse(token_stream).into_output_errors();
let errors: Vec<ParseErrorInfo> = errs
.iter()
.map(|e| {
let span = e.span();
let (line, col) = offset_to_line_col(source, span.start);
let contexts: Vec<(String, std::ops::Range<usize>)> = e
.contexts()
.map(|(label, ctx_span)| (label.to_string(), ctx_span.start..ctx_span.end))
.collect();
ParseErrorInfo {
message: format!("Parse error at line {}, column {}: {}", line, col, e.reason()),
span: Some(span.start..span.end),
expected: e.expected().map(|exp| format!("{}", exp)).collect(),
found: e.found().map(|tok| format!("{}", tok)),
contexts,
}
})
.collect();
(result, errors)
}
use crate::ast::{
ConfigBlock, ConnectionBlock, LanguageBlock, Spanned, StartAgentBlock, SystemBlock, TopicBlock,
VariablesBlock,
};
use crate::lexer::Token;
enum TopLevelBlock {
Config(Spanned<ConfigBlock>),
Variables(Spanned<VariablesBlock>),
System(Spanned<SystemBlock>),
StartAgent(Spanned<StartAgentBlock>),
Topic(Spanned<TopicBlock>),
Language(Spanned<LanguageBlock>),
Connection(Spanned<ConnectionBlock>),
Connections(Vec<Spanned<ConnectionBlock>>),
}
fn agent_file_parser<'tokens, 'src: 'tokens>() -> impl Parser<
'tokens,
ParserInput<'tokens, 'src>,
AgentFile,
extra::Err<Rich<'tokens, Token<'src>, primitives::Span>>,
> + Clone {
let recovery_until = choice((
just(Token::Topic).ignored(),
just(Token::StartAgent).ignored(),
just(Token::Config).ignored(),
just(Token::Variables).ignored(),
just(Token::System).ignored(),
just(Token::Language).ignored(),
just(Token::Connection).ignored(),
just(Token::Connections).ignored(),
));
skip_toplevel_noise()
.ignore_then(choice((
config_block().map(TopLevelBlock::Config),
variables_block().map(TopLevelBlock::Variables),
system_block().map(TopLevelBlock::System),
start_agent_block().map(TopLevelBlock::StartAgent),
topic_block().map(TopLevelBlock::Topic),
language_block().map(TopLevelBlock::Language),
connection_block().map(TopLevelBlock::Connection),
connections_wrapper_block().map(TopLevelBlock::Connections),
)))
.recover_with(skip_then_retry_until(any().ignored(), recovery_until))
.repeated()
.collect::<Vec<_>>()
.then_ignore(skip_toplevel_noise())
.then_ignore(end())
.map(|blocks| {
let mut file = AgentFile::default();
for block in blocks {
match block {
TopLevelBlock::Config(c) => file.config = Some(c),
TopLevelBlock::Variables(v) => file.variables = Some(v),
TopLevelBlock::System(s) => file.system = Some(s),
TopLevelBlock::StartAgent(sa) => file.start_agent = Some(sa),
TopLevelBlock::Topic(t) => file.topics.push(t),
TopLevelBlock::Language(l) => file.language = Some(l),
TopLevelBlock::Connection(c) => file.connections.push(c),
TopLevelBlock::Connections(cs) => file.connections.extend(cs),
}
}
file
})
}