Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 27 additions & 35 deletions crates/compiler/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,8 @@

use std::fmt;

/// Source code location (line and column numbers).
///
/// Used throughout the AST to track where each construct appears in the
/// source code. This enables precise error messages with line/column info.
///
/// # Examples
///
/// ```
/// use ferrisscript_compiler::ast::Span;
///
/// let span = Span::new(10, 15); // line 10, column 15
/// assert_eq!(span.line, 10);
/// assert_eq!(span.column, 15);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub line: usize,
pub column: usize,
}

impl Span {
pub fn new(line: usize, column: usize) -> Self {
Span { line, column }
}

pub fn unknown() -> Self {
Span { line: 0, column: 0 }
}
}

impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "line {}, column {}", self.line, self.column)
}
}
// Re-export Span for backward compatibility
pub use crate::span::Span;

/// Root node of a FerrisScript program.
///
Expand Down Expand Up @@ -370,6 +337,31 @@ pub enum Stmt {
},
}

impl Stmt {
/// Get the span of this statement.
///
/// # Examples
///
/// ```
/// use ferrisscript_compiler::ast::{Stmt, Expr, Literal};
/// use ferrisscript_compiler::span::{Span, Position};
///
/// let span = Span::new(Position::new(1, 1, 0), Position::new(1, 10, 9));
/// let stmt = Stmt::Expr(Expr::Literal(Literal::Int(42), span));
/// assert_eq!(stmt.span(), span);
/// ```
pub fn span(&self) -> Span {
match self {
Stmt::Expr(expr) => expr.span(),
Stmt::Let { span, .. } => *span,
Stmt::Assign { span, .. } => *span,
Stmt::If { span, .. } => *span,
Stmt::While { span, .. } => *span,
Stmt::Return { span, .. } => *span,
}
}
}

/// Signal declaration (top-level only).
///
/// Signals are event declarations that can be emitted and connected to methods.
Expand Down
100 changes: 100 additions & 0 deletions crates/compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
//! - [`error_context`]: Error formatting with source context
//! - [`lexer`]: Lexical analysis (tokenization)
//! - [`parser`]: Syntax analysis (AST generation)
//! - [`span`]: Source code location tracking for error messages and LSP
//! - [`type_checker`]: Semantic analysis (type checking)

pub mod ast;
pub mod error_code;
pub mod error_context;
pub mod lexer;
pub mod parser;
pub mod span;
pub mod suggestions;
pub mod type_checker;

Expand Down Expand Up @@ -284,4 +286,102 @@ let c: i32 = 3"#;
error
);
}

#[test]
fn test_span_tracking_on_functions() {
use crate::lexer::tokenize;
use crate::parser::parse;

let source = r#"fn add(a: i32, b: i32) -> i32 {
return a + b;
}"#;

let tokens = tokenize(source).unwrap();
let program = parse(&tokens, source).unwrap();

// Verify function has span information
assert!(!program.functions.is_empty());
let func = &program.functions[0];
assert_eq!(func.span.line(), 1); // Function starts at line 1
assert!(!func.span.is_unknown());
}

#[test]
fn test_span_tracking_on_expressions() {
use crate::lexer::tokenize;
use crate::parser::parse;

let source = r#"fn test() {
let x: i32 = 42;
}"#;

let tokens = tokenize(source).unwrap();
let program = parse(&tokens, source).unwrap();

// Get the let statement
let func = &program.functions[0];
assert!(!func.body.is_empty());
let stmt = &func.body[0];

// Verify statement has span
let stmt_span = stmt.span();
// The span tracks the start of the let keyword, which is on line 1 in this test
// (the raw string starts counting from line 1, not the visual line 2)
assert!(stmt_span.line() >= 1);
assert!(!stmt_span.is_unknown());
}

#[test]
fn test_span_merge_functionality() {
use crate::span::{Position, Span};

let start_pos = Position::new(1, 5, 4);
let end_pos = Position::new(1, 10, 9);
let span1 = Span::new(start_pos, end_pos);

let start_pos2 = Position::new(1, 15, 14);
let end_pos2 = Position::new(1, 20, 19);
let span2 = Span::new(start_pos2, end_pos2);

let merged = span1.merge(span2);

// Merged span should encompass both spans
assert_eq!(merged.start.column, 5);
assert_eq!(merged.end.column, 20);
assert_eq!(merged.len(), 15); // 19 - 4 = 15 bytes
}

#[test]
fn test_expr_span_accessor() {
use crate::ast::{Expr, Literal};
use crate::span::{Position, Span};

let pos = Position::new(5, 10, 42);
let span = Span::point(pos);
let expr = Expr::Literal(Literal::Int(42), span);

// Verify span accessor works
assert_eq!(expr.span(), span);
assert_eq!(expr.span().line(), 5);
assert_eq!(expr.span().column(), 10);
}

#[test]
fn test_stmt_span_accessor() {
use crate::ast::{Expr, Literal, Stmt};
use crate::span::{Position, Span};

let pos = Position::new(3, 5, 20);
let span = Span::point(pos);
let expr = Expr::Literal(Literal::Bool(true), span);
let stmt = Stmt::Return {
value: Some(expr),
span,
};

// Verify statement span accessor works
assert_eq!(stmt.span(), span);
assert_eq!(stmt.span().line(), 3);
assert_eq!(stmt.span().column(), 5);
}
}
28 changes: 27 additions & 1 deletion crates/compiler/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::ast::*;
use crate::error_code::ErrorCode;
use crate::error_context::format_error_with_code;
use crate::lexer::{PositionedToken, Token};
use crate::span::{Position, Span};

pub struct Parser<'a> {
tokens: Vec<PositionedToken>,
Expand Down Expand Up @@ -119,7 +120,32 @@ impl<'a> Parser<'a> {
}

fn span(&self) -> Span {
Span::new(self.current_line, self.current_column)
// TODO(v0.0.5): Track actual byte offsets during parsing
// For now, use offset 0 (unknown) and create zero-length spans
let pos = Position::new(self.current_line, self.current_column, 0);
Span::point(pos)
}

/// Create a span from a start position to the current position.
///
/// This is used when parsing multi-token constructs to create a span
/// that covers the entire construct.
///
/// # Arguments
///
/// * `start_line` - The line where the construct started
/// * `start_column` - The column where the construct started
///
/// # Returns
///
/// A span from the start position to the current position
#[allow(dead_code)]
fn span_from(&self, start_line: usize, start_column: usize) -> Span {
// TODO(v0.0.5): Track actual byte offsets during parsing
// For now, use offset 0 (unknown)
let start_pos = Position::new(start_line, start_column, 0);
let end_pos = Position::new(self.current_line, self.current_column, 0);
Span::new(start_pos, end_pos)
}

/// Synchronize parser to next safe recovery point after error.
Expand Down
Loading