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
49 changes: 23 additions & 26 deletions src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! Parser module for the TeaLang compiler front-end.
//!
//! This module is responsible for transforming a raw TeaLang source string into
//! a typed Abstract Syntax Tree (AST). It uses the [pest] PEG parser generator
//! to tokenise and structurally parse the source according to the grammar
//! defined in `tealang.pest`, and then walks the resulting parse tree to build
//! the AST types defined in [`crate::ast`].
//! Transforms a raw TeaLang source string into a typed Abstract Syntax Tree
//! (AST). The [pest] PEG parser generator tokenises and structurally parses
//! the source according to the grammar defined in `tealang.pest`; the
//! resulting parse tree is then lowered into the AST types defined in
//! [`crate::ast`].
//!
//! # Main entry points
//! * [`Parser`] – the public façade that implements [`crate::common::Generator`].
Expand Down Expand Up @@ -34,10 +34,9 @@ use self::common::{grammar_error_static, ParseResult, Rule, TeaLangParser};

/// Public parser that turns a TeaLang source string into an AST.
///
/// After construction with [`Parser::new`] you must call
/// [`Generator::generate`] before accessing the [`Parser::program`] field.
/// [`Generator::generate`] must be called after construction before
/// accessing the [`Parser::program`] field.
pub struct Parser<'a> {
/// The raw TeaLang source text to be parsed.
input: &'a str,
/// The parsed AST program, populated by [`Generator::generate`].
/// `None` until `generate` completes successfully.
Expand All @@ -47,8 +46,8 @@ pub struct Parser<'a> {
impl<'a> Parser<'a> {
/// Creates a new `Parser` for the given source string.
///
/// The parser is not yet run; call [`Generator::generate`] to perform
/// parsing and populate [`Parser::program`].
/// Parsing is not run yet; [`Generator::generate`] performs parsing and
/// populates [`Parser::program`].
pub fn new(input: &'a str) -> Self {
Self {
input,
Expand Down Expand Up @@ -78,46 +77,44 @@ impl<'a> Generator for Parser<'a> {
let ast = self
.program
.as_ref()
// Guard: generate() must be called before output().
.ok_or_else(|| grammar_error_static("output before generate"))?;
write!(w, "{ast}")?;
Ok(())
}
}

/// Internal context that owns a single parse pass over one source string.
///
/// `ParseContext` is constructed by [`Parser`] and carries the source slice so
/// that all parser helper methods can reference it if needed.
pub(crate) struct ParseContext<'a> {
#[allow(dead_code)]
/// The original source text being parsed.
input: &'a str,
}

impl<'a> ParseContext<'a> {
/// Creates a new `ParseContext` for the given source string.
fn new(input: &'a str) -> Self {
Self { input }
}

/// Parses the full source string into a boxed [`ast::Program`].
///
/// Uses [`TeaLangParser`] to produce a parse tree for the `program` rule,
/// then iterates over top-level nodes to collect `use` statements and
/// program elements (variable declarations, struct definitions, function
/// declarations and definitions).
fn parse(&self) -> ParseResult<Box<ast::Program>> {
// Run the pest parser; convert any pest::Error into Error::Syntax.
let pairs = <TeaLangParser as PestParser<Rule>>::parse(Rule::program, self.input)
.map_err(|e| Error::Syntax(e.to_string()))?;
.map_err(|e| {
// Pest reports either a single position or a span; the span's
// start position populates the structured fields.
let (line, column) = match e.line_col {
pest::error::LineColLocation::Pos(pos) => pos,
pest::error::LineColLocation::Span(start, _) => start,
};
Error::Syntax {
line,
column,
message: e.to_string(),
}
})?;

let mut use_stmts = Vec::new();
let mut elements = Vec::new();

for pair in pairs {
if pair.as_rule() == Rule::program {
// Walk the top-level children of the `program` node.
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::use_stmt => {
Expand All @@ -128,7 +125,7 @@ impl<'a> ParseContext<'a> {
elements.push(*elem);
}
}
// End-of-input marker; nothing to do.
// End-of-input marker.
Rule::EOI => {}
_ => {}
}
Expand Down
71 changes: 45 additions & 26 deletions src/parser/common.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
//! Shared definitions and helpers for the TeaLang parser.
//!
//! Everything in this submodule is `pub(crate)` infrastructure used by the
//! other parser submodules: the [`Error`] type and [`ParseResult`] alias, the
//! pest-derived [`TeaLangParser`] (generated from `tealang.pest`) together
//! with the [`Pair`] parse-tree node alias, and helper functions for building
//! [`Error::Grammar`] values with source positions, collapsing source
//! snippets into compact previews, and parsing integer literals.

use pest_derive::Parser as DeriveParser;

/// Errors that can be produced during parsing of a TeaLang source file.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// A syntax error reported directly by the PEG parser (pest).
/// The inner `String` contains the human-readable pest error message.
#[error("{0}")]
Syntax(String),
/// `line` and `column` give the 1-based position where the error was
/// detected; `message` contains the human-readable pest error text,
/// which itself renders the offending source line and position.
#[error("{message}")]
Syntax {
line: usize,
column: usize,
message: String,
},

/// An integer literal that could not be parsed as a valid `i32`.
/// Includes the original source text, its position, and the underlying
Expand All @@ -25,10 +40,16 @@ pub enum Error {
Io(#[from] std::io::Error),

/// The parse tree had an unexpected structure at the given location.
/// The inner `String` names the grammar rule or context where the
/// unexpected structure was found.
#[error("unexpected parse tree structure in {0}")]
Grammar(String),
/// `message` names the grammar rule or context where the unexpected
/// structure was found, followed by a compact source snippet. A
/// `line`/`column` of `0` means that no position was available
/// (see [`grammar_error_static`]).
#[error("unexpected parse tree structure in {message} at line {line}, column {column}")]
Grammar {
line: usize,
column: usize,
message: String,
},
}

/// The pest-derived parser for the TeaLang grammar.
Expand All @@ -39,13 +60,9 @@ pub enum Error {
pub(crate) struct TeaLangParser;

/// A specialized `Result` type used throughout the parser.
/// `Ok` carries a successfully parsed value of type `T`; `Err` carries an
/// [`Error`] describing what went wrong.
pub(crate) type ParseResult<T> = Result<T, Error>;

/// A single node in the pest parse tree, parameterised by the input lifetime.
/// This is a type alias for [`pest::iterators::Pair`] bound to the [`Rule`]
/// enum produced by [`TeaLangParser`].
pub(crate) type Pair<'a> = pest::iterators::Pair<'a, Rule>;

/// Collapses a raw source snippet into a compact, single-line preview string
Expand All @@ -57,9 +74,7 @@ pub(crate) type Pair<'a> = pest::iterators::Pair<'a, Rule>;
pub(crate) fn compact_snippet(snippet: &str) -> String {
const MAX_CHARS: usize = 48;

// Collapse all whitespace sequences into a single space.
let compact = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
// Fall back to trimming if the split produced nothing (e.g., all whitespace).
let normalized = if compact.is_empty() {
snippet.trim().to_string()
} else {
Expand All @@ -70,7 +85,6 @@ pub(crate) fn compact_snippet(snippet: &str) -> String {
return "<empty>".to_string();
}

// Take up to MAX_CHARS characters; append "..." if the string is longer.
let mut chars = normalized.chars();
let preview: String = chars.by_ref().take(MAX_CHARS).collect();
if chars.next().is_some() {
Expand All @@ -87,35 +101,40 @@ pub(crate) fn compact_snippet(snippet: &str) -> String {
/// or function where the unexpected structure was encountered.
pub(crate) fn grammar_error(context: &'static str, pair: &Pair<'_>) -> Error {
let span = pair.as_span();
// Extract line and column numbers from the start of the span.
let (line, column) = span.start_pos().line_col();
let near = compact_snippet(span.as_str());

Error::Grammar(format!(
"{context} at line {line}, column {column}, near `{near}`"
))
Error::Grammar {
line,
column,
message: format!("{context} near `{near}`"),
}
}

/// Creates a [`Error::Grammar`] variant from a static string alone, without
/// access to a specific parse-tree node.
///
/// Use this when position information is unavailable (e.g., when validating
/// program state rather than a particular source span).
/// program state rather than a particular source span). The `line` and
/// `column` fields are set to `0` as a "position unavailable" sentinel.
pub(crate) fn grammar_error_static(context: &'static str) -> Error {
Error::Grammar(context.to_string())
Error::Grammar {
line: 0,
column: 0,
message: context.to_string(),
}
}

/// Returns the byte offset of the start of `pair`'s span within the source
/// string. This is used to track source positions in AST nodes.
/// Byte offset of the start of `pair`'s span; recorded as the source position
/// of AST nodes.
pub(crate) fn get_pos(pair: &Pair<'_>) -> usize {
pair.as_span().start()
}

/// Parses an integer literal from a `num` parse-tree node.
/// Parses the text of a `num` parse-tree node as an `i32`.
///
/// Reads the raw text of `pair`, attempts to parse it as an `i32`, and wraps
/// any failure in [`Error::InvalidNumber`] that includes the literal text and
/// its source position.
/// Failures return [`Error::InvalidNumber`] with the literal text and its
/// source position.
pub(crate) fn parse_num(pair: Pair) -> ParseResult<i32> {
let literal = pair.as_str().to_string();
let (line, column) = pair.as_span().start_pos().line_col();
Expand Down
Loading