Skip to main content

Overview

Full Moon provides comprehensive error handling with two main error types and utilities for error recovery. Whether you’re building a strict compiler or a forgiving IDE, full-moon has you covered.

Error Types

Full Moon uses a unified Error enum that encompasses both tokenization and parsing errors:
pub enum Error {
    /// Triggered if there's an issue creating an AST (tokenizing succeeded)
    AstError(ast::AstError),
    /// Triggered if there's an issue when tokenizing
    TokenizerError(tokenizer::TokenizerError),
}

When Each Error Occurs

1

TokenizerError

Occurs when the input cannot be broken into valid tokens (lexical errors).Examples:
  • Invalid escape sequences in strings
  • Malformed numbers
  • Unclosed string literals
2

AstError

Occurs when tokens don’t form valid Lua syntax (parse errors).Examples:
  • Missing then after if condition
  • Unexpected end keyword
  • Invalid expression syntax

Basic Error Handling

Checking for Errors

use full_moon::parse;

let result = parse("local x = ");

match result {
    Ok(ast) => {
        println!("Parsed successfully!");
        println!("Code: {}", ast);
    }
    Err(errors) => {
        println!("Found {} error(s)", errors.len());
        for error in errors {
            eprintln!("Error: {}", error);
        }
    }
}

Extracting Error Information

use full_moon::Error;

fn handle_error(error: &Error) {
    // Get human-readable message
    let message = error.error_message();
    println!("Message: {}", message);
    
    // Get position range
    let (start, end) = error.range();
    println!("Position: line {}, column {} to line {}, column {}",
        start.line(),
        start.character(),
        end.line(),
        end.character()
    );
}

Error Recovery with parse_fallible

For tools like linters, formatters, and IDEs, you often want to parse incomplete or invalid code:
use full_moon::{parse_fallible, LuaVersion};

let code = "if x == 2 code()";
let result = parse_fallible(code, LuaVersion::new());

// ALWAYS produces an AST, even with errors
let ast = result.ast();
println!("AST: {}", ast);

// Check if there were errors
if !result.errors().is_empty() {
    println!("Recovered from {} error(s)", result.errors().len());
    
    for error in result.errors() {
        eprintln!("Error: {}", error.error_message());
    }
}
parse_fallible may insert phantom tokens (like missing then keywords) to create a valid AST structure. These phantom tokens have null positions.

Working with Positions

Error positions help you highlight problematic code:
use full_moon::tokenizer::Position;
use full_moon::Error;

fn print_error_context(error: &Error, source: &str) {
    let (start, end) = error.range();
    
    // Get the problematic line
    let lines: Vec<&str> = source.lines().collect();
    if let Some(line) = lines.get(start.line() - 1) {
        eprintln!("{}| {}", start.line(), line);
        
        // Print error indicator
        let spaces = " ".repeat(start.character() - 1);
        let carets = "^".repeat(
            (end.character() - start.character()).max(1)
        );
        eprintln!(" {} {}", spaces, carets);
    }
    
    eprintln!("Error: {}", error.error_message());
}

Example Output

1| local x = 
           ^
Error: expected expression

Distinguishing Error Types

use full_moon::Error;

fn categorize_error(error: &Error) {
    match error {
        Error::TokenizerError(err) => {
            println!("Lexical error: {}", err);
            // Handle tokenizer-specific errors
        }
        Error::AstError(err) => {
            println!("Syntax error: {}", err.error_message());
            // Handle AST-specific errors
        }
    }
}

Building Error-Tolerant Tools

IDE-Style Parsing

For IDE features like syntax highlighting, you want maximum error recovery:
use full_moon::{parse_fallible, LuaVersion};

fn ide_parse(source: &str) -> (full_moon::ast::Ast, Vec<Diagnostic>) {
    let result = parse_fallible(source, LuaVersion::new());
    
    let diagnostics = result.errors().iter().map(|err| {
        let (start, end) = err.range();
        Diagnostic {
            message: err.error_message().to_string(),
            severity: Severity::Error,
            start_line: start.line(),
            start_col: start.character(),
            end_line: end.line(),
            end_col: end.character(),
        }
    }).collect();
    
    (result.into_ast(), diagnostics)
}

struct Diagnostic {
    message: String,
    severity: Severity,
    start_line: usize,
    start_col: usize,
    end_line: usize,
    end_col: usize,
}

enum Severity {
    Error,
    Warning,
}

Compiler-Style Parsing

For compilers, you want strict validation:
use full_moon::parse;

fn compile(source: &str) -> Result<CompiledOutput, String> {
    let ast = parse(source).map_err(|errors| {
        errors.iter()
            .map(|e| e.error_message().to_string())
            .collect::<Vec<_>>()
            .join("\n")
    })?;
    
    // Proceed with compilation
    Ok(compile_ast(ast))
}

Phantom Tokens

When using parse_fallible, full-moon may insert phantom tokens:
use full_moon::{parse_fallible, LuaVersion};
use full_moon::node::Node;

let code = "if x == 2 code()";
let result = parse_fallible(code, LuaVersion::new());
let ast = result.ast();

// The AST will have a phantom `then` token
for stmt in ast.nodes().stmts() {
    if let Stmt::If(if_stmt) = stmt {
        let then_token = if_stmt.then_token();
        
        // Check if token is phantom (null position)
        if then_token.start_position().is_none() {
            println!("Phantom token detected");
        }
    }
}

Updating Phantom Positions

If you need valid positions for phantom tokens:
use full_moon::{parse_fallible, LuaVersion};

let mut ast = parse_fallible(code, LuaVersion::new()).into_ast();

// Update positions for phantom tokens
ast = ast.update_positions();
update_positions() calculates reasonable positions for phantom tokens based on surrounding tokens.

Best Practices

Use parse for validation

When you need to ensure code is completely valid, use parse() which fails on any error.

Use parse_fallible for tooling

For linters, formatters, and IDEs, use parse_fallible() to work with incomplete code.

Always show error positions

Use error.range() to highlight the exact location of errors in the source code.

Collect all errors

Both functions return Vec<Error>, so you can show all errors at once.

Error Display Formats

Simple Format

use full_moon::Error;

fn simple_error(error: &Error) {
    println!("{}", error);
}
// Output: "error occurred while creating ast: unexpected token"

Detailed Format

use full_moon::Error;

fn detailed_error(error: &Error, filename: &str) {
    let (start, end) = error.range();
    println!("{}:{}:{}: error: {}",
        filename,
        start.line(),
        start.character(),
        error.error_message()
    );
}
// Output: "script.lua:3:15: error: expected 'then'"

Integration with Error Reporting Libraries

Using codespan-reporting

use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term;
use full_moon::Error;

fn report_error(error: &Error, source: &str, filename: &str) {
    let mut files = SimpleFiles::new();
    let file_id = files.add(filename, source);
    
    let (start, end) = error.range();
    
    let diagnostic = Diagnostic::error()
        .with_message(error.error_message().to_string())
        .with_labels(vec![
            Label::primary(file_id, start.bytes()..end.bytes())
        ]);
    
    let writer = term::termcolor::StandardStream::stderr(
        term::termcolor::ColorChoice::Auto
    );
    let config = codespan_reporting::term::Config::default();
    
    term::emit(&mut writer.lock(), &config, &files, &diagnostic).unwrap();
}

Next Steps

Parsing Guide

Learn more about parsing functions

Static Analysis Example

Build a tool with proper error handling

Build docs developers (and LLMs) love