mod cst_parser;
mod cst_test_helpers;
pub mod green;
mod lower;
mod preparser;
mod red;
pub mod token;
pub mod tokenizer;
use crate::utils::error::{ReportableError, SimpleError};
use crate::utils::metadata::Location;
pub use cst_parser::{ParserError, parse_cst};
pub use green::{GreenNodeArena, GreenNodeId, SyntaxKind};
pub use lower::{add_global_context, parse_program, parse_to_expr};
pub use preparser::{PreParsedTokens, preparse};
pub use red::{AstNode, RedNode, red_to_ast};
pub use token::{Token, TokenKind};
pub use tokenizer::tokenize;
pub fn green_to_red(green_id: GreenNodeId, offset: usize) -> std::sync::Arc<RedNode> {
RedNode::new(green_id, offset)
}
pub fn parse(
source: &str,
) -> (
AstNode,
Vec<Token>,
PreParsedTokens,
GreenNodeArena,
Vec<cst_parser::ParserError>,
) {
let tokens = tokenize(source);
let preparsed = preparse(&tokens);
let (green_id, arena, tokens, errors) = parse_cst(tokens, &preparsed);
let red = green_to_red(green_id, 0);
let ast = red_to_ast(&red, source, &tokens, &arena);
(ast, tokens, preparsed, arena, errors)
}
pub fn parser_errors_to_reportable(
source: &str,
file_path: std::path::PathBuf,
errors: Vec<cst_parser::ParserError>,
) -> Vec<Box<dyn ReportableError>> {
let tokens = tokenize(source);
let fallback_span = tokens.last().map(|t| t.start..t.end()).unwrap_or(0..0);
errors
.into_iter()
.map(|err| {
let span = tokens
.get(err.token_index)
.map(|t| t.start..t.end())
.unwrap_or_else(|| fallback_span.clone());
Box::new(SimpleError {
message: format!("Parse error: {err}"),
span: Location {
span,
path: file_path.clone(),
},
}) as Box<dyn ReportableError>
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_full_pipeline() {
let source = "fn dsp() { 42 }";
let (ast, tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { statements } => {
assert!(!statements.is_empty());
}
_ => panic!("Expected Program node"),
}
assert!(!tokens.is_empty());
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
}
#[test]
fn test_with_comments() {
let source = r#"
// This is a comment
fn dsp() {
/* multi-line
comment */
42
}
"#;
let (ast, tokens, _preparsed, _arena, errors) = parse(source);
let has_comments = tokens.iter().any(|t| {
matches!(
t.kind,
TokenKind::SingleLineComment | TokenKind::MultiLineComment
)
});
assert!(has_comments);
match ast {
AstNode::Program { .. } => {}
_ => panic!("Expected Program node"),
}
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
}
#[test]
fn test_let_binding() {
let source = "let x = 42";
let (ast, _tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { statements } => {
assert_eq!(statements.len(), 1);
match &statements[0] {
AstNode::LetDecl { name, value } => {
assert_eq!(name, "x");
match value.as_ref() {
AstNode::IntLiteral(n) => assert_eq!(*n, 42),
_ => panic!("Expected IntLiteral"),
}
}
_ => panic!("Expected LetDecl"),
}
}
_ => panic!("Expected Program node"),
}
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
}
#[test]
fn test_function_with_params() {
let source = "fn add(x, y) { x }";
let (ast, _tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { statements } => {
assert_eq!(statements.len(), 1);
match &statements[0] {
AstNode::FunctionDecl {
name,
params,
body: _,
} => {
assert_eq!(name, "add");
assert_eq!(params, &vec!["x".to_string(), "y".to_string()]);
}
_ => panic!("Expected FunctionDecl"),
}
}
_ => panic!("Expected Program node"),
}
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
}
#[test]
fn test_position_information() {
let source = "fn dsp() { 42 }";
let tokens = tokenize(source);
assert_eq!(tokens[0].kind, TokenKind::Function);
assert_eq!(tokens[0].text(source), "fn");
assert_eq!(tokens[0].start, 0);
assert_eq!(tokens[0].length, 2);
let num_token = tokens.iter().find(|t| t.kind == TokenKind::Int).unwrap();
assert_eq!(num_token.text(source), "42");
}
#[test]
fn test_unexpected_closing_token_does_not_stall() {
let source = "}";
let (ast, tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { .. } => {}
_ => panic!("Expected Program node"),
}
assert!(!tokens.is_empty());
assert!(
!errors.is_empty(),
"Expected parser errors for malformed input"
);
}
#[test]
fn test_nested_function_types_in_module_params() {
let source = r#"
mod fdn{
#stage(main)
fn zipwith(fun:(((float)->float),float)->float,left:[(float)->float],right:[float])->[float]{
0
}
}
"#;
let (ast, tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { statements } => {
assert!(!statements.is_empty());
}
_ => panic!("Expected Program node"),
}
assert!(!tokens.is_empty());
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
}
#[test]
fn test_malformed_module_body_does_not_stall() {
let source = r#"
mod fdn{
fn broken(x:){
0
}
}
"#;
let (ast, tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { .. } => {}
_ => panic!("Expected Program node"),
}
assert!(!tokens.is_empty());
assert!(
!errors.is_empty(),
"Expected parser errors for malformed module input"
);
}
#[test]
fn test_exact_module_zipwith_source() {
let source = "mod fdn{\n #stage(main)\n fn zipwith(fun:(((float)->float),float)->float,left:[(float)->float],right:[float])->[float]{\n 0\n }\n}\n";
let (ast, tokens, _preparsed, _arena, errors) = parse(source);
match ast {
AstNode::Program { statements } => {
assert!(!statements.is_empty());
}
_ => panic!("Expected Program node"),
}
assert!(!tokens.is_empty());
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
}
#[test]
fn test_exact_module_zipwith_cst_print() {
let source = "mod fdn{\n #stage(main)\n fn zipwith(fun:(((float)->float),float)->float,left:[(float)->float],right:[float])->[float]{\n 0\n }\n}\n";
let tokens = tokenize(source);
let preparsed = preparse(&tokens);
let (green_id, arena, tokens, errors) = parse_cst(tokens, &preparsed);
assert!(errors.is_empty(), "Expected no errors, got {errors:?}");
let tree_output = arena.print_tree(green_id, &tokens, source, 0);
assert!(tree_output.contains("FunctionDecl"));
assert!(tree_output.contains("zipwith"));
}
}